Permit Smart Contracts with EIP 712

Ahmed Castro - Feb 4 '23 - - Dev Community

Account Abstraction, Rollups, and privacy on blockchain are possible thanks to the ability to execute transactions on behalf of someone else securely. In this blog post, we are going to create a "Permit" setup where transactions will be executed by a Relayer using a Verifying Smart Contract. All of this is done securely thanks to cryptography. I hope this video helps you understand where blockchain is headed.

Before we start

For this tutorial you will need NodeJs that I recommend downloading it from Linux via NVM, and also you will need Metamask or another compatible wallet with Goerli funds that you can get from a faucet. You will also need an Infura API Key.

The Verifying Smart Contract

First, we will launch the verifying contract on Goerli Testnet.

// SPDX-License-Identifier: MIT
pragma solidity 0.8.18;

contract Greeter {
    string public greetingText = "Hello World!";
    address public greetingSender;

    struct EIP712Domain {
        string  name;
        string  version;
        uint256 chainId;
        address verifyingContract;
    }

    struct Greeting {
        string text;
        uint deadline;
    }

    bytes32 DOMAIN_SEPARATOR;

    constructor () {
        DOMAIN_SEPARATOR = hash(EIP712Domain({
            name: "Ether Mail",
            version: '1',
            chainId: block.chainid,
            verifyingContract: address(this)
        }));
    }

    function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
            keccak256(bytes(eip712Domain.name)),
            keccak256(bytes(eip712Domain.version)),
            eip712Domain.chainId,
            eip712Domain.verifyingContract
        ));
    }

    function hash(Greeting memory greeting) internal pure returns (bytes32) {
        return keccak256(abi.encode(
            keccak256("Greeting(string text,uint deadline)"),
            keccak256(bytes(greeting.text)),
            greeting.deadline
        ));
    }

    function verify(Greeting memory greeting, address sender, uint8 v, bytes32 r, bytes32 s) public view returns (bool) {
        bytes32 digest = keccak256(abi.encodePacked(
            "\x19\x01",
            DOMAIN_SEPARATOR,
            hash(greeting)
        ));
        return ecrecover(digest, v, r, s) == sender;
    }

    function greet(Greeting memory greeting, address sender, uint8 v, bytes32 r, bytes32 s) public {
        require(verify(greeting, sender, v, r, s), "Invalid signature");
        require(block.timestamp <= greeting.deadline, "Deadline reached");
        greetingText = greeting.text;
        greetingSender = sender;
    }
}
Enter fullscreen mode Exit fullscreen mode

The Frontend

Then we build the frontend consisting of the HTML and JS file. The frontend is the interface that allows us to sign transactions and send them to the Relayer.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
</head>
<body>
  <div>
    <input id="connect_button" type="button" value="Connect" onclick="connectWallet()" style="display: none"></input>
    <p id="account_address" style="display: none"></p>
    <h1>Greeter Verifier</h1>
    <p id="web3_message"></p>
    <p id="contract_state"></p>
    <h3>Sign Greeting</h3>
    <span>Greeting Text</span><br>
    <input type="text" id="_greetingText"></input><br>
    <span>Greeting Deadline</span><br>
    <input type="text" id="_greetingDeadline"></input><br>
    <button type="button" id="sign" onclick="_signMessage()">Sign</button>
    <p id="hashed_message"></p>
    <p id="signature"></p>
    <h3>Verifiy Greeting</h4>
    <span>Greeting Text</span><br>
    <input type="text" id="_greetingTextRelay"></input><br>
    <span>Greeting Deadline</span><br>
    <input type="text" id="_greetingDeadlineRelay"></input><br>
    <span>Greeting Setter</span><br>
    <input type="text" id="_greetingSenderRelay"></input><br>
    <span>Signature</span><br>
    <input type="text" id="_signatureRelay"></input><br>
    <button type="button" id="relay" onclick="_relayGreeting()">Relay</button>
  </div>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/web3/1.3.5/web3.min.js"></script>
  <script type="text/javascript" src="blockchain_stuff.js"></script>
</body>

  <script>
    function _signMessage()
    {
      _greetingText = document.getElementById("_greetingText").value
      _greetingDeadline = document.getElementById("_greetingDeadline").value
      signMessage(_greetingText, _greetingDeadline)
    }

    function _relayGreeting()
    {
      _greetingTextRelay = document.getElementById("_greetingTextRelay").value
      _greetingDeadlineRelay = document.getElementById("_greetingDeadlineRelay").value
      _greetingSenderRelay = document.getElementById("_greetingSenderRelay").value
      _signatureRelay = document.getElementById("_signatureRelay").value
      relayGreeting(_greetingTextRelay, _greetingDeadlineRelay, _greetingSenderRelay, _signatureRelay)
    }
  </script>
</html>
Enter fullscreen mode Exit fullscreen mode

In JavaScript, remember to set the GREETER_CONTRACT_ADDRESS variable with the contract you just launched

blockchain_stuff.js

const NETWORK_ID = 5

const GREETER_CONTRACT_ADDRESS = "0x374257dC5707AEDCC1D4F7D0d1b476a57Fc11194"
const GREETER_CONTRACT_ABI_PATH = "./json_abi/Greeter.json"
var greeterContract

var accounts
var web3

function metamaskReloadCallback() {
  window.ethereum.on('accountsChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se cambió el account, refrescando...";
    window.location.reload()
  })
  window.ethereum.on('networkChanged', (accounts) => {
    document.getElementById("web3_message").textContent="Se el network, refrescando...";
    window.location.reload()
  })
}

const getWeb3 = async () => {
  return new Promise((resolve, reject) => {
    if(document.readyState=="complete")
    {
      if (window.ethereum) {
        const web3 = new Web3(window.ethereum)
        window.location.reload()
        resolve(web3)
      } else {
        reject("must install MetaMask")
        document.getElementById("web3_message").textContent="Error: Porfavor conéctate a Metamask";
      }
    }else
    {
      window.addEventListener("load", async () => {
        if (window.ethereum) {
          const web3 = new Web3(window.ethereum)
          resolve(web3)
        } else {
          reject("must install MetaMask")
          document.getElementById("web3_message").textContent="Error: Please install Metamask";
        }
      });
    }
  });
};

const getContract = async (web3, address, abi_path) => {
  const response = await fetch(abi_path);
  const data = await response.json();

  const netId = await web3.eth.net.getId();
  contract = new web3.eth.Contract(
    data,
    address
    );
  return contract
}

async function loadDapp() {
  metamaskReloadCallback()
  document.getElementById("web3_message").textContent="Please connect to Metamask"
  var awaitWeb3 = async function () {
    web3 = await getWeb3()
    web3.eth.net.getId((err, netId) => {
      if (netId == NETWORK_ID) {
        var awaitContract = async function () {
          greeterContract = await getContract(web3, GREETER_CONTRACT_ADDRESS, GREETER_CONTRACT_ABI_PATH)
          document.getElementById("web3_message").textContent="You are connected to Metamask"
          onContractInitCallback()
          web3.eth.getAccounts(function(err, _accounts){
            accounts = _accounts
            if (err != null)
            {
              console.error("An error occurred: "+err)
            } else if (accounts.length > 0)
            {
              onWalletConnectedCallback()
              document.getElementById("account_address").style.display = "block"
            } else
            {
              document.getElementById("connect_button").style.display = "block"
            }
          });
        };
        awaitContract();
      } else {
        document.getElementById("web3_message").textContent="Please connect to Goerli";
      }
    });
  };
  awaitWeb3();
}

async function connectWallet() {
  await window.ethereum.request({ method: "eth_requestAccounts" })
  accounts = await web3.eth.getAccounts()
  onWalletConnectedCallback()
}

loadDapp()

const onContractInitCallback = async () => {
  var greetingText = await greeterContract.methods.greetingText().call()
  var greetingSender = await greeterContract.methods.greetingSender().call()

  var contract_state = "Greeting Text: " + greetingText
    + ", Greeting Setter: " + greetingSender

  document.getElementById("contract_state").textContent = contract_state;
}

const onWalletConnectedCallback = async () => {
}

// Sign and Relay functions

async function signMessage(message, deadline)
{
  const msgParams = JSON.stringify({
    types: {
        EIP712Domain: [
            { name: 'name', type: 'string' },
            { name: 'version', type: 'string' },
            { name: 'chainId', type: 'uint256' },
            { name: 'verifyingContract', type: 'address' },
        ],
        Greeting: [
            { name: 'text', type: 'string' },
            { name: 'deadline', type: 'uint' }
        ],
    },
    primaryType: 'Greeting',
    domain: {
        name: 'Ether Mail',
        version: '1',
        chainId: NETWORK_ID,
        verifyingContract: GREETER_CONTRACT_ADDRESS,
    },
    message: {
        text: message,
        deadline: deadline,
    },
  });
  console.log(msgParams)

  const signature = await ethereum.request({
    method: "eth_signTypedData_v4",
    params: [accounts[0], msgParams],
  });

  document.getElementById("signature").textContent="Signature: " + signature;
}

async function relayGreeting(greetingText, greetingDeadline, greetingSender, signature)
{
  const r = signature.slice(0, 66);
  const s = "0x" + signature.slice(66, 130);
  const v = parseInt(signature.slice(130, 132), 16);
  console.log({v,r,s})

  var url = "http://localhost:8080/relay?"
  url += "greetingText=" + greetingText
  url += "&greetingDeadline=" + greetingDeadline
  url += "&greetingSender=" + greetingSender
  url += "&v=" + v
  url += "&r=" + r
  url += "&s=" + s

  const relayRequest = new Request(url, {
    method: 'GET',
    headers: new Headers(),
    mode: 'cors',
    cache: 'default',
  });

  fetch(relayRequest);

  alert("Message sent!")
}
Enter fullscreen mode Exit fullscreen mode

The Relayer Backend

Now an example of a backend that is responsible for transmitting transactions to blockchain.

Remember to set the GREETER_CONTRACT_ADDRESS variable with the contract you just launched. And BACKEND_WALLET_ADDRESS with the wallet that will pay the funds.

backend.js

import createAlchemyWeb3 from "@alch/alchemy-web3"
import dotenv from "dotenv"
import fs from "fs"
import cors from "cors"
import express from "express"

const app = express()
dotenv.config();

const GREETER_CONTRACT_ADDRESS = "0x374257dC5707AEDCC1D4F7D0d1b476a57Fc11194"
const BACKEND_WALLET_ADDRESS = "0xb6F5414bAb8d5ad8F33E37591C02f7284E974FcB"
const GREETER_CONTRACT_ABI_PATH = "./json_abi/Greeter.json"
const PORT = 8080
var web3 = null
var greeterContract = null

const loadContract = async (data) => {
  data = JSON.parse(data);

  const netId = await web3.eth.net.getId();
  greeterContract = new web3.eth.Contract(
    data,
    GREETER_CONTRACT_ADDRESS
  );
}

async function initAPI() {
  const { GOERLI_RPC_URL, PRIVATE_KEY } = process.env;
  web3 = createAlchemyWeb3.createAlchemyWeb3(GOERLI_RPC_URL);

  fs.readFile(GREETER_CONTRACT_ABI_PATH, 'utf8', function (err,data) {
    if (err) {
      return console.log(err);
    }
    loadContract(data, web3)
  });

  app.listen(PORT, () => {
    console.log(`Listening to port ${PORT}`)
  })
  app.use(cors({
    origin: '*'
  }));
}

async function relayGreeting(greetingText, greetingDeadline, greetingSender, v, r, s)
{
  const nonce = await web3.eth.getTransactionCount(BACKEND_WALLET_ADDRESS, 'latest'); // nonce starts counting from 0
  const transaction = {
   'from': BACKEND_WALLET_ADDRESS,
   'to': GREETER_CONTRACT_ADDRESS,
   'value': 0,
   'gas': 300000,
   'nonce': nonce,
   'data': greeterContract.methods.greet(
     [greetingText, greetingDeadline],
     greetingSender,
     v,
     r,
     s)
     .encodeABI()
  };
  const { GOERLI_RPC_URL, PRIVATE_KEY } = process.env;
  const signedTx = await web3.eth.accounts.signTransaction(transaction, PRIVATE_KEY);

  web3.eth.sendSignedTransaction(signedTx.rawTransaction, function(error, hash) {
    if (!error) {
      console.log("🎉 The hash of your transaction is: ", hash, "\n");
    } else {
      console.log("❗Something went wrong while submitting your transaction:", error)
    }
  });
}

app.get('/relay', (req, res) => {
  var greetingText = req.query["greetingText"]
  var greetingDeadline = req.query["greetingDeadline"]
  var greetingSender = req.query["greetingSender"]
  var v = req.query["v"]
  var r = req.query["r"]
  var s = req.query["s"]
  var message = greetingSender + " sent a greet: " + " " + greetingText
  relayGreeting(greetingText, greetingDeadline, greetingSender, v, r, s)
  res.setHeader('Content-Type', 'application/json');
  res.send({
    "message": message
  })
})
initAPI()
Enter fullscreen mode Exit fullscreen mode

We will also need to add the json_abi/Contract.json file which contains the Json ABI of the contract we just launched.
json_abi/Greeter.json

[
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "string",
                        "name": "text",
                        "type": "string"
                    },
                    {
                        "internalType": "uint256",
                        "name": "deadline",
                        "type": "uint256"
                    }
                ],
                "internalType": "struct Example.Greeting",
                "name": "greeting",
                "type": "tuple"
            },
            {
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "internalType": "uint8",
                "name": "v",
                "type": "uint8"
            },
            {
                "internalType": "bytes32",
                "name": "r",
                "type": "bytes32"
            },
            {
                "internalType": "bytes32",
                "name": "s",
                "type": "bytes32"
            }
        ],
        "name": "greet",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [],
        "name": "greetingSender",
        "outputs": [
            {
                "internalType": "address",
                "name": "",
                "type": "address"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "greetingText",
        "outputs": [
            {
                "internalType": "string",
                "name": "",
                "type": "string"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "string",
                        "name": "text",
                        "type": "string"
                    },
                    {
                        "internalType": "uint256",
                        "name": "deadline",
                        "type": "uint256"
                    }
                ],
                "internalType": "struct Example.Greeting",
                "name": "greeting",
                "type": "tuple"
            },
            {
                "internalType": "address",
                "name": "sender",
                "type": "address"
            },
            {
                "internalType": "uint8",
                "name": "v",
                "type": "uint8"
            },
            {
                "internalType": "bytes32",
                "name": "r",
                "type": "bytes32"
            },
            {
                "internalType": "bytes32",
                "name": "s",
                "type": "bytes32"
            }
        ],
        "name": "verify",
        "outputs": [
            {
                "internalType": "bool",
                "name": "",
                "type": "bool"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]
Enter fullscreen mode Exit fullscreen mode

And also remember to add a .env with your RPC url and your private key.

.env

GOERLI_RPC_URL=YOURURLHERE
PRIVATE_KEY=YOURKEYHERE
Enter fullscreen mode Exit fullscreen mode

Remember to add your .env files to your gitignore!

.gitignore

.env
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "relayer-demo",
  "version": "1.0.0",
  "description": "",
  "main": "backend.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node backend.js"
  },
  "keywords": [],
  "author": "Filosofía Código",
  "license": "MIT",
  "dependencies": {
    "@alch/alchemy-web3": "^1.4.7",
    "dotenv": "^16.0.3",
    "node-fetch": "^3.3.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, we install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Alternatively, we install the dependencies manually: npm install @alch/alchemy-web3 dotenv node-fetch.

Test the DApp

To start the frontend.

npm install -g lite-server
lite-server
Enter fullscreen mode Exit fullscreen mode

To start the relayer backend

node backend.js
Enter fullscreen mode Exit fullscreen mode

Now you can sign and relay transactions.

Thanks for watching this video!

Follows us on dev.to and in Youtube for everything related to Blockchain development.

. . . . . . . . . . . . . . . . . . . . . . . . .