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