DEV Community

Bhaskar
Bhaskar

Posted on • Edited on

Biconomy - Win your customer's heart with Gasless Transactions

Gas Price is a necessary evil in the world of Web3. Although it plays a vital role in terms of rewarding the “Miners” and making smart contract execution more secure, it creates an awful user experience. Even from a purely business perspective, it makes much more sense in having a fixed billing system for the users, while the execution costs are covered by the service provider. With the traditional approach, it’s impossible to build such a system without introducing a centralized entity into the system.

💡Biconomy - A ray of hope

Among other features, Biconomy makes it possible for developers to provide “Gasless Transactions” with the least amount of effort. This is a fully decentralized solution powered by smart contracts and no compromise is made in terms of security and stability.

Biconomy is based on the simple concept that the service provider pays the gas on behalf of the users. The users are only required to sign a particular message while interacting with the smart contract. This signature is then validated by Biconomy and if sufficient funds are available to sponsor the transaction, the gas is paid, and the transaction details are forwarded to the Dapp smart contract.

The important point to note is that there is nothing like a transaction without gas fees, however, Biconomy just enables us to maintain a special account where we deposit enough funds to pay for the gas price for all users. The gas price gets paid from this “special” account and it’s up to us or the project admin to ensure that sufficient funds are always available for sponsoring transactions. To make this even simpler, Biconomy provides an intuitive Dashboard for managing everything.

🎯Scope of this article

This article will act as a beginner-friendly tutorial for getting started with Biconomy. For the sake of simplicity, we will be focusing only on Gasless Transactions, specifically Gasless Transactions using EIP 2771-based approach. This article won’t discuss the architecture used by Biconomy nor the various other jaw-dropping features provided. To learn about them I recommend giving their official documentation a read.

In this tutorial we will:

  • Write a simple Smart Contract using EIP 2771 to make it compatible with Biconomy.
  • Deploy and test our smart contract on the Polygon Testnet (Mumbai).
  • Build a very simple front end using React to interact with our smart contract.

I follow a “code-first-explanation-next” approach. Please let me know if this is helpful or if I should try any different approach in the comment section.

⚠️Disclaimer

At the point of writing this article, Biconomy is transitioning to a newer SDK-based approach that brings yet more to the table, however here we will be using the older approach which is much simpler and sufficient for our needs. Documentation of the latest SDK can be found here. The new SDK is still WIP and not polished enough for developers to use. Once ready, I plan to cover it in my future articles.

🎒Pre-requisites

I know you all are excited to get started, but before we dive in, make sure you got the following pre-requisites sorted:

  • Remix IDE: For developing smart contracts
  • Metamask: Metamask is a popular non-custodial wallet used for interacting with the blockchain.
  • Connected with Testnet: Although you can use any network of your choice, I will be using Polygon’s Mumbai Testnet. If you want to follow along, make sure you have added Mumbai Testnet in Metamask following this article, and then fund your account using this faucet.

💻Writing the Smart Contract

Let’s get started with the crux of our project, i.e., the smart contract. In your Remix IDE create a new file named Greetings.sol and paste the following code.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

contract Greetings {
    mapping (address => string) public greetings;

    function setGreeting(string memory newGreeting) public {
        greetings[msg.sender] = newGreeting;
    }
}
Enter fullscreen mode Exit fullscreen mode

This is a very simple smart contract that stores a string each corresponding to each address that called the setGreetingfunction. The string to be stored is passed as an argument to the function. The important point to note here is msg.sender is used for finding the account address that invoked the setGreeting function.

Let’s try to understand the challenge of using the above contract with Biconomy. Refer to the following diagram to understand how Biconomy forwards transactions:
Biconomy TrustedForwarder
If you look closely, you will understand that a “TrustedForwarder” contract is used to invoke our smart contract. In this scenario, regardless of which account initiates the call, the msg.sender will always return the address of the caller, i.e., the TrustedForwarder’s address. To solve this issue, we have to make the following changes to our smart contract:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

import "@openzeppelin/contracts/metatx/ERC2771Context.sol";

contract Greetings is ERC2771Context {

    constructor(address trustedForwarderAddress) ERC2771Context(trustedForwarderAddress) {}

    mapping (address => string) public greetings;

    function setGreeting(string memory newGreeting) public {
        greetings[_msgSender()] = newGreeting;
    }
}
Enter fullscreen mode Exit fullscreen mode

Here we are doing the following:

  • We are using the ERC2771Context contract from OpenZeppelin contracts. It provides us with the _msgSender() function that will be very important to us.
  • The trustedForwarderAddress is the contract that we expect to call our smart contract. This forwarder is responsible for signature verification and protection against replay attacks. Before forwarding the transaction, the signer (our user’s address) is appended to the calldata and passed to the smart contact. The _msgSender() function returns this address.

    You can find the list of Biconomy’s TrustedForwarder’s addresses here.

  • Inside our code we replace msg.sender with _msgSender() to reflect the correct user.

And with that our smart contract is ready for gasless transactions!!

Deployment and Biconomy Setup

Now that our smart contract is ready, it’s time to deploy our smart contract. Let’s deploy it to Mumbai Testnet (You can choose any other network of your choice).

  • In the deploy section of Remix, select Injected Provider - MetaMask as the Environment. Also, make sure your Metamask is connected with your desired chain.
  • For your chosen chain, find the respective TrustedForwarder address here. Since I am using Mumbai Testnet, mine is 0x69015912AA33720b842dCD6aC059Ed623F28d9f7. You must pass this address as a constructor argument while deploying the contract.
  • Finally, click on transact and confirm the MetaMask popup to deploy the smart contract. Deploying Smart Contract Now to set up Biconomy, sign in to your Biconomy Dashboard. Next, you have to register a new Dapp and mention the chain in which your smart contract is deployed. Register new Dapp in Biconomy Once registered, click on the card corresponding to your Dapp and this will open the respective Dapp’s dashboard. Here you can see the various details associated with your Dapp. Newly registered Dapp For a freshly created Dapp, the Gas Balance would be 0. Before you can let users enjoy gasless transactions, you have to load crypto (native token) into this Dapp to pay for the gas on behalf of the users. Click on the Fill Gas button, enter the amount of token you want to deposit, and click Fill Me Up. A MetaMask popup will ask you to send the required amount of tokens and confirm it to deposit the funds. It should be reflected on your Dapp promptly under GAS BALANCE.

A single Dapp can be associated with multiple smart contracts. Head to the Smart Contract section in your dashboard. Enter the Name, address, and ABI of your smart contract. All these details can be found in Remix. Make sure that Meta Transaction Type is set to Trusted Forwarder. Click on Add to add the smart contract to your Dapp.
Adding Smart Contract
Now the final step is to create API Endpoints for the function we want to invoke. Head to the Dapp APIs section.

  • Click on the white Add Button
  • Enter a suitable name for your API.
  • Choose the corresponding Smart Contract and Method to be invoked.
  • Click on the orange Add button to create a new API endpoint. Creating new Endpoint Hurray!! Finally, we are ready for gasless transactions 👏👏👏.

🌐Building a basic website

Now that we are ready for gasless transactions, it’s time to build a simple website for interacting with our smart contract. In this tutorial, we are going to use React for building our demo website.

Use the following command to create a new react project:

npx create-react-app biconomy_tutorial
Enter fullscreen mode Exit fullscreen mode

I have named my project biconomy_tutorial, however, feel free to choose any name of your liking. Once the project is initialized, move inside the newly created project folder. Next, we will be using the biconomy/mexa package for interacting with our smart contract. Use the following command for installing it:

npm install @biconomy/mexa
Enter fullscreen mode Exit fullscreen mode

🚫Known Issue

In case you have used any other libraries like web3.js, you must be already familiar with the Webpack Error. For those who are new, whenever you try to import the biconomy/mexa package in your React code, you would get an error message similar to the following:
Webpack Error
To resolve this we will need react-app-rewired. First, install the required dependencies using the following command:

npm install react-app-rewired crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url path-browserify browserify-zlib process buffer util
Enter fullscreen mode Exit fullscreen mode

Now create a new file called config-overrides.js and paste in the following code:

const webpack = require('webpack');
module.exports = function override(config) {
  config.ignoreWarnings = [/Failed to parse source map/];
  const fallback = config.resolve.fallback || {};
  Object.assign(fallback, {
    crypto: require.resolve("crypto-browserify"),
    stream: require.resolve("stream-browserify"),
    assert: require.resolve("assert"),
    http: require.resolve("stream-http"),
    https: require.resolve("https-browserify"),
    os: require.resolve("os-browserify"),
    url: require.resolve("url"),
    path: require.resolve("path-browserify"),
    zlib: require.resolve("browserify-zlib"),
    process: require.resolve("process/browser"),
    buffer: require.resolve("buffer"),
    util: require.resolve("util"),
    fs: false,
    tls: false,
    net: false,
  });
  config.resolve.fallback = fallback;
  config.plugins = (config.plugins || []).concat([
    new webpack.ProvidePlugin({
      process: 'process/browser',
      Buffer: ['buffer', 'Buffer'],
    })
  ])
  return config;
}
Enter fullscreen mode Exit fullscreen mode

Finally in the package.json file change the start task to react-app-wired start. Finally, close and restart the react app. Now this problem should be resolved.

Building the website

Open the App.js file in our react project in any code editor of your choice and make the following modifications:

import './App.css';
import { Biconomy } from "@biconomy/mexa";
import { ethers } from 'ethers';

function App() {

  const setMessage = async (message) => {
  }

  return (
    <div className="App">
      <header className="App-header">
        <button onClick={() => setMessage("Hello World")}>Set Message</button>
      </header>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Here we added a simple button that calls the setMessage function. This function takes a string as input and passes it to the smart contract function. For simplicity, here we are hardcoding the string to be “Hello World”. To simplify our project even further, we will implement the complete logic of using Biconomy inside the setMessage function, however, an experienced React developer would prefer to use proper state management.

Before using Biconomy, we have to first initialize it. The code for initializing Biconomy is as follows:

const setMessage = async (message) => {
  const biconomy = new Biconomy(window.ethereum, {
    apiKey: "5hUl....1a87",
    debug: true,
    contractAddresses: [
      "0x160d3a01bf31cabbb6fcb60b5573bbad859e46c7"
    ]
  })
  await biconomy.init();
}
Enter fullscreen mode Exit fullscreen mode

In the above code snippet note that:

  • As the Ethereum provider, we are using window.ethereum, i.e., the Ethereum object injected by the Metamask wallet. In case you want to use a different wallet, you have to modify it respectively. (Let me know in the comments if you want an example of using a different wallet).
  • Make sure you enter your own API Key for Biconomy that you will find in the Biconomy Dashboard. Needless to say, instead of hardcoding it, it’s recommended to use environment variables.
  • We also have to pass a list of contract addresses that we want to interact with. Ideally, this list will contain only those contract addresses that we want to interact with, and not all the contract addresses added to our dashboard.

The next step is to create a contract instance. The beauty of Biconomy is from here on, the process of interacting with the smart contract is similar to the traditional approach. I am using ethers, however, in the official documentation, you will also find how to use web3.js.

const setMessage = async(message) => {
  ...
    const contractInstance = new ethers.Contract(
        CONTRACT_ADDRESS,
        CONTRACT_ABI,
        biconomy.ethersProvider
    )

    const ethersProvider = new ethers.providers.Web3Provider(window.ethereum);
    const connectedAddress = (await ethersProvider.listAccounts())[0];

    const tx = await contractInstance.populateTransaction.setGreeting(message, {
        gasLimit: 1000000,
        gasPrice: 1000000000
    })
    const txDetails = {
        from: contractAddress,
        to: CONTRACT_ADDRESS,
        data: tx.data,
        signatureType: "PERSONAL_SIGN"
    }

    const provider = await biconomy.provider
    await provider.send("eth_sendTransaction", [txDetails])
}
Enter fullscreen mode Exit fullscreen mode

Here we are performing the following tasks:

  • Creating an instance of the smart contract.
  • We get and store the current connected Metamask account in the connectedAddress variable.
  • Next, we define the transaction and store it in tx variable. Note that here we are not sending the transaction to the blockchain. Rather we are using the populateTransaction method because we want to generate the calldata that would be sent to the blockchain.
  • Any function invocation in the blockchain is also recorded in form of a transaction. Without getting into the complexities, you can assume that what differentiates a transfer of native tokens (also called transaction) and calling a function is that in the latter, a signed payload containing the function signature and the arguments are passed which is also termed as the calldata. We generate these transaction details and store them in the txDetails variable.
  • Next, we obtain the provider from the defined Biconomy object.
  • Finally, we send the transaction for verification to the blockchain. Here we use the eth_sendTransaction function. This is where the users receive a Metamask popup asking them to sign a specific payload. Behind the hood, this transaction payload is actually passed by the Biconomy Relay to the TrustedForwarder contract, and it also pays for the required gas.

With this, you are now ready to make Gasless transactions 😎. Go ahead and give your newly created website a try. I have a small task for you:

👉Try to make a gasless transaction. Verify whether the message was set properly and
whether the user had to pay any gas.

👉Let me know your findings in the comment section
Enter fullscreen mode Exit fullscreen mode

In case you run into errors or feel I was not able to explain the topic properly, please let me know and I will try my best to clear every doubt.

👨‍💻About Me

Hi, I am Bhaskar Dutta, a Software Developer here at Xenabler where we try to bring technology and people together, one commit at a time.

If you have read so far, I feel we are going to be partners in an otherwise long journey of exploring the ever-changing world of technology. I would really want to connect with my readers and know them a bit more and if you want something similar, do send a connect request on LinkedIn.

Before saying goodbye, remember to take care of yourself and let the special people in your life know you Love Them. We will soon meet again in a different tutorial, till then Take Care of Yourself, let the special people in your life know you Love Them, and Keep Building.

Top comments (0)