blue haired anime girl forging a letter

✍️ 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.

For most use cases it is probably best to use a framework such as Forgery SDK instead of directly interfacing with the Forgery Core API.
The API is documented here for posterity but is not intended to be used directly.

Webserver interface

Forgery requires you to implement the following:

  1. You entrypoint contract must be located at ./src/Index.sol
  2. The contract must implement a start() function
  3. 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
NOTE:
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);
    }
}