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:
-
The
create-react-app
package to bootstrap a React application, along with OpenZeppelin Network JS to easily set up a web3 object with GSN support -
OpenZeppelin GSN Helpers to emulate the GSN in your local ganache instance
-
The
@openzeppelin/contracts-ethereum-package
smart contract library to get GSN -
The OpenZeppelin CLI to manage and deploy our contracts
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.
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:
-
To learn how to use OpenZeppelin Contracts to build a GSN-capable contract, head to the GSN basics guide.
-
If you want to learn how to use OpenZeppelin Contracts' pre-made accept and charge strategies, go to the GSN Strategies guide.
-
If instead you wish to know more about how to use GSN from your application, head to the OpenZeppelin GSN Provider guides.
-
For information on how to test GSN-enabled contracts, go to the OpenZeppelin GSN Helpers documentation.