# Writing GSN-capable contracts

The Gas Station Network (opens new window) 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.

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!

# Install OpenGSN Contracts

Before you get started, install the OpenGSN contracts using either npm or yarn within your repository. Using yarn this will look like as follows:

yarn add @opengsn/contracts

Please bear in mind that at the time of writing, OpenGSN requires solidity version >=0.8.6.

# Receiving a Relayed Call

The first step to writing a recipient is to inherit from our ERC2771Recipient contract. If you're also inheriting from OpenZeppelin contracts (opens new window), such as ERC20 or ERC721, this will work just fine: adding ERC2771Recipient to your token contracts will make them GSN-callable.

import "@opengsn/contracts/src/ERC2771Recipient.sol";

contract MyContract is ERC2771Recipient {
    ...
}

# _msgSender, not msg.sender

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 the Forwarder contract instead of your user! This doesn't mean however you won't be able to retrieve your users' addresses: ERC2771Recipient provides _msgSender(), which is a drop-in replacement for msg.sender that takes care of the low-level details. As long as you use this function instead of the original msg.sender, you're good to go!

WARNING

Third-party contracts you inherit from may not use these replacement functions, making them unsafe to use when mixed with ERC2771Recipient. If in doubt, head on over to our Discord support group (opens new window).

# Paying for your user's meta-transaction

The relays in GSN are not running a charity. In order to cover their expenses, the transaction costs will be charged from a balance of a special contract, called Paymaster.

In order to start paying the meta-transaction fees, create a contract that inherits from BasePaymaster. You will be required to provide implementations of 2 methods: preRelayedCall and postRelayedCall (their implementations will be discussed in the next paragraph).

Once the contract is deployed to your network and configured with the RelayHub address, you will need to maintain its balance on the RelayHub. Read more about it here.

# Example Paymaster contract that pays for gas in ERC20 tokens

One of the most commonly requested features in Ethereum is the ability to pay gas fees in ERC20 tokens.

A reference implementation exists in the TokenPaymaster (opens new window) contract.

uml diagram

# Rejecting meta-transactions and alternative gas charging methods

Unlike regular contract function calls, each relayed call has an additional number of steps it must go through, which are functions of the Paymaster interface that RelayHub will call before and after calling your contract. These steps are designed to provide flexibility, but a basic Paymaster can safely ignore most of them while still being secure and sound.

# getGasLimits and acceptanceBudget

First, RelayHub will ask your Paymaster contract how much gas does it require to execute all the logic in the preRelayedCall and postRelayedCall methods.

But by far the most important value returned by this method is GasLimits.acceptanceBudget. Your Paymaster will be charged for the transaction after it consumes this amount of gas, even if it reverts the calls to either preRelayedCall or postRelayedCall.

WARNING

Make sure you understand the logic before overriding the default values from the BasePaymaster.

function getGasLimits()
external
view
returns (
    GasLimits memory limits
);

# pre and postRelayedCall

Next, RelayHub will ask your Paymaster 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 preRelayedCall(
    GsnTypes.RelayRequest relayRequest,
    bytes approvalData,
    uint256 maxPossibleGas
)
external
returns (
    bytes memory context,
    bool rejectOnRecipientRevert
);

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.

This function should revert if your paymaster decides to not accept the relayed call. You can also return some arbitrary data that will be passed along to the postRelayedCall as an execution context.

The parameter called maxPossibleGas defines the absolute maximum the entire operation may cost to the Paymaster. This is useful if the user may spend their gas allowance as part of the relayed call itself, so you can pre-lock some funds here.

After a relayed call is accepted, RelayHub will give your Paymaster contract another opportunity to charge your user for their call, perform some bookkeeping, etc. after the actual relayed call is made. This function is aptly named postRelayedCall.

function postRelayedCall(
    bytes context,
    bool success,
    bytes32 preRetVal,
    uint256 gasUseWithoutPost,
    GsnTypes.RelayData calldata relayData
) external;

postRelayedCall will give you an accurate estimate of the transaction cost (excluding the gas needed for postRelayedCall itself), 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.

# Delegating the preRelayedCall logic to Recipient via the rejectOnRecipientRevert flag

You may have noticed that preRelayedCall has a boolean return parameter called rejectOnRecipientRevert. If set to true, this flag allows your Paymaster to delegate the decision of whether to pay for the relayed call or not to the Recipient.

Note that the Paymaster will pay in one of two scenarios:

  • The Recipient call is successful
  • The Recipient call is reverted but (taken together with preRelayedCall) it consumed more than acceptanceBudget gas.

Only use it if you write and audit both the Paymaster and Recipient and these two components can trust each other.

# Trusted Forwarder: Minimum Viable Trust

As your contract now seemingly allows GSN - a complicated network of third-party contracts - to handle your dapp's user authentication, you may feel worried that you will need to verify and audit every bit of the GSN as thoroughly as your own code. Worry no more!

The GSN project provides you with a default implementation of the Forwarder contract. This contract is extremely simple and basically does just one thing - it validates the user's signature. This way, your ERC2771Recipient contract is shielded from any potential vulnerabilities across the GSN.

You can read more about the security considerations in our forwarder ERC draft (opens new window).