✍️ Forgery
Forgery is a Solidity web-server runtime written in Rust and based on the foundry-rs
stack by Paradigm. It implements a fully-featured HTTP framework alongside
Foundry's suite of tools and cheatcodes. If you are unfamiliar with Foundry,
it is highly recommended to read up on it, and specifically
Forge, Foundry's scripting environment.
🤔 Wtf
Write Solidity code as your backend, while running on a forked Foundry environment. This lets you interact with contracts directly, efficiently retrieve information onchain and use Foundry's cheatcodes to simulate transactions, mock internal calls and more.
Web3 backends today are usually written in languages not fit for smart contract interactions. JSON ABIs and awkward async APIs encourage convoluted code with inconsistent behavior. Instead, why not write Solidity directly, use Solidity interfaces and execute in a completely synchronous way, excatly how it would behave onchain.
For example, a Uniswap price endpoint would be trivially implemented in Forgery:
contract Index is Server {
using JSONBodyParser for Request;
using JSONBodyWriter for Response;
QuoterV2 quoter = QuoterV2(0x61fFE014bA17989E743c5F6cB21bF9697530B21e);
function start () external override {
router.post('/quote', quote);
}
function quote (
Request calldata request
) public {
address[] memory tokens = request.json().at('tokens').asAddressArray();
uint amountIn = request.json().at('amountIn').asUint();
uint fee = request.json().at('fee').asUint();
(uint amountOut,,,) = quoter.quoteExactInputSingle(
QuoterV2.QuoteExactInputSingleParams({
tokenIn: tokens[0],
tokenOut: tokens[1],
amountIn: amountIn,
fee: uint24(fee),
sqrtPriceLimitX96: 0
})
);
response.status = 200;
response.header('content-type', 'application/json');
response.write('amountOut', amountOut);
}
}
Anyone who has tried implementing the same feature using Node.js, Python or Rust, knows how awkward and involved it would be.
Forgery is the native web3 backend.
🔨 How it works
Under the hood Forgery is based on Foundry and Hyper. Together a new Solidity runtime is born, one that merges web2 and web3 seamlessly.
Forgery sets up a low overhead Hyper instance while running a Foundry EVM instance on the main thread. Your contracts are deployed on the Foundry EVM instance and the Hyper server communicates with your contracts by broadcasting transations.
Every request is converted into ABI-encoded transaction using Alloy, which is then passed into the Solidity runtime as calldata. The contract responds with an ABI-encoded response which is converted into an HTTP response that is then sent back to the user.
In addition, Forgery SDK is a framework which lets you write web-servers intuitively in a familiar way akin to other popular web frameworks.
Core API
The Forgery Core API is a simple Solidity interface that must be implemented in order for Forgery to communicate properly with your backend.
The API is documented here for posterity but is not intended to be used directly.
Webserver interface
Forgery requires you to implement the following:
- You entrypoint contract must be located at
./src/Index.sol
- The contract must implement a
start()
function - The contract must implement a
server(Request calldata) returns (Response memory)
function
Here is the expected interface:
interface ForgeryServer {
struct SolHttpHeader {
string key;
string value;
}
struct SolHttpRequest {
string method;
string uri;
SolHttpHeader[] headers;
bytes body;
}
struct SolHttpResponse {
uint16 status;
SolHttpHeader[] headers;
bytes body;
}
function start () external;
function serve (SolHttpRequest calldata) external returns (SolHttpResponse memory);
}
start()
This method is called on deployment. Whenever you start up your server, your contract is deployed on the Foundry instance. This method will run once, right after the server starts up, but before it starts listening on any connections.
serve()
This method will be executed for every incoming request. This is the main entrypoint into your backend. It is usually recommended to use some sort of router helper to help manage different endpoint, such as the one available in the Forgery SDK.
Installation
Installation is done from source currently. We provide a helper script that will install Forgery using cargo by cloning the repo and compiling locally. It might take a couple of minutes to complete.
Requirements
You will need the following tools installed:
- bash
- curl
- git
- Rust + Cargo
Installation
Helper
Use the helper script:
curl https://github.com/Tudmotu/forgery-rs/blob/main/getforgery.sh | bash
Manually
Clone the git repo:
git clone https://github.com/Tudmotu/forgery-rs.git
Inside the repo, install using Cargo:
cargo install --locked --path .
Optionally, add the Cargo binary directory to your $PATH
if it's not already
included:
echo 'export PATH=$PATH:~/.cargo/bin' >> ~/.zshrc
This command might vary depending on your OS and shell. This instruction assumes a POSIX OS with zsh.
Quickstart
To start writing Forgery backends, you will first need to install Forgery.
If you are following the Core API, all you need is to
implement the src/Index.sol
contract. This is not recommended ― the preferred
method is using a framework such as Forgery SDK.
Otherwise, it is recommended to use the Forgery boilerplate. The boilerplate includes a simple example contract with tests, which should get you up an running in no time.
Technically speaking, a Forgery project is simply a Foundry Forge project. This
means you can configure it using foundry.toml
and install dependencies using
forge install
.
The forgery
command includes a utility for generating a basic example project
based on the forgery-boilerplate
repo. This utility requires forge
to be
installed. Follow the Foundry docs
for instructions. Once cloned, treat the Forgery project like any other Forge project.
forgery init
This command will generate the project inside the current working directory,
similar to forge init
.
Make sure to create a .env
file with FORGERY_RPC
configured.
Now all that is left is to start modifying Index.sol
to implement the desired
logic.
Forgery SDK
The Forgery SDK is meant to be a fully-fledged web framework for Forgery.
Its use is optional, though highly recommended.
It introduces a simple API that would be somewhat familiar for those coming with a background in other web frameworks.
Overview
The main entry point implements an abstract Server
contract. The Server
contract utilizes a Router
to direct requests to their endpoint
implementations.
The Request
object is kept in the calldata to reduce overhead, while the
Response
object is copied into contract storage for easier manipulation.
In addition, Forgery SDK provides two utilities for working with JSONs:
JSONBodyParser
and JSONBodyWriter
. These are meant to help parse & generate
JSON objects from the Request
and Response
objects respectively.
Other than that, the Server
contract exposes the Foundry cheatcodes via vm
,
similar to Forge scripts or tests.
Hello, world
A basic example of a Forgery SDK contract might look like so:
contract Index is Server {
function start () external override {
router.get('/hello', hello);
}
function hello (
Request calldata request
) internal {
response.status = 200;
response.header('content-type', 'text/plain');
response.body = 'Hello, world';
}
}
Server contract
The Server
contract implements the basic structure of a Forgery app.
It instantiates a Router
for you which lets you easily map paths to functions.
The Router
expects functions with (Request calldata)
signatures.
To use the Server
contract, import it and inherit from it:
import 'forgery-sdk/Server.sol';
contract Index is Server {
function start () external override {
router.get('/hello', hello);
}
function hello (
Request calldata request
) public {
// ...
}
}
The start()
function is executed when the server starts up. This is where you
would usually register your routes using the Router
.
Router
The Router
object lets you map between routes and functions.
It is a user-defined Solidity type which saves a mapping of paths to functions
per HTTP method.
The Router
object expects your routes to comply with the following signature:
function routeName (Request calldata) external;
The router does not execute routes on its own. It is only used as an abstraction
over a nested struct datastructure. Its purpose is convenience. The actual
executor is the Server
contract.
The Router
API exposes functions to register routes for each HTTP method. You
would usually register the routes in the start()
function.
Example
import 'forgery-sdk/Server.sol';
contract Index is Server {
function start () external override {
router.get('/query', queryOrders);
router.post('/create', createOrder);
router.put('/replace', replaceOrder);
router.patch('/update', updateOrder);
router.del('/delete', deleteOrder);
}
}
Request & Response
The Request
and Response
objects are simple structs with added user-defined
Solidity methods.
These structs are very simple and have the following definitions:
struct Header {
string key;
string value;
}
struct Request {
string method;
string uri;
Header[] headers;
bytes body;
}
struct Response {
uint16 status;
Header[] headers;
bytes body;
}
In addition, they each have a method to help read/write headers.
Request
contract Index is Server {
// ...
function hello (
Request calldata request
) public {
string memory contentType = request.header('content-type');
}
}
Response
contract Index is Server {
// ...
function hello (
Request calldata request
) public {
response.header('content-type', 'application/json');
}
}
JSONBodyParser
The JSONBodyParser
is a library meant to be used in conjunction with the
Request
struct. This library adds convenience methods for reading JSON-encoded
request bodies.
The implementation is based on Foundry JSON manipulation cheatcodes and therefore should be relatively performant.
The API should feel similar to other typed-languages JSON handling utils.
Example
Let's assume the request contains the following JSON body:
{
"user": {
"address": "0x61880628e88b391C0161225887D65087EF5bD19B",
"ens": "dog.eth"
},
"tokens": [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
]
}
We could read the values like so:
import 'forgery-sdk/JSONBodyParser.sol';
contract Index is Server {
using JSONBodyParser for Request;
// ...
function myRoute (
Request calldata request
) public {
address userAddress = request.json().at('user').at('address').asAddress();
string memory ens = request.json().at('user').at('ens').asString();
address[] memory tokens = request.json().at('tokens').asAddressArray();
// ...
}
}
JSONBodyWriter
The JSONBodyWriter
is a library meant to be used in conjunction with the
Response
struct. This library adds convenience methods for writing JSON-encoded
response bodies.
The implementation is based on Foundry JSON manipulation cheatcodes and therefore should be relatively performant.
This API is slightly awkward due to some Solidity and Foundry design choices.
The main inconvenience is encoding custom objects/structs into JSON which
requires a slightly more involved API. To learn how to serialize objects, check
out the Foundry documentation about vm.serializeJson()
.
Example
Let's assume we want to write the following JSON to the response body:
{
"user": {
"address": "0x61880628e88b391C0161225887D65087EF5bD19B",
"ens": "dog.eth"
},
"tokens": [
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
]
}
We would do it like so:
import 'forgery-sdk/JSONBodyWriter.sol';
contract Index is Server {
using JSONBodyWriter for Response;
// ...
function myRoute (
Request calldata request
) public {
// ...
address[] memory tokens = new address[](2);
tokens[0] = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
tokens[1] = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
response.write('tokens', tokens);
string memory userObject = vm.serializeAddress('user object', 'address', 0x61880628e88b391C0161225887D65087EF5bD19B);
userObject = vm.serializeString('user object', 'ens', 'dog.eth');
response.write('user', userObject);
}
}