BLOG — Developer Tutorials

387 days ago

How to create your first gasless app

Introduction

Summary:

  • Gasless transactions are transactions that abstract the payment of gas away from the normal wallet flow (e.g. Metamask), allowing your users to send signed transactions without possessing a network’s native token
  • Gelato Relay enables meta transactions so users can interact with your smart contracts without having to pay for gas
  • This article will teach you how to enable gasless transactions for your smart contracts, allowing you to build apps that offer your users a gasless experience

Showcase App: Gasless Proposal Voting for DAOs

We will start with an app that allows DAO members to send proposals. Once a proposal is sent, users have 30 minutes to vote, after which an automated Gelato task will close the voting period.

Not Gasless at first

Initially, every proposal and vote are signed transactions. The github repo can be found here.

To get started, open your command line and type the following:

git clone  https://github.com/donoso-eth/gasless-voting
cd gasless-voting
git checkout main
yarn

In a second terminal, make sure to run a local hardhat node:

1 Terminal:

npm run fork

2 Terminal:

npm run compile
npm run deploy

Our frontend is built with angular, we launch it with:

npx ng serve -o

This will open a browser tab at http://localhost:4200/. In the local hardhat node, we can test by creating a proposal and voting. Later on, we will deploy to testnet to test the relayed transactions, as this involves off-chain infrastructure.

Why Gasless?

One issue many DAOs struggle with is member participation, especially when members experience difficulties voting for proposals due to gas constraints. To remove the friction of paying gas for voting, we will convert our contract into one that is relay-aware. But first, let’s dive into how a relayer works. gelaro-relay.png

  • The app sends an HTTP post request to Gelato with the help of the Gelato Relay-SDK
  • Gelato forwards the request payload to the Gelato Relay Contract
  • The target contract executes the transaction

To implement the relayer, we need to decide how we want to fund the transactions and whether we need Gelato’s support to authenticate the users.

How to Gasless Step by Step

Here’s how we’re making the process easier: DAOs can easily create an app that allows users to create proposals and vote without paying for gas.

We are going to convert the createProposal() and vote() transactions into gasless transactions.

Following the table below, we will determine which contract and SDK method to use.

Gelato Auth Payment Inheriting Contract SDK/API method
no User GelatoRelayContext relayWithSyncFee
yes User GelatoRelayContextERC2771 relayWithSyncFeeERC2771
no 1Balance n. a. relayWithSponsoredCall
yes¹ 1Balance ERC2771Context relayWithSponsoredCallERC2771
  1. A SponsorKey is required; visit Gelato 1Balance here

Transaction without authentication and 1Balance (relayWithSyncFee)

In this case, we are allowing all users to create a proposal, so there's no need to authenticate users. Typically we recommend using 1Balance as the payment method, but for this demo we will utilize Gelato Relay’s SyncFee payment method.

If we follow the table above, we are in the first row and don’t need to authenticate users.

  • Inherit contract: GelatoRelayContext
  • Sdk method: relayWithSyncFee

Smart Contract Update

Our current transaction in solidity looks like this:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract GaslessProposing  {

 // @notice createProposal Transaction
 // @dev external
 function createProposalTransaction(bytes calldata payload) external  {
   require(
     proposal.proposalStatus == ProposalStatus.Ready,
     "OLD_PROPOSAL_STILL_ACTIVE"
   );
  proposalId++;
   proposal.proposalStatus = ProposalStatus.Voting;
   proposal.proposalId = proposalId;
   proposalTimestamp = block.timestamp;
   proposalBytes = payload;
   IGaslessVoting(gaslessVoting)._createProposal(proposalId, payload);

  finishingVotingTask =  createFinishVotingTask();
  proposal.taskId = finishingVotingTask;
   emit ProposalCreated(finishingVotingTask);
 }

}

We will follow the next steps to transform our contract into a relay-aware contract:

Install the Relay-Context contracts

npm i @gelatonetwork/relay-context

Import the relay-context contract:

import {GelatoRelayContext} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol";

Inherit the relay-context contract:

contract GaslessProposing is GelatoRelayContext {

Finally we'll restrict our createProposal method with the onlyGelatoRelay() modifier and use the helper function _transferRelayFee() to transfer the fees to Gelato Relay.

Without the correct payment to the right feeCollector address, Gelato will not execute your transaction

 // @notice
 // @dev external only Gelato relayer
 // @dev transfer Fee to Geato with _transferRelayFee();
 function createProposal(bytes calldata payload) external onlyGelatoRelay {
   require(
     proposal.proposalStatus == ProposalStatus.Ready,
     "OLD_PROPOSAL_STILL_ACTIVE"
   );
 
   _transferRelayFee();
 
   proposalId++;
   proposal.proposalStatus = ProposalStatus.Voting;
   proposal.proposalId = proposalId;
   proposalTimestamp = block.timestamp;
   proposalBytes = payload;
   IGaslessVoting(gaslessVoting)._createProposal(proposalId, payload);
 
  finishingVotingTask =  createFinishVotingTask();
  proposal.taskId = finishingVotingTask;
   emit ProposalCreated(finishingVotingTask);
 }

With these changes, our contract is now relay-aware and ready to receive gasless transactions. If we look under the hood, we can see the changes applied:

  • Ensure that only the Gelato Relay contract can call the createProposal() method
  • Decode the fee, fee collector, and feeToken appended to the calldata and use it for transferring the fees to Gelato

Frontend Update (SDK)

Now that our contract is ready, we must update how our frontend calls the contract.

Here we can see how we call the transaction so far:

 async createProposal() {  
 
   let name = this.proposalForm.controls.nameCtrl.value;
   let description = this.proposalForm.controls.descriptionCtrl.value;
 
   let payload = this.abiCoder.encode(
     ['string', 'string'],
     [name, description]
   );
   	
   await doSignerTransaction(
     this.gaslessProposing.createProposalTransaction(payload)
   );
 
 }

Now let’s change our transaction to a relay-SDK call to Gelato Relay.

First we will install the Gelato Relay SDK, build and send the request

npm i @gelato-network/relay-sdk:

Import the SDK and relevant methods

import {  CallWithSyncFeeRequest, GelatoRelay } from '@gelatonetwork/relay-sdk';

Instantiate the GelatoRelay object

const relay = new GelatoRelay();

Build the request as per the docs

const request = {
     chainId  // network 
     target // target contract address
     data // encoded transaction data 
     isRelayContext // are we using context contracts
     feeToken // token to pay the relayer
   };

We use the native token, we are on Goerli and we know the target contract address, so the only part missing is the data (encoded transaction data)

   const { data } =
     await this.readGaslessProposing.populateTransaction.createProposal(payload);

Finally, we send our request with the SDK method callWithSyncFeeand retrieve the task Id.

   // send relayRequest to Gelato Relay API
   const relayResponse = await  relay.callWithSyncFee(request);
    let taskId = relayResponse.taskId

Our code at the end looks like this:

 async createProposal() {
   let name = this.proposalForm.controls.nameCtrl.value;
   let description = this.proposalForm.controls.descriptionCtrl.value;
 
   let payload = this.abiCoder.encode(
     ['string', 'string'],
     [name, description]
   );
 
   const feeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
   const { data } =
     await this.readGaslessProposing.populateTransaction.createProposal(
       payload
     );
 
   // populate the relay SDK request body
   const request = {
     chainId: 5, // Goerli in this case
     target: this.readGaslessProposing.address, // target contract address
     data: data!, // encoded transaction data
     isRelayContext: true, // are we using context contracts
     feeToken: feeToken, // token to pay the relayer
   };
 
   // send relayRequest to Gelato Relay API
   const relayResponse = await  relay.callWithSyncFee(request);
   console.log(relayResponse);
   let taskId = relayResponse.taskId
 
  }

Et Voilà! Our first gasless transaction has complete! Check the demo app is live here https://gelato-gasless-dao.web.app/landing.

Transaction with authentication and using 1Balance(relayWithSponsoredCallERC2771)

Now lets say our app wants to have one vote per user. In this case, we will need to authenticate the users, and we will use 1Balance for sponsoring our transactions.

If we follow the handy table again, this use case matches the last row:

  • Inherit contract: ERC2771Context
  • Sdk method: relayWithSponsoredCallERC2771

Configure 1Balance

We will configure the 1Balance beta here, and you can find more information on how to do this here.

We deposit GETH:

We click on the Relay Apps tab, click ‘create app’ and input the target contract address and the method to be called.

Finally, we will copy the API key (sponsorApikey) from the API Key tab for later when we need it to send our request..

Smart Contract Update

Our current transaction in Solidity looks like this:

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

contract GaslessVoting  {
 //  @notice voting proposal
 //  @dev
 function votingProposal(bool positive) external {
   address voter = msg.sender;
 
   _votingProposal(positive, voter);
 
   emit ProposalVoted();
 }
}

We will follow the next steps to transform our contract into a relay-aware contract:

Install the Relay-Context contracts (we have already done that in the above example)

npm i @gelatonetwork/relay-context

Import the relay-context contract:

import {
   ERC2771Context
} from "@gelatonetwork/relay-context/contracts/vendor/ERC2771Context.sol";

Inherit the relay-context contract and we pass the address of the trusted forwarder 0xBf1.. contract GaslessVoting is ERC2771Context {

constructor() ERC2771Context(address(0xBf175FCC7086b4f9bd59d5EAE8eA67b8f940DE0d)) { }

Finally we update our method including the onlyTrustedForwarder method

 //  @notice voting proposal
 //  @dev function called by the relaer implementing the onlyTrusted Forwarder
 function votingProposal(bool positive) external onlyTrustedForwarder {
   address voter = _msgSender();
 
   _votingProposal(positive, voter);
 
   emit ProposalVoted();
 }

In this example, we don’t need to transfer the fees from the contract as we are using Gelato 1Balance. These changes allow only the trusted forwarder's address to call the function and (under the hood) decode the original transcaction sender and make it available through the method _msgSender().

Frontend Update (SDK)

Now that our contract is ready, we must update how our frontend calls the contract.

Here we can see how we call the transaction:

 /// Vote function
 async vote(value: boolean) {
   try {
       await doSignerTransaction(this.gaslessVoting.votingProposal(value));
 
   } catch (error) {
     alert('only one vote per user');
   }
 }

First we will install the Gelato Relay SDK

npm i @gelato-network/relay-sdk:

Import the sdk

import {  GelatoRelay } from '@gelatonetwork/relay-sdk';

Instantiate the Gelato Relay object

const relay = new GelatoRelay();

Build the request as per the [docs] (https://docs.gelato.network/developer-services/relay/quick-start/sponsoredcallerc2771)

 const request = {
       chainId: 5, // Goerli in this case
       target: this.readGaslessVoting.address, // target contract address
       data: data!, // encoded transaction datas
       user: signerAddress!, // signer address
     };

Our data will be

const { data } =
       await this.gaslessVoting.populateTransaction.votingProposal(value);

Finally, we will send our request with the SDK method sponsoredCallERC2771 passing the request object, the provider and the 1Balance sponsorApiKey, and retrieve the task ID.

  const sponsorApiKey = '1NnnocBNgXnG1VgUnFTHXmUICsvYqfjtKsAq1OCmaxk_';
   
     const relayResponse = await relay.sponsoredCallERC2771(
       request,
       new ethers.providers.Web3Provider(ethereum),
       sponsorApiKey
     );
 
   let taskId = relayResponse.taskId

Our gasless transaction now looks like this

async vote(value: boolean) {
   try {
 
     const { data } =
       await this.gaslessVoting.populateTransaction.votingProposal(value);
 
     const request = {
       chainId: 5, // Goerli in this case
       target: this.readGaslessVoting.address, // target contract address
       data: data!, // encoded transaction datas
       user: this.dapp.signerAddress!, //user sending the trasnaction
     };
 
     const sponsorApiKey = '1NnnocBNgXnG1VgUnFTHXmUICsvYqfjtKsAq1OCmaxk_';
   
     const relayResponse = await relay.sponsoredCallERC2771(
       request,
       new ethers.providers.Web3Provider(ethereum),
       sponsorApiKey
     );
 
 
   let taskId = relayResponse.taskId
 
   } catch (error) {
     alert('only one vote per user');
   }
 }
 

With these small changes, we have transformed a regular transaction into a gasless one, funding the gas fees with a cross-chain central balance.

Bonus: Implementing Gelato Automate

In our demo app, we implemented Gelato Automate to close the voting proposals after the voting period (approximately 30 mins later). If you are interested in integrating Gelato Automate in any contract, we’ve included some handy code snippets below. The Gelato Automate contract addresses can be found here.

The Gelato contracts to be inherited are in the following repo.

/ #region  ========== =============  GELATO OPS AUTOMATE CLOSING PROPOSAL  ============= ============= //
 
 //@dev creating the  gelato task
 function createFinishVotingTask() internal returns (bytes32 taskId) {
   bytes memory timeArgs = abi.encode(
     uint128(block.timestamp + proposalValidity),
     proposalValidity
   );
 
   //@dev executing function encoded
   bytes memory execData = abi.encodeWithSelector(this.finishVoting.selector);
 
   LibDataTypes.Module[] memory modules = new LibDataTypes.Module[](2);
 
   //@dev using execution prefixed at a certain interval and doing only one execution
   modules[0] = LibDataTypes.Module.TIME;
   modules[1] = LibDataTypes.Module.SINGLE_EXEC;
 
   bytes[] memory args = new bytes[](1);
 
   args[0] = timeArgs;
 
   LibDataTypes.ModuleData memory moduleData = LibDataTypes.ModuleData(
     modules,
     args
   );
 
   //@dev  task creation
   taskId = IOps(ops).createTask(address(this), execData, moduleData, ETH);
 }
 
 //@dev executing function to be called by Gelato
 function finishVoting() public onlyOps {
   (uint256 fee, address feeToken) = IOps(ops).getFeeDetails();
 
   transfer(fee, feeToken);
 }
 
 //@dev transfer fees to Gelato
 function transfer(uint256 _amount, address _paymentToken) internal {
   (bool success, ) = gelato.call{value: _amount}("");
   require(success, "_transfer: ETH transfer failed");
 }
 
 //@dev only Gelato modifier
 modifier onlyOps() {
   require(msg.sender == address(ops), "OpsReady: onlyOps");
   _;
 }
 
 // #endregion  ========== =============  GELATO OPS AUTOMATE CLOSING PROPOSAL  ============= ============= //

About Gelato

Gelato is a Web3 Cloud Platform empowering developers to create automated, gasless, and off-chain-aware Layer 2 chains and smart contracts. Over 400 web3 projects rely on Gelato for years to facilitate millions of transactions in DeFi, NFTs, and gaming.

  • Gelato RaaS: Deploy your own tailor-made ZK or OP L2 chains in a single click with native Account Abstraction and all Gelato middleware baked in.

  • Web3 Functions: Connect your smart contracts to off-chain data & computation by running decentralized cloud functions.

  • Automate: Automate your smart contracts by executing transactions automatically in a reliable, developer-friendly & decentralized manner.

  • Relay: Give your users access to reliable, robust, and scalable gasless transactions via a simple-to-use API.

  • Account Abstraction SDK: Gelato has partnered with Safe, to build a fully-fledged Account Abstraction SDK, combining Gelato's industry's best gasless transaction capabilities, with the industry's most secure smart contract wallet.

Subscribe to our newsletter and turn on your Twitter notifications to get the most recent updates about the Gelato ecosystem! If you are interested in being part of the Gelato team and building the future of the Internet browse the open positions and apply here.