How to create your first gasless app

Gelato Team

Mar 7, 2023

Introduction

Gasless transactions illustration

Summary:

  • Gasless transactions abstract gas payment away from wallets (e.g. MetaMask), enabling users to send signed transactions without holding the native token.

  • Gelato Relay enables meta transactions so users can interact with smart contracts without paying gas.

  • This guide explains how to enable gasless transactions for your contracts and build dApps that offer a gasless user experience.

Showcase App: Gasless Proposal Voting for DAOs

Example app: DAO members create proposals, then have 30 minutes to vote. Afterward, an automated Gelato task closes the voting period.


Not Gasless at first

Initially, proposals and votes are signed transactions. Repo:

github.com/donoso-eth/gasless-voting.


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

Run a local Hardhat node:

# Terminal 1
npm run fork

# Terminal 2
npm run compile
npm run deploy

Launch the Angular frontend:

npx ng serve -o

App runs at http://localhost:4200/. Test proposals locally before deploying to testnet for relayed transactions.

Why Gasless?

DAO participation is often hindered by gas costs. To remove friction, we convert our contract to be relay-aware.

Here’s how a relayer works:


  • The app sends an HTTP POST request to Gelato via the Relay SDK.

  • Gelato forwards the request to the Relay contract.

  • The target contract executes the transaction.

How to Gasless Step by Step

We’ll convert createProposal() and vote() into gasless transactions, following the table:

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

*Requires SponsorKey from Gelato 1Balance.

Transaction without authentication and 1Balance (relayWithSyncFee)

We allow all users to create proposals, no authentication needed. We use Gelato’s SyncFee payment method.

  • Inherit contract: GelatoRelayContext

  • SDK method: relayWithSyncFee

Smart Contract Update

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

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

contract GaslessProposing is GelatoRelayContext {
  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);
  }
}

  • Ensures only the Gelato Relay contract can call createProposal().

  • Transfers fees to Gelato Relay’s feeCollector.

Frontend Update (SDK)

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

const relay = new GelatoRelay();

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 { data } =
    await this.readGaslessProposing.populateTransaction.createProposal(payload);

  const request: CallWithSyncFeeRequest = {
    chainId: 5,
    target: this.readGaslessProposing.address,
    data: data!,
    isRelayContext: true,
    feeToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
  };

  const relayResponse = await relay.callWithSyncFee(request);
  console.log(relayResponse.taskId);
}

✅ Gasless transaction complete! Demo:

gelato-gasless-dao.web.app/landing


Transaction with authentication + 1Balance (relayWithSponsoredCallERC2771)

  • Inherit contract: ERC2771Context

  • SDK method: relayWithSponsoredCallERC2771

Configure 1Balance in the dashboard, deposit GETH, create app, and copy your sponsor API key.

Smart Contract Update

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

contract GaslessVoting is ERC2771Context {
  constructor() ERC2771Context(0xBf175FCC7086b4f9bd59d5EAE8eA67b8f940DE0d) {}

  function votingProposal(bool positive) external onlyTrustedForwarder {
    address voter = _msgSender();
    _votingProposal(positive, voter);
    emit ProposalVoted();
  }
}

Frontend Update (SDK)

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

const relay = new GelatoRelay();

async vote(value: boolean) {
  try {
    const { data } =
      await this.gaslessVoting.populateTransaction.votingProposal(value);

    const request = {
      chainId: 5,
      target: this.readGaslessVoting.address,
      data: data!,
      user: this.dapp.signerAddress!
    };

    const sponsorApiKey = 'YOUR_1BALANCE_API_KEY';

    const relayResponse = await relay.sponsoredCallERC2771(
      request,
      new ethers.providers.Web3Provider(ethereum),
      sponsorApiKey
    );

    console.log(relayResponse.taskId);
  } catch {
    alert('only one vote per user');
  }
}

Bonus: Implementing Gelato Automate

In the demo app, Gelato Automate is used to close proposals after ~30 minutes. See Automate contract addresses and the repo.

function createFinishVotingTask() internal returns (bytes32 taskId) {
  bytes memory timeArgs = abi.encode(uint128(block.timestamp + proposalValidity), proposalValidity);
  bytes memory execData = abi.encodeWithSelector(this.finishVoting.selector);

  LibDataTypes.Module ;
  modules[0] = LibDataTypes.Module.TIME;
  modules[1] = LibDataTypes.Module.SINGLE_EXEC;

  bytes ;
  args[0] = timeArgs;

  LibDataTypes.ModuleData memory moduleData = LibDataTypes.ModuleData(modules, args);

  taskId = IOps(ops).createTask(address(this), execData, moduleData, ETH);
}

function finishVoting() public onlyOps {
  (uint256 fee, address feeToken) = IOps(ops).getFeeDetails();
  transfer(fee, feeToken);
}

function transfer(uint256 _amount, address _paymentToken) internal {
  (bool success, ) = gelato.call{value: _amount}("");
  require(success, "ETH transfer failed");
}

modifier onlyOps() {
  require(msg.sender == address(ops), "OpsReady: onlyOps");
  _;
}

About Gelato

Gelato is a Web3 Cloud Platform enabling developers to build automated, gasless, and off-chain-aware Layer 2 chains and smart contracts.

400+ web3 projects use Gelato to power millions of DeFi, NFT, and gaming transactions.


  • Gelato RaaS: Deploy ZK or OP L2 chains with account abstraction and middleware baked in.

  • Web3 Functions: Connect contracts to off-chain data via decentralized cloud functions.

  • Automate: Automate smart contracts with decentralized execution.

  • Relay: Enable robust, gasless transactions with a simple API.

  • Account Abstraction SDK: Built with Safe, combining gasless transactions with the security of Safe wallets.

Subscribe to our newsletter and turn on

Twitter notifications for the latest Gelato updates!

Interested in building the future of web3? Explore open roles and apply

here.


Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.

Ready to build?

Start with a testnet, launch your mainnet in days, and scale with industry-leading UX.