We have a very specific use case whereby we get both the address and the signature of a given meta transaction at the same time. Put another way, we don't know the address of the EOA until after the signature is complete.
This means that we do not know the from
address in order to include it in the EIP-712 signature for a typical Forwarder, although we do know it at the point at which the transaction is submitted to the forwarder such that it can be appended as required per ERC-2771.
I wanted to explore any concerns regarding vulnerabilities that might emerge from a Forwarder implementation which omits from
from within the signed digest as described. My sense is that this doesn't raise security concerns since we must still recover the signature against the from
address submitted to the Forwarder and that submitting a bad from
address will halt the verification.
I found it easier to modify the MinimalForwarder from 4 as an example:
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.9.0) (metatx/MinimalForwarder.sol)
pragma solidity ^0.8.17;
import "../utils/cryptography/ECDSA.sol";
import "../utils/cryptography/EIP712.sol";
/**
* @dev Simple minimal forwarder to be used together with an ERC2771 compatible contract. See {ERC2771Context}.
*
* MinimalForwarder is mainly meant for testing, as it is missing features to be a good production-ready forwarder. This
* contract does not intend to have all the properties that are needed for a sound forwarding system. A fully
* functioning forwarding system with good properties requires more complexity. We suggest you look at other projects
* such as the GSN which do have the goal of building a system like that.
*/
contract MinimalForwarder is EIP712 {
using ECDSA for bytes32;
struct ForwardRequest {
address from;
address to;
uint256 value;
uint256 gas;
uint256 nonce;
bytes data;
}
// Included for demonstrative purposes, this is what we actually sign -- without `from`
// struct ForwardRequestSig {
// address to;
// uint256 value;
// uint256 gas;
// uint256 nonce;
// bytes data;
// }
bytes32 private constant _TYPEHASH =
keccak256("ForwardRequest(address to,uint256 value,uint256 gas,uint256 nonce,bytes data)");
mapping(address => uint256) private _nonces;
constructor() EIP712("MinimalForwarder", "0.0.1") {}
function getNonce(address from) public view returns (uint256) {
return _nonces[from];
}
function verify(ForwardRequest calldata req, bytes calldata signature) public view returns (bool) {
address signer = _hashTypedDataV4(
// `from` removed from the digest used in the signed message
keccak256(abi.encode(_TYPEHASH, req.to, req.value, req.gas, req.nonce, keccak256(req.data)))
).recover(signature);
return _nonces[req.from] == req.nonce && signer == req.from;
}
function execute(
ForwardRequest calldata req,
bytes calldata signature // See ForwardRequestSig for what we've actually signed
) public payable returns (bool, bytes memory) {
require(verify(req, signature), "MinimalForwarder: signature does not match request");
_nonces[req.from] = req.nonce + 1;
(bool success, bytes memory returndata) = req.to.call{gas: req.gas, value: req.value}(
abi.encodePacked(req.data, req.from)
);
// Validate that the relayer has sent enough gas for the call.
// See https://ronan.eth.limo/blog/ethereum-gas-dangers/
if (gasleft() <= req.gas / 63) {
// We explicitly trigger invalid opcode to consume all gas and bubble-up the effects, since
// neither revert or assert consume all gas since Solidity 0.8.0
// https://docs.soliditylang.org/en/v0.8.0/control-structures.html#panic-via-assert-and-error-via-require
/// @solidity memory-safe-assembly
assembly {
invalid()
}
}
return (success, returndata);
}
}