A novel without gas barriers

Imagine this: you just created an incredible decentralized app, and it’s so cool that even your grandma wants to try it. But as soon as users face the need to pay a fee, the entire UX (User Experience) quickly goes downhill. Blockchain promises a bright future where decentralization, transparency, and security are our best friends, but it makes us pay for everyday transactions. Imagine if you had to pay every time you like something on social media or send a message in a messenger. Horrible, right? And yet, dApps users face something like this every day.

But here comes GSN (Gas Station Network), like a prince on a white horse. With its help, developers can make their applications gas-less, and users can finally forget about commissions as a bad dream.

In this article, we will look at what GSN is, how it works, and how to implement it in your projects to please your users.

Introduction to GSN

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 implementing payments using GSN:

1. Payment pool allocated by the developer. The developer can create a pool of funds that will be used to pay gas fees for users. This allows users to interact with the application without any costs on their part.

2. Payment via ERC20. Users can pay gas fees using ERC20 tokens. One of the contracts, Paymaster (which I will discuss later), processes these payments and covers the corresponding gas fees.

3. Subscriptions. Developers can implement a subscription model, where users pay a fixed amount to access the dApp for a certain period. In this case, gas fees are covered by the subscription.

GSN is not just magic, but a well-oiled system consisting of several key components:

Relay server

Think of a relay server as a kind of postman that takes your meta transaction and verifies it with the other components of the system. If everything is OK, this smart postman signs the transaction and sends it to Ethereum, and then returns the signed transaction to you for verification. All you have to do is sit back and enjoy the process, leaving the transaction in safe digital hands.

Paymaster

Paymaster is a kind of financial manager that monitors gas expenses. It provides logic for gas fee refunds. For example, it can only accept transactions from whitelisted users or process refunds in tokens. Paymaster always keeps a reserve of ETH in RelayHub to cover expenses on time.

Forwarder

Forwarder is a small but very important contract, which 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. Without its permission, no transaction will go through.

dApp contract

Your dApp contract must inherit from ERC2771Recipient (in older versions it was called BaseRelayRecipient). In this case, to find the original sender, `_msgSender()` is used instead of `msg.sender`. Thus, 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. He connects clients, relay servers and Paymasters, providing a trusted environment for all participants.

All of these components work together to make sure users are happy, developers are happy, and everyone is happy. To use GSN, you don’t have to deploy all of the contracts yourself. For example, some of these smart contracts have already been deployed to the main networks. You can see a list of these contracts and their network addresses at official website. At a minimum, you only need to deploy your own smart contract inherited from ERC2771Recipient/BaseRelayRecipient and Paymaster with your own logic. Thanks to this, integration with GSN is simpler and faster than it seems at first glance.

Let's test it locally?

Step 1: Starting a Local Network with Hardhat

First, let's install hardhat and start a local network using npx hardhat node

Step 2: GSN Deployment

To work with GSN, install the package @opengsn/cli and execute the corresponding command:

yarn add --dev @opengsn/cli
npx gsn start [--workdir <directory>] [-n <network>]

At the output we get a list of addresses in the local network, as well as the URL of the deployed 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 will use a ready-made one from the library. This contract is not recommended for use in the main network, since it compensates all transactions without any verification.

Step 3: Create a simple smart contract

Let's look at the creation of 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();
    }
}

To simplify contract deployment, you can use https://remix.ethereum.orgconnecting it to the local network.

Step 4: Interacting with ethers.js

Let's install the necessary dependencies:

npm install @opengsn/provider ethers

First, let's 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'
};

Now we need to top up Paymaster by transferring some funds to RelayHub. We do this as follows:

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("______________________________________________________________")  
}

Now let's create the gas-less transaction itself. To do this, we need to initialize a new provider according to 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 called in the usual way with payment of a commission, and the second (gaslessTx) — will already use the gasless method, in which the user himself will not pay a 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);

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 in your projects and make users’ lives easier and more enjoyable. Let your dApps become popular and loved, and gas fees will be a thing of the past, and hopefully your grandma will finally try your cool app and say: “Oh, this is nothing, I like it!”

Good luck to everyone in development, and may your code always compile the first time!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *