Imagine: you've just created an incredible decentralized application, and it's so cool that even your grandmother wanted to try it. But once users are faced with the need to pay a commission, the entire UX (User Experience) rapidly slides down like a ball down a slide. Blockchain promises a bright future in which decentralization, transparency and security are our best friends, and it makes us pay for daily operations. Imagine if you had to pay every time you like on social networks or send a message in a messenger. It's terrible, isn't it? But dApps users face something like this every day.
But now, like a prince on a white horse, GSN (Gas Station Network) appears. With its help, developers can make their applications gas-less, and users will finally be able to forget about commissions like a nightmare.
In this article, we will look at what GSN is, how it works, and how to implement it into your projects to please users.
Introduction to GSN
The Gas Station Network (GSN) is an infrastructure that allows users to interact with decentralized applications without having to pay for gas (or allowing them to do so in some other way, such as paying with ERC-20 tokens).
At the moment, there are three main scenarios for the implementation of payment using GSN:
The payment pool allocated by the developer. The developer can create a pool of funds that will be used to pay gas commissions for users. This allows users to interact with the application without any cost on their part.
Payment using ERC20. Users can pay gas commissions using ERC20 tokens. One of the contracts, Paymaster (which I will talk about later), processes these payments and covers the corresponding gas commissions.
Subscriptions. Developers can implement a subscription model where users pay a fixed amount for access to the DApp for a certain period. In this case, gas commissions are covered by subscription.
GSN is not just a magical solution, it's a well-established system consisting of several essential components:
Relay Server (Relayer)
Imagine that the relay server is a kind of mailman that accepts your meta—transaction and verifies it using other components of the system. If everything is in order, this smart postman signs the transaction and sends it to Ethereum, and then returns the signed transaction to you for verification. All that remains for you is to sit and enjoy the process, transferring the transaction into reliable digital "hands".
Paymaster
A paymaster is something like a financial manager who monitors gas costs. It provides the logic for the return of gas commissions. For example, it can only accept transactions from whitelisted users or process refunds in tokens. Paymaster always keeps a stock of ETH in RelayHub to cover expenses on time.
Forwarder
Forwarder is a small but very important contract that can be compared to a security guard at the entrance. It verifies the authenticity of meta transactions, making sure that the signature and nonce of the original sender are correct. No transaction will take place without his permission.
DApp's contract
Your dApp contract should inherit from ERC2771Recipient (in older versions it was called BaseRelayRecipient). At the same time, to search for the original sender, instead of `msg.sender`, `_msgSender()` is used. This way, the contract always knows who exactly sent the request.
RelayHub
RelayHub is the real CEO of this corporation. He is the main coordinator of all this fuss. It connects clients, relay servers and Paymasters, providing a trusted environment for all participants.
Thanks to the well-coordinated work of all these components, users are satisfied, developers are satisfied, and everything is fine. To use GSN, you don't need to deploy all the contracts yourself. For example, some of these smart contracts have already been deployed on major networks. A list of these contracts and their addresses in the networks can be found on the official website. Minimally, you only need to deploy your own smart contract inherited from ERC2771Recipient/BaseRelayRecipient, and Paymaster with its own logic. Thanks to this, integration with GSN is easier and faster than it seems at first glance.
Let's test it locally!
Step 1. Starting a local network using Hardhat
First, install hardhat and start the local network using npx hardhat node
Step 2. GSN deployment
To work with GSN, install the package @opengsn/cli
and execute the appropriate command:
yarn add --dev @opengsn/cli
npx gsn start [--workdir <directory>] [-n <network>]
At the output, we will get a list of addresses on the local network, as well as the url of the expanded relay:
...
RelayHub: 0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9
RelayRegistrar: 0x5FbDB2315678afecb367f032d93F642f64180aa3
StakeManager: 0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Penalizer: 0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
Forwarder: 0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9
TestToken (test only): 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
Paymaster : 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
info: Relay is active, URL = http://127.0.0.1:57599/ . Press Ctrl-C to abort
In the test case, we will not use the Paymaster contract ourselves, but use a ready-made one from the library. This contract is not recommended for use on the main network, because it compensates for all transactions without any verification.
Step 3. Creation of simple smart-contract
Let's create the simplest smart contract as an example:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@opengsn/contracts/src/BaseRelayRecipient.sol";
contract SimpleContract is BaseRelayRecipient {
constructor(address forwarder ) {
_setTrustedForwarder(forwarder);
}
address public lastCaller;
function versionRecipient() external override virtual view returns (string memory){
return "1.0.0";
}
function updateCaller() external {
lastCaller = _msgSender();
}
}
For the simplicity of the contract deposit, you can use https://remix.ethereum.org by connecting it to a local network.
Step 4. Interaction using ethers.js
Let's install the necessary dependencies:
npm install @opengsn/provider ethers
First, fill in the configuration according to the addresses obtained in the previous steps:
const config = {
relayHubAddress: "0xrelayHubAddress",
ownerAddress: "0xownerAddress",
payMaster: "0xpayMaster",
trustForwarder: "0xtrustForwarder",
stakeManagerAddress: "0xstakeManagerAddress",
relayersURL: ['http://127.0.0.1:YOUR-PORT/'],
gasPriceFactor: 1,
maxFeePerGas: 1000000000000,
ethereumNodeUrl: "http://127.0.0.1:8545", //Возьмите из настроек hardhat
chainId: 1337,
simpleContractAddress: "0xSimpleContractAddress",
};
// Адреса и приватные ключи, можно взять при запуске локальной сети в hardhat
const sender = {
address: 'YOUR-ADDRESS',
privateKey: 'YOUR-PRIVATE-KEY'
};
We need to add money to Paymaster. We do it like this:
async function sendEtherToPaymaster() {
const relayHubAddress = config.relayHubAddress;
const paymasterAddress = config.payMaster;
const depositAmount = ethers.utils.parseEther("1.0"); // 1 ETH, например
const relayHubContract = new ethers.Contract(relayHubAddress, RelayHubABI, senderWallet);
const tx = await relayHubContract.depositFor(paymasterAddress, { value: depositAmount });
await tx.wait();
const balance = await relayHubContract.balanceOf(paymasterAddress)
console.log(`Funds deposited to RelayHub for Paymaster: ${paymasterAddress} with balance ${balance}`);
console.log("______________________________________________________________")
}
And now let's create the gas‑less transaction itself. To do this, you will need to initialize the new provider in accordance with our config:
async function gaslessTx() {
console.log("________________________SERVICE MESSAGES______________________")
const gsnProvider = await RelayProvider.newProvider({
provider: provider,
config: {
auditorsCount: 0,
relayHubAddress: config.relayHubAddress,
stakeManagerAddress: config.managerStakeTokenAddress,
gasPriceFactor: config.gasPriceFactor,
maxFeePerGas: config.maxFeePerGas,
paymasterAddress: config.payMaster,
forwarderAddress: config.trustForwarder,
chainId: config.chainId,
performDryRunViewRelayCall: false,
preferredRelays: config.relayersURL
}
}).init();
console.log("______________________________________________________________")
const ethersGsnProvider = new ethers.providers.Web3Provider(gsnProvider);
const gsnSigner = ethersGsnProvider.getSigner(sender.address);
const simpleContract = new ethers.Contract(config.simpleContractAddress, simpleContractABI, gsnSigner);
try {
const txResponse = await simpleContract.connect(gsnSigner).updateCaller({
gasLimit: 1000000 // Пример лимита газа, значение зависит от транзакции
});
console.log(`Transaction hash: ${txResponse.hash}`);
await txResponse.wait();
console.log('Transaction confirmed');
} catch (error) {
console.error('Transaction failed:', error);
}
}
Thus, the first replenishment transaction (sendEtherToPaymaster
) will be triggered in the usual way with the payment of a commission, and the second (gaslessTx
) — it will already use the gasless method, in which the user himself will not pay the commission.
Full code:
const { ethers } = require('ethers');
const { RelayProvider } = require('@opengsn/provider');
const RelayHubABI = [
// Будем использовать минимально необходимые функции
"function depositFor(address target) public payable",
"function balanceOf(address target) view returns (uint256)"
];
const simpleContractABI = [
"function updateCaller() external",
"function lastCaller() view returns (address)"
];
// Замените адреса!
const config = {
relayHubAddress: "0xrelayHubAddress",
ownerAddress: "0xownerAddress",
payMaster: "0xpayMaster",
trustForwarder: "0xtrustForwarder",
stakeManagerAddress: "0xstakeManagerAddress",
relayersURL: ['http://127.0.0.1:YOUR-PORT/'],
gasPriceFactor: 1,
maxFeePerGas: 1000000000000,
ethereumNodeUrl: "http://127.0.0.1:8545", //Возьмите из настроек hardhat
chainId: 1337,
simpleContractAddress: "0xSimpleContractAddress",
};
// Адреса и приватные ключи
const sender = {
address: 'YOUR-ADDRESS',
privateKey: 'YOUR-PRIVATE-KEY'
};
const provider = new ethers.providers.JsonRpcProvider(config.ethereumNodeUrl);
const senderWallet = new ethers.Wallet(sender.privateKey, provider);
//Пополнить Paymaster для оплаты комиссии
async function sendEtherToPaymaster() {
const relayHubAddress = config.relayHubAddress;
const paymasterAddress = config.payMaster;
const depositAmount = ethers.utils.parseEther("1.0"); // 1 ETH, например
const relayHubContract = new ethers.Contract(relayHubAddress, RelayHubABI, senderWallet);
const tx = await relayHubContract.depositFor(paymasterAddress, { value: depositAmount });
await tx.wait();
const balance = await relayHubContract.balanceOf(paymasterAddress)
console.log(`Funds deposited to RelayHub for Paymaster: ${paymasterAddress} with balance ${balance}`);
console.log("______________________________________________________________")
}
async function getBalance(address) {
console.log("______________________BALANCES_______________________________")
const etherBalance = await provider.getBalance(address);
console.log(`BALANCE OF ${address} IS ${ethers.utils.formatEther(etherBalance)} ETH`);
console.log("______________________________________________________________")
}
async function gaslessTx() {
console.log("________________________SERVICE MESSAGES______________________")
const gsnProvider = await RelayProvider.newProvider({
provider: provider,
config: {
auditorsCount: 0,
relayHubAddress: config.relayHubAddress,
stakeManagerAddress: config.managerStakeTokenAddress,
gasPriceFactor: config.gasPriceFactor,
maxFeePerGas: config.maxFeePerGas,
paymasterAddress: config.payMaster,
forwarderAddress: config.trustForwarder,
chainId: config.chainId,
performDryRunViewRelayCall: false,
preferredRelays: config.relayersURL
}
}).init();
console.log("______________________________________________________________")
const ethersGsnProvider = new ethers.providers.Web3Provider(gsnProvider);
const gsnSigner = ethersGsnProvider.getSigner(sender.address);
const simpleContract = new ethers.Contract(config.simpleContractAddress, simpleContractABI, gsnSigner);
try {
const txResponse = await simpleContract.connect(gsnSigner).updateCaller({
gasLimit: 1000000 // Пример лимита газа, значение зависит от транзакции
});
console.log(`Transaction hash: ${txResponse.hash}`);
await txResponse.wait();
console.log('Transaction confirmed');
} catch (error) {
console.error('Transaction failed:', error);
}
}
async function main() {
await getBalance(sender.address);
await sendEtherToPaymaster()
await getBalance(sender.address);
await gaslessTx();
await getBalance(sender.address);
}
main().catch(console.error);
Conclusion
Using GSN is like having a personal superhero who is always ready to pay for you. So don't be afraid to implement these technologies into your projects and make users' lives easier and more enjoyable. Let your dApps become popular and beloved, and gas commissions will be a thing of the past, and hopefully your grandmother will finally try your cool app and say, "That's okay, I like it!"