Sending gasless transactions

This guide is now deprecated, as it uses GSNv1, which is no longer supported. Consider using Defender for setting up your own meta-transaction relayers instead, or using GSNv2 from the OpenGSN team for a decentralized solution.
This article is no longer maintained. Read here for more info.

Anyone who sends an Ethereum transaction needs to have Ether to pay for its gas fees. This forces new users to purchase Ether (which can be a daunting task) before they can start using a dapp. This is a major hurdle in user onboarding.

In this guide, we will explore the concept of gasless (also called meta) transactions, where the user does not need to pay for their gas fees. We will also introduce the Gas Station Network, a decentralized solution to this problem, as well as the OpenZeppelin libraries that allow you to leverage it in your dapps:

What is a Meta-transaction?

All Ethereum transactions use gas, and the sender of each transaction must have enough Ether to pay for the gas spent. Even though these gas costs are low for basic transactions (a couple of cents), getting Ether is no easy task: dApp users often need to go through Know Your Customer and Anti Money-Laundering processes (KYC & AML), which not only takes time but often involves sending a selfie holding their passport over the Internet (!). On top of that, they also need to provide financial information to be able to purchase Ether through an exchange. Only the most hardcore users will put up with this hassle, and dApp adoption greatly suffers when Ether is required. We can do better.

Enter meta-transactions. This is a fancy name for a simple idea: a third-party (called a relayer) can send another user’s transactions and pay themselves for the gas cost. In this scheme, users sign messages (not transactions) containing information about a transaction they would like to execute. Relayers are then responsible for signing valid Ethereum transactions with this information and sending them to the network, paying for the gas cost. A base contract preserves the identity of the user that originally requested the transaction. In this way, users can interact directly with smart contracts without needing to have a wallet or own Ether.

This means that, in order to support meta transactions in your application, you need to keep a relayer process running - or leverage a decentralized relayer network.

The Gas Station Network

The Gas Station Network (GSN) is a decentralized network of relayers. It allows you to build dapps where you pay for your users transactions, so they do not need to hold Ether to pay for gas, easing their onboarding process.

The GSN was originally conceived and designed by TabooKey, and it has grown to encompass many companies in the Ethereum space looking to work together to solve the problem of onboarding users to Ethereum applications.

However, relayers in the GSN are not running a charity: they’re running a business. The reason why they’ll gladly pay for your users' gas costs is because they will in turn charge your contract, the recipient. That way relayers get their money back, plus a bit extra as a fee for their services.

This may sound strange at first, but paying for user onboarding is a very common business practice. Lots of money is spent on advertising, free trials, new user discounts, etc., all with the goal of user acquisition. Compared to those, the cost of a couple of Ethereum transactions is actually very small.

Additionally, you can leverage the GSN in scenarios where your users pay you off-chain in advance (e.g. via credit card), with each GSN-call deducting from their balance on your system. The possibilities are endless!

Furthermore, the GSN is set up in such a way where it’s in the relayers' best interest to serve your requests, and there are measures in place to penalize them if they misbehave. All of this happens automatically, so you can safely start using their services worry-free.

You can learn more about how the GSN works in Interacting With RelayHub.

Building a GSN-powered DApp

Time to build a dapp leveraging the GSN and push it to a testnet. In this section, we will use:

It might feel like there are many moving pieces here, but each component has a well-defined role in building this application. That said, if you are new to the OpenZeppelin platform, it may help to look into the OpenZeppelin Contracts GSN guide and the Building a Dapp tutorial before you continue reading.

We will create a simple contract that just counts transactions sent to it, but will tie it into the GSN so that users will not have to pay for the gas when sending these transactions. Let’s get started!

Setting up the Environment

We will begin by creating a new npm project and installing all dependencies, including Ganache (which we’ll use to run a local network):

$ mkdir gsn-dapp && cd gsn-dapp
$ npm init -y
$ npm install @openzeppelin/network
$ npm install --save-dev @openzeppelin/gsn-helpers @openzeppelin/contracts-ethereum-package @openzeppelin/upgrades @openzeppelin/cli ganache-cli

Use the CLI to set up a new project and follow the prompts so we can write our first contract.

$ npx oz init
Check out Getting Started with the OpenZeppelin CLI if you’re unfamiliar with it.

Creating our Contract

We will write our vanilla Counter contract in the newly created contracts folder.

// contracts/Counter.sol
pragma solidity ^0.5.0;

contract Counter {
    uint256 public value;

    function increase() public {
        value += 1;
    }
}

This is simple enough. Now, let’s modify it to add GSN support. This requires extending from the GSNRecipient contract and implementing the acceptRelayedCall method. This method must return whether we accept or reject to pay for a user transaction. For the sake of simplicity, we will be paying for all transactions sent to this contract.

For most (d)apps, it is probably not a good idea to have such a generous policy since any malicious user could easily drain your contract’s funds. Check out our guide on GSN payment strategies for different approaches to this problem.
// contracts/Counter.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";

contract Counter is GSNRecipient {
    uint256 public value;

    function increase() public {
        value += 1;
    }

    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) {
        return _approveRelayedCall();
    }

    // We won't do any pre or post processing, so leave _preRelayedCall and _postRelayedCall empty
    function _preRelayedCall(bytes memory context) internal returns (bytes32) {
    }

    function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal {
    }
}

Start ganache on a separate terminal by running npx ganache-cli. Then, create an instance of our new contract using the OpenZeppelin CLI with npx oz create and follow the prompts, including choosing to call a function to initialize the instance.

Be sure to take note of the address of your instance, which is returned at the end of this process!

It is important that you remember to call the initialize() function when creating the contract, as this will get your contract ready to be used in the GSN.
$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate Counter
? Pick a network development
All contracts are up to date
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize()
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

Great! Now, if we deployed this contract to mainnet or the goerli testnet, we would be almost ready to start sending gasless transactions to it, since the GSN is already set up on both of those networks. However, since we are on a local ganache, we’ll need to set it up ourselves.

Deploying a Local GSN for Development

The GSN is composed of a central RelayHub contract that coordinates all relayed transactions, as well as multiple decentralized relayers. The relayers are processes that receive requests to relay a transaction via an HTTP interface and send them to the network via the RelayHub.

With ganache running, you can start a relayer in a new terminal using the following command from the OpenZeppelin GSN Helpers:

$ npx oz-gsn run-relayer
Deploying singleton RelayHub instance
RelayHub deployed at 0xd216153c06e857cd7f72665e0af1d7d82172f494
Starting relayer
 -Url http://localhost:8090
...
RelayHttpServer starting. version: 0.4.0
...
Relay funded. Balance: 4999305160000000000
Under the hood, this command takes care of several steps to have a local relayer up and running. First, it will download a relayer binary for your platform and start it. It will then deploy the RelayHub contract to your local ganache, registering the relayer on the hub, and funding it so it can relay transactions. You can run these steps individually by using other oz-gsn commands or even directly from your JavaScript code.

The last step will be to fund our Counter contract. GSN relayers require recipient contracts to have funds since they will then charge the cost of the relayed transaction (plus a fee!) to it. We will again use the oz-gsn set of commands to do this:

$ npx oz-gsn fund-recipient --recipient 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
Make sure to replace the recipient address with the address of your Counter contract instance!

Cool! Now that we have our GSN-powered contract and a local GSN to try it out, let’s build a small (d)app.

Creating the Dapp

We will create our (d)app using the create-react-app package, which bootstraps a simple client-side application using React.

$ npx create-react-app client

First, create a symlink so we can access our compiled contract .json files. From inside the client/src directory, run:

$ ln -ns ../../build

This will allow our front end to reach our contract artifacts.

Then, replace client/src/App.js with the following code. This will use OpenZeppelin Network JS to create a new provider connected to the local network. It will use a key generated on the spot to sign all transactions on behalf of the user and will use the GSN to relay them to the network. This allows your users to start interacting with your (d)app right away, even if they do not have MetaMask installed, an Ethereum account, or any Ether at all.

// client/src/App.js
import React, { useState, useEffect, useCallback } from "react";
import { useWeb3Network } from "@openzeppelin/network/react";

const PROVIDER_URL = "http://127.0.0.1:8545";

function App() {
  // get GSN web3
  const context = useWeb3Network(PROVIDER_URL, {
    gsn: { dev: true }
  });

  const { accounts, lib } = context;

  // load Counter json artifact
  const counterJSON = require("./build/contracts/Counter.json");

  // load Counter Instance
  const [counterInstance, setCounterInstance] = useState(undefined);

  if (
    !counterInstance &&
    context &&
    context.networkId
  ) {
    const deployedNetwork = counterJSON.networks[context.networkId.toString()];
    const instance = new context.lib.eth.Contract(counterJSON.abi, deployedNetwork.address);
    setCounterInstance(instance);
  }

  const [count, setCount] = useState(0);

  const getCount = useCallback(async () => {
    if (counterInstance) {
      // Get the value from the contract to prove it worked.
      const response = await counterInstance.methods.value().call();
      // Update state with the result.
      setCount(response);
    }
  }, [counterInstance]);

  useEffect(() => {
    getCount();
  }, [counterInstance, getCount]);

  const increase = async () => {
    await counterInstance.methods.increase().send({ from: accounts[0] });
    getCount();
  };

  return (
    <div>
      <h3> Counter counterInstance </h3>
      {lib && !counterInstance && (
        <React.Fragment>
          <div>Contract Instance or network not loaded.</div>
        </React.Fragment>
      )}
      {lib && counterInstance && (
        <React.Fragment>
          <div>
            <div>Counter Value:</div>
            <div>{count}</div>
          </div>
          <div>Counter Actions</div>
            <button onClick={() => increase()} size="small">
              Increase Counter by 1
            </button>
        </React.Fragment>
      )}
    </div>
  );
}

export default App;
You can pass a dev: true flag to the gsn options when setting up the provider. This will use the GSNDevProvider instead of the regular GSN provider. This is a provider set up specifically for testing or development, and it does not require a relayer to be running to work. This can make development easier, but it will feel less like the actual GSN experience. If you want to use an actual relayer, you can run npx oz-gsn run-relayer locally (see the Preparing a Testing Environment for more info).

Great! We can now fire up our application running npm start from within the client folder. Remember to keep both your ganache and relayer up and running. You should be able to send transactions to your Counter contract without having to use MetaMask or have any ETH at all!

Moving to a Testnet

It is not very impressive to send a local transaction in your ganache network, where you already have a bunch of fully-funded accounts. To witness the GSN at its full potential, let’s move our application to the goerli testnet. If you later want to go onto mainnet, the instructions are the same.

You will need to create a new entry in the networks.js file, with a goerli account that has been funded. For detailed instructions on how to do this, check out Deploying to Public Tests Network.

We can now deploy our Counter contract to goerli:

$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate: Counter
? Pick a network: goerli
✓ Added contract Counter
✓ Contract Counter deployed
? Call a function to initialize the instance after creating it?: Yes
? Select which function * initialize()
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

The next step will be to instruct our (d)app to connect to a goerli node instead of the local network. Change the PROVIDER_URL in your App.js to, for example, an Infura goerli endpoint.

We will now be using a real GSN provider rather than our developer environment, so you may want to also provide a configuration object, which will give you more control over things such as the gas price you are willing to pay. For production (d)apps, you will want to configure this to your requirements.

import { useWeb3Network, useEphemeralKey } from "@openzeppelin/network/react";

// inside App.js#App()
const context = useWeb3Network('https://goerli.infura.io/v3/' + INFURA_API_TOKEN, {
  gsn: { signKey: useEphemeralKey() }
});

We are almost there! If you try to use your (d)app now, you will notice that you are not able to send any transactions. This is because your Counter contract has not been funded on this network yet. Instead of using the oz-gsn fund-recipient command we used earlier, we will now use the online gsn-tool by pasting in the address of your instance. To do this, the web interface requires that you use MetaMask on the goerli Network, which will allow you to deposit funds into your contract.

OpenZeppelin GSN Dapp Tool

That’s it! We can now start sending transactions to our Counter contract on the goerli network from our browser without even having MetaMask installed.

The GSN Starter Kit

Starter Kits are pre-configured project templates to bootstrap dapp development. One of them, the GSN Starter Kit, is a ready-to-use dapp connected to the GSN, with a similar setup as the one we built from scratch in the previous section.

If you are building a new dapp and want to include meta-transaction support, you can run oz unpack gsn to jumpstart your development and start with a GSN-enabled box!

Next steps

To learn more about the GSN, head over to the following resources: