Writing GSN-capable contracts

The Gas Station Network allows you to build apps where you pay for your users transactions, so they do not need to hold Ether to pay for gas, easing their onboarding process. In this guide, we will learn how to write smart contracts that can receive transactions from the GSN, by using OpenZeppelin Contracts.

If you’re new to the GSN, you probably want to first take a look at the overview of the system to get a clearer picture of how gasless transactions are achieved. Otherwise, strap in!

Receiving a Relayed Call

The first step to writing a recipient is to inherit from our GSNRecipient contract. If you’re also inheriting from other contracts, such as ERC20, this will work just fine: adding GSNRecipient will make all of your token functions GSN-callable.

import "@openzeppelin/contracts/GSN/GSNRecipient.sol";

contract MyContract is GSNRecipient, ... {

}

msg.sender and msg.data

There’s only one extra detail you need to take care of when working with GSN recipient contracts: you must never use msg.sender or msg.data directly. On relayed calls, msg.sender will be RelayHub instead of your user! This doesn’t mean however you won’t be able to retrieve your users' addresses: GSNRecipient provides two functions, _msgSender() and _msgData(), which are drop-in replacements for msg.sender and msg.data while taking care of the low-level details. As long as you use these two functions instead of the original getters, you’re good to go!

Third-party contracts you inherit from may not use these replacement functions, making them unsafe to use when mixed with GSNRecipient. If in doubt, head on over to our support forum.

Accepting and Charging

Unlike regular contract function calls, each relayed call has an additional number of steps it must go through, which are functions of the IRelayRecipient interface RelayHub will call on your contract. GSNRecipient includes this interface but no implementation: most of writing a recipient involves handling these function calls. They are designed to provide flexibility, but basic recipients can safely ignore most of them while still being secure and sound.

The OpenZeppelin Contracts provide a number of tried-and-tested approaches for you to use out of the box, but you should still have a basic idea of what’s going on under the hood.

acceptRelayedCall

First, RelayHub will ask your recipient contract if it wants to receive a relayed call. Recall that you will be charged for incurred gas costs by the relayer, so you should only accept calls that you’re willing to pay for!

    function acceptRelayedCall(
        address relay,
        address from,
        bytes calldata encodedFunction,
        uint256 transactionFee,
        uint256 gasPrice,
        uint256 gasLimit,
        uint256 nonce,
        bytes calldata approvalData,
        uint256 maxPossibleCharge
    ) external view returns (uint256, bytes memory);

There are multiple ways to make this work, including:

  1. having a whitelist of trusted users

  2. only accepting calls to an onboarding function

  3. charging users in tokens (possibly issued by you)

  4. delegating the acceptance logic off-chain

All relayed call requests can be rejected at no cost to the recipient.

In this function, you return a number indicating whether you accept the call (0) or not (any other number). You can also return some arbitrary data that will get passed along to the following two functions (pre and post) as an execution context.

pre and postRelayedCall

After a relayed call is accepted, RelayHub will give your contract two opportunities to charge your user for their call, perform some bookkeeping, etc.: before and after the actual relayed call is made. These functions are aptly named preRelayedCall and postRelayedCall.

function preRelayedCall(bytes calldata context) external returns (bytes32);

preRelayedCall will inform you of the maximum cost the call may have, and can be used to charge the user in advance. This is useful if the user may spend their allowance as part of the call, so you can lock some funds here.

    function postRelayedCall(
        bytes calldata context,
        bool success,
        uint256 actualCharge,
        bytes32 preRetVal
    ) external;

postRelayedCall will give you an accurate estimate of the transaction cost, making it a natural place to charge users. It will also let you know if the relayed call reverted or not. This allows you, for instance, to not charge users for reverted calls - but remember that you will be charged by the relayer nonetheless.

These functions allow you to implement, for instance, a flow where you charge your users for the relayed transactions in a custom token. You can lock some of their tokens in pre, and execute the actual charge in post. This is similar to how gas fees work in Ethereum: the network first locks enough ETH to pay for the transaction’s gas limit at its gas price, and then pays for what it actually spent.

Further reading

Read our guide on the payment strategies pre-built and shipped in OpenZeppelin Contracts, or check out the API reference of the GSN base contracts.