【Wanted Help・ ERC2771 Forwarder】 write code Metatrasactions'S test code

I would like to prepare a test code for a smart contract using @openzeppelin/contracts/metatx/ERC2771Forwarder.sol.

I have created a test code referring to the file under the test folder, but an error occurs in the verify method of the forwarder contract. I believe there is a problem with the signature data, but I would like to know if you know of a specific fix.

My Forwarder Contract code is here

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

contract ERC2771ForwarderMock is ERC2771Forwarder {
 
    struct ForwardRequest {
        address from;
        address to;
        uint256 value;
        uint256 gas;
        uint256 nonce;
        uint48 deadline;
        bytes data;
    }
    constructor(string memory name) ERC2771Forwarder(name) {}

    function structHash(ForwardRequest calldata request) external view returns (bytes32) {
        return
            _hashTypedDataV4(
                keccak256(
                    abi.encode(
                        _FORWARD_REQUEST_TYPEHASH,
                        request.from,
                        request.to,
                        request.value,
                        request.nonce,
                        request.gas,
                        request.deadline,
                        keccak256(request.data)
                    )
                )
            );
    }
}

My Test code is here

describe("MetaTransaction", function () {

   async function relay(
    forwarder: any, 
    request: any, 
  ) {
    console.log("request:", request)
    // まずトランザクションデータを検証する。
    // const valid = await forwarder.verify(request);
    //if (!valid) throw new Error(`Invalid request`);
    
    // 検証して問題なければトランザクションを実行して Recipientコントラクトの処理を呼び出す。
    const gasLimit = (parseInt(request.gas) + 50000).toString();
    // forward コントラクトのexecuteメソッドを呼び出す。
    await forwarder.execute(request, { gasLimit });
    return 
  }

    it("Gasless tranfer NFT", async function () {
      const { mockNFTV1, forwarder, owner, addr1 } = await loadFixture(deployTokenFixture);
      // mint NFT
      await mintNft(mockNFTV1, owner, addr1.address, "mock", 1);
      
      const beforeBalance = await mockNFTV1.balanceOf(owner.address);
      const beforeBalance2 = await mockNFTV1.balanceOf(addr1.address);
      
      const beforeEthBalance = await addr1.provider.getBalance(addr1.address);
   
      const data = mockNFTV1.interface.encodeFunctionData('safeTransferFrom(address,address,uint256)', [addr1.address, owner.address, 0]);
    
      const {
        request,
        signature,
      } = await signMetaTxRequest(owner, forwarder, {
        to: await mockNFTV1.getAddress(), 
        from: addr1.address, 
        data
      });

      request.signature = signature;
    
      await relay(forwarder, request);
      
      const afterBalance = await mockNFTV1.balanceOf(owner.address);
      const afterBalance2 = await mockNFTV1.balanceOf(addr1.address);
      const afterEthBalance = await addr1.provider.getBalance(addr1.address);
  
      expect(beforeBalance).to.equal(0);
      expect(beforeBalance2).to.equal(1);
      expect(afterBalance).to.equal(1);
      expect(afterBalance2).to.equal(0);
      expect(beforeEthBalance).to.equal(afterEthBalance);
    });

helper's code is here

import ethSigUtil from 'eth-sig-util';
import { ethers } from "ethers";

const EIP712Domain = [
  { name: 'name', type: 'string' },
  { name: 'version', type: 'string' },
  { name: 'chainId', type: 'uint256' },
  { name: 'verifyingContract', type: 'address' }
];

const ForwardRequest = [
  { name: 'from', type: 'address' },
  { name: 'to', type: 'address' },
  { name: 'value', type: 'uint256' },
  { name: 'gas', type: 'uint256' },
  { name: 'nonce', type: 'uint256' },
  { name: 'deadline', type: 'uint48' },
  { name: 'data', type: 'bytes' },
];

export function getMetaTxTypeData(
  chainId: any, 
  verifyingContract: any
) {
  return {
    types: {
      ForwardRequest,
    },
    domain: {
      name: 'ERC2771ForwarderMock',
      version: '0.0.1',
      chainId,
      verifyingContract,
    },
    primaryType: 'ForwardRequest',
  }
};

export async function signTypedData(
  signer: any, 
  data: any
) {
  return await signer.signTypedData(data.domain, data.types, data.request)
}

export async function buildRequest(
  forwarder: any, 
  input: any
) {
  const nonce = await forwarder.nonces(input.from).then((nonce: any) => nonce.toString());
  const numberNonce = Number(nonce);
  const currentTimestamp = Math.floor(new Date().getTime() / 1000);
  const oneWeekInSeconds = 60;
  const futureTimestamp = currentTimestamp + oneWeekInSeconds;
  const uint48Value = ethers.toNumber(futureTimestamp);
  return { 
    data: input.data,
    deadline: uint48Value,
    from: input.from,
    gas: 100000, 
    nonce: numberNonce, 
    to: input.to,
    value: 0, 
  };
}

export async function buildTypedData(
  forwarder: any, 
  request: any  
) {
  const chainId = 31337;
  const typeData = getMetaTxTypeData(chainId, await forwarder.getAddress());
  return { ...typeData, request };
}

export async function signMetaTxRequest(
  signer: any, 
  forwarder: any, 
  input: any
) {
  const request = await buildRequest(forwarder, input);
  const toSign = await buildTypedData(forwarder, request);
  const signature = await signTypedData(signer, toSign);
  return { signature, request };
}

test code's result is here

 24 passing (7s)
  1 failing

  1) Upgradable NFT contract's test
       MetaTransaction
         Gasless tranfer NFT:
     Error: VM Exception while processing transaction: reverted with custom error 'ERC2771ForwarderInvalidSigner("0x6e1B8e2C47a6ae080eEdaA9264055c20cc414744", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8")'
    at ERC2771ForwarderMock.executeBatch (@openzeppelin/contracts/metatx/ERC2771Forwarder.sol:182)
    at ERC2771ForwarderMock.execute (@openzeppelin/contracts/metatx/ERC2771Forwarder.sol:134)
    at processTicksAndRejections (node:internal/process/task_queues:95:5)

Why from address & recovered address are not match??

【Version info】
@openzeppelin/contracts 5.0.1
Compiler version 0.8.20
framework hardhat

My guess is that prior to executing the code above, you need to execute something like this:

await mockNFTV1.approve(mockNFTV1.address, 0, {from: addr1.address})

Or perhaps something like this:

await mockNFTV1.connect(addr1).approve(mockNFTV1.address, 0)

I'm assuming that your NFT type is ERC721.

1 Like

Thank you for looking my code.

Yes, I developed NFT(Type ERC721)

1 Like

I fixed it !! Thak you very much!!

1 Like

NP, just note that you've marked your own comment as a solution to your question.

1 Like

NP, feel free to mark the answer as the solution to your question.

@barakman how is the gas field value calculated ( gas field inside the ForwardRequest struct ) ?

Probably using something like:

await mockNFTV1.approve(mockNFTV1.address, 0, {from: addr1.address})
const gas = await mockNFTV1.safeTransferFrom.estimateGas(addr1.address, owner.address, 0);

Or perhaps:

await mockNFTV1.approve(mockNFTV1.address, 0, {from: addr1.address})
const gas = await mockNFTV1.safeTransferFrom(addr1.address, owner.address, 0).estimateGas();

It really depends on the framework that you're using (e.g., Truffle, HardHat, etc).


BTW, the gas calculation here looks unnecessary as far as I can say:

You can just do await forwarder.execute(request) and let the underlying framework itself determine the exact amount of gas required for this transaction.

2 Likes