paint-brush
Ethereum Gasless Metatransactions by Using EIP-712by@thebojda
1,393 reads
1,393 reads

Ethereum Gasless Metatransactions by Using EIP-712

by Laszlo FazekasOctober 17th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In the case of a metatransaction, the user creates a structure describing a transaction, and then digitally signs it with their private key. It is similar to writing a check for someone. The digitally signed transaction is then sent to a relay node, which submits it to a smart contract. The contract verifies the signature, and if it is valid, executes the transaction. The relay node pays for the execution of the contract.
featured image - Ethereum Gasless Metatransactions by Using EIP-712
Laszlo Fazekas HackerNoon profile picture


I recently wrote an article about Karma Money, an alternative currency system based on a unique ERC-20 token. I envisioned Karma Money as a closed system, where users can also pay transaction fees with karma. Building our own blockchain would be one way to make this a reality, but it is a challenging task. To ensure the security and credibility of the blockchain, we would need to establish the necessary infrastructure and a sufficiently large community. It would be much simpler to use an existing blockchain. There are chains such as Gnosis or Polygon, which are fully compatible with Ethereum and have very low transaction fees. The fee for an ERC20 transaction on these chains is typically less than 1 cent. The problem is that this fee must be paid in the chain's own cryptocurrency, which can complicate the use of the chain for users. Fortunately, there is a bridging solution, the EIP-712 metal transactions.


In the case of a metatransaction, the user creates a structure describing a transaction, and then digitally signs it with their private key. It is similar to writing a check for someone. The digitally signed transaction is then sent to a relay node, which submits it to a smart contract. The contract verifies the signature, and if it is valid, executes the transaction. The relay node pays for the execution of the contract.


In a karma transaction, for example, the user provides the amount of the transaction (e.g., 10 karma dollars), the Ethereum address where they want to send the amount, and a transaction fee (in karma dollars) that they are willing to offer for the transaction. This structure is digitally signed and sent to a relay node. If the node finds the transaction fee acceptable, it submits the digitally signed structure to the karma contract, which verifies the signature and executes the transaction. Since the transaction fee is paid in the native currency of the blockchain by the relay node, it appears to the user as if they are paying with karma dollars for the transaction without needing their own blockchain.


After theory, let's take a look at the practice.


The EIP-712 standard defines how to sign structured data packages in a standardized manner. MetaMask displays these structured data in a readable format for the user. An EIP-712 compliant structure, as shown on MetaMask (can be tested on this URL) looks like this:


EIP-712 compliant structure


The above transaction was generated using the following simple code:


async function main() {
      if (!window.ethereum || !window.ethereum.isMetaMask) {
        console.log("Please install MetaMask")
        return
      }

      const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' });
      const chainId = await window.ethereum.request({ method: 'eth_chainId' });
      const eip712domain_type_definition = {
        "EIP712Domain": [
          {
            "name": "name",
            "type": "string"
          },
          {
            "name": "version",
            "type": "string"
          },
          {
            "name": "chainId",
            "type": "uint256"
          },
          {
            "name": "verifyingContract",
            "type": "address"
          }
        ]
      }
      const karma_request_domain = {
        "name": "Karma Request",
        "version": "1",
        "chainId": chainId,
        "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"
      }

      document.getElementById('transfer_request')?.addEventListener("click", async function () {
        const transfer_request = {
          "types": {
            ...eip712domain_type_definition,
            "TransferRequest": [
              {
                "name": "to",
                "type": "address"
              },
              {
                "name": "amount",
                "type": "uint256"
              }
            ]
          },
          "primaryType": "TransferRequest",
          "domain": karma_request_domain,
          "message": {
            "to": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC",
            "amount": 1234
          }
        }
        let signature = await window.ethereum.request({
          "method": "eth_signTypedData_v4",
          "params": [
            accounts[0],
            transfer_request
          ]
        })
        alert("Signature: " + signature)
      })
    }
    main()


The eip712domain_type_definition is a description of a general structure, which contains the metadata. The name field is the name of the structure, the version field is the definition version of the structure, and the chainId and verifyingContract fields determine which contract the message is intended for. The executing contract verifies this metadata in order to ensure that the signed transaction is only executed on the target contract.


The karma_request_domain contains the specific value of the metadata defined by the EIP712Domain structure.


The actual structure that we send to MetaMask for signature is contained in the transfer_request variable. The types block contains the type definitions. Here, the first element is the mandatory EIP712Domain definition, which describes the metadata. This is followed by the actual structure definition, which in this case is the TransferRequest. This is the structure that will appear in MetaMask for the user. The domain block contains the specific value of the metadata, while the message contains the specific structure that we want to sign with the user.


When it comes to karma money, an example of how a metatransaction is put together and sent to the smart contract looks like this:


        const types = {
            "TransferRequest": [
                {
                    "name": "from",
                    "type": "address"
                },
                {
                    "name": "to",
                    "type": "address"
                },
                {
                    "name": "amount",
                    "type": "uint256"
                },
                {
                    "name": "fee",
                    "type": "uint256"
                },
                {
                    "name": "nonce",
                    "type": "uint256"
                }
            ]
        }

        let nonce = await contract.connect(MINER).getNonce(ALICE.address)
        const message = {
            "from": ALICE.address,
            "to": JOHN.address,
            "amount": 10,
            "fee": 1,
            "nonce": nonce
        }

        const signature = await ALICE.signTypedData(karma_request_domain, types, message)
        await contract.connect(MINER).metaTransfer(ALICE.address, JOHN.address, 10, 1, nonce, signature)
        assert.equal(await contract.balanceOf(ALICE.address), ethers.toBigInt(11))


The types variable defines the structure of the transaction. The “from” is the address of the sender, while the “to” is the address of the recipient. The amount represents the quantity of tokens to be transferred. The fee is the “amount” of tokens that we offer to the relay node in exchange for executing our transaction and covering the cost in the native currency of the chain. The “nonce” serves as a counter to ensure the uniqueness of the transaction. Without this field, a transaction could be executed multiple times. However, thanks to the nonce, a signed transaction can only be executed once.


The signTypedData function provided by ethers.js makes it easy to sign EIP-712 structures. It does the same thing as the code presented earlier but with a simpler usage.


The metaTransfer is the method of the karma contract for executing meta-transaction. Let's see how it works:


    function metaTransfer(
        address from,
        address to,
        uint256 amount,
        uint256 fee,
        uint256 nonce,
        bytes calldata signature
    ) public virtual returns (bool) {
        uint256 currentNonce = _useNonce(from, nonce);
        (address recoveredAddress, ECDSA.RecoverError err) = ECDSA.tryRecover(
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        TRANSFER_REQUEST_TYPEHASH,
                        from,
                        to,
                        amount,
                        fee,
                        currentNonce
                    )
                )
            ),
            signature
        );

        require(
            err == ECDSA.RecoverError.NoError && recoveredAddress == from,
            "Signature error"
        );

        _transfer(recoveredAddress, to, amount);
        _transfer(recoveredAddress, msg.sender, fee);
        return true;
    }


In order to validate the signature, we must first generate the hash of the structure. The exact steps for doing this are described in detail in the EIP-712 standard, which includes a sample smart contract and a sample javascript code.


In summary, the essence is that we combine the TYPEHASH (which is the hash of the structure description) with the fields of the structure using abi.encode. Then produces a keccak256 hash. The hash is passed to the _hashTypedDataV4 method, inherited from the EIP712 OpenZeppelin contract in the Karma contract. This function adds metadata to our structure and generates the final hash, making structure validation very simple and transparent. The outermost function is ECDSA.tryRecover, which attempts to recover the signer's address from the hash and signature. If it matches the address of the “from“ parameter, the signature is valid. At the end of the code, the actual transaction is executed and the relay node performing the transaction receives the fee.


EIP-712 is a general standard for signing structures, making it just one of many uses for implementing meta-transactions. As the signature can be validated not only with smart contracts, it can also be very useful in non-blockchain applications. For example, it can be used for server-side authentication, where the user identifies themselves with their private key. Such a system can provide a high level of security typically associated with cryptocurrencies, allowing for the possibility of using a web application only with a hardware key. In addition, individual API calls can also be signed with the help of MetaMask.


I hope that this brief overview of the EIP-712 standard has been inspiring for many and that you will be able to utilize it in both blockchain-based and non-blockchain projects.


Every code is available on the GitHub repo of karma money.