I'm following this workshop in an attempt to implement meta transactions - https://blog.openzeppelin.com/gasless-metatransactions-with-openzeppelin-defender/
I am able to sign and submit the tx from my dapp to the autotask webhook URL.
But the autotask fails verification on the forwarder. If I skip the check, the transaction is submitted to the forwarder contract and fails with an invalid signature error: Fail with error 'MinimalForwarder: signature does not match request'
Environment
I'm using autotasks and relayer. The autotask uses the Defender relayer provider to submit the tx to a forwarder. This is on Mumbai.
The forwarder inherits the MinimalForwarder
OZ contract - https://mumbai.polygonscan.com/address/0x7E7F53Ac8A5450EF43eA1Fe20aA8B20492c6bc9f#code
While the recipient inherits ERC2771Context
and specifies the forwarder as trusted in the constructor -
Details
I'm using the code from the workshop example for signing the meta transacation.
I'm just not sure really where to start. I'm confident the args to the function are correct.
Are there any common missteps here to look out for? Ideas for how to debug why the signature is invalid?
Code to reproduce
import ethSigUtil from 'eth-sig-util';
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: 'data', type: 'bytes' },
];
function getMetaTxTypeData(chainId, verifyingContract) {
return {
types: {
EIP712Domain,
ForwardRequest,
},
domain: {
name: 'GSNv2 Forwarder',
version: '0.0.1',
chainId,
verifyingContract,
},
primaryType: 'ForwardRequest',
};
}
async function signTypedData(signer, from, data) {
// If signer is a private key, use it to sign
if (typeof signer === 'string') {
const privateKey = Buffer.from(signer.replace(/^0x/, ''), 'hex');
return ethSigUtil.signTypedMessage(privateKey, { data });
}
// Otherwise, send the signTypedData RPC call
// Note that hardhatvm and metamask require different EIP712 input
const isHardhat = data.domain.chainId == 31337;
const [method, argData] = isHardhat
? ['eth_signTypedData', data]
: ['eth_signTypedData_v4', JSON.stringify(data)];
return await signer.send(method, [from, argData]);
}
async function buildRequest(forwarder, input) {
const nonce = await forwarder
.getNonce(input.from)
.then((nonce) => nonce.toString());
return { value: 0, gas: 1e6, nonce, ...input };
}
async function buildTypedData(forwarder, request) {
const chainId = await forwarder.provider.getNetwork().then((n) => n.chainId);
const typeData = getMetaTxTypeData(chainId, forwarder.address);
return { ...typeData, message: request };
}
async function signMetaTxRequest(signer, forwarder, input) {
const request = await buildRequest(forwarder, input);
const toSign = await buildTypedData(forwarder, request);
const signature = await signTypedData(signer, input.from, toSign);
return { signature, request };
}
export { signMetaTxRequest, buildRequest, buildTypedData };
import { Provider } from '@ethersproject/abstract-provider';
import { Signer } from '@ethersproject/abstract-signer';
import { Contract } from '@ethersproject/contracts';
import Forwarder from '../../deployments/mumbai/Forwarder.json';
import TicTacToe from '../../deployments/mumbai/TicTacToe.json';
import { signMetaTxRequest } from './signer';
export async function sendMetaTx(
provider: Provider,
signer: Signer,
method: string,
args: any[],
) {
const url = process.env.REACT_APP_WEBHOOK_URL;
if (!url) throw new Error(`Missing relayer url`);
const forwarder = new Contract(Forwarder.address, Forwarder.abi, provider);
const ticTacToe = new Contract(TicTacToe.address, TicTacToe.abi, provider);
const from = await signer.getAddress();
const data = ticTacToe.interface.encodeFunctionData(method, args);
const to = ticTacToe.address;
const request = await signMetaTxRequest(signer.provider, forwarder, {
to,
from,
data,
});
console.warn(request);
return fetch(url, {
method: 'POST',
body: JSON.stringify(request),
headers: { 'Content-Type': 'application/json' },
});
}
autotask:
async function relay(forwarder: Contract, request: any, signature: string) {
// Validate request on the forwarder contract
const valid = await forwarder.verify(request, signature);
if (!valid) throw new Error(`Invalid request`);
// Send meta-tx through relayer to the forwarder contract
const gasLimit = (parseInt(request.gas) + 50000).toString();
return await forwarder.execute(request, signature, {gasLimit});
}
// Entrypoint for the Autotask
export async function handler(event: any) {
// Parse webhook payload
if (!event.request || !event.request.body) throw new Error(`Missing payload`);
const {request, signature} = event.request.body;
console.log(`Relaying`, request);
// Initialize Relayer provider and signer, and forwarder contract
const credentials = {...event};
const provider = new DefenderRelayProvider(credentials);
const signer = new DefenderRelaySigner(credentials, provider, {
speed: 'fast',
});
const forwarder = new Contract(Forwarder.address, Forwarder.abi, signer);
// Relay transaction!
const tx = await relay(forwarder, request, signature);
console.log(`Sent meta-tx: ${tx.hash}`);
return {txHash: tx.hash};
}