MinimalForwarder Contract Reference¶
The MinimalForwarder is an ERC-2771 trusted forwarder that enables gasless meta-transactions for the Agent Registry. It verifies EIP-712 signatures from agents and creators, then forwards their calls to the registry contract while appending the real sender address.
Source: contracts/MinimalForwarder.sol
Solidity: 0.8.24 (optimizer: 200 runs)
License: MIT
Overview¶
The MinimalForwarder sits between agents (who hold no ETH) and the Agent Registry contract:
Agent (no ETH) Relayer (has ETH) MinimalForwarder Agentenregister
| | | |
| 1. Sign EIP-712 | | |
|----------------------->| | |
| | 2. execute(req, sig) | |
| |-------------------------->| |
| | | 3. Verify signature |
| | | 4. Check nonce |
| | | 5. call(data + from) |
| | |---------------------->|
| | | _msgSender() = agent|
| | |<----------------------|
| | 6. Return result | |
| |<--------------------------| |
The agent never touches ETH. The relayer pays gas. The Agent Registry sees the agent's real address as the sender via _msgSender().
EIP-712 Domain¶
The MinimalForwarder uses EIP-712 typed data for signature verification with the following domain:
| Parameter | Value |
|---|---|
name |
"MinimalForwarder" |
version |
"1" |
chainId |
Set at deployment (e.g. 84532 for Base Sepolia) |
verifyingContract |
The MinimalForwarder contract address |
The domain separator is computed at construction time and cached as an immutable value. It is recalculated on-the-fly if the chain ID changes (fork protection).
Structs¶
ForwardRequest¶
The core request struct that agents sign off-chain.
struct ForwardRequest {
address from; // Real signer (agent or creator)
address to; // Target contract (Agentenregister)
uint256 value; // ETH to forward (usually 0)
uint256 gas; // Gas limit for the inner call
uint256 nonce; // Replay protection (auto-incrementing)
uint48 deadline; // Expiry timestamp (0 = no expiry)
bytes data; // Encoded function call
}
| Field | Type | Description |
|---|---|---|
from |
address |
The real signer. Must match the recovered signer from the EIP-712 signature |
to |
address |
Target contract. The relayer typically restricts this to the Agent Registry |
value |
uint256 |
ETH to forward with the call. Usually 0 for registry operations |
gas |
uint256 |
Gas limit for the inner call (e.g. 800000 for registration) |
nonce |
uint256 |
Must equal the current nonce for the from address. Incremented after each execution |
deadline |
uint48 |
Unix timestamp after which the request is invalid. 0 means no expiry |
data |
bytes |
ABI-encoded function call data for the target contract |
EIP-712 Type Hash¶
bytes32 private constant _TYPEHASH = keccak256(
"ForwardRequest(address from,address to,uint256 value,uint256 gas,uint256 nonce,uint48 deadline,bytes data)"
);
Functions¶
getNonce¶
Get the current nonce for a signer address. This value must be used in the next ForwardRequest for that signer.
Parameters:
| Parameter | Type | Description |
|---|---|---|
from |
address |
The signer's address |
Returns: The current nonce (starts at 0, increments by 1 after each successful execution).
domainSeparator¶
Get the EIP-712 domain separator. Recalculated if the chain ID has changed (fork protection).
Returns: The bytes32 domain separator hash.
verify¶
Verify that a signature is valid for a given forward request. Checks:
- Deadline: If
req.deadline != 0, the current block timestamp must not exceed the deadline - Nonce:
req.noncemust equal the stored nonce forreq.from - Signature: The EIP-712 signature must recover to
req.from
Parameters:
| Parameter | Type | Description |
|---|---|---|
req |
ForwardRequest |
The forward request to verify |
signature |
bytes |
65-byte EIP-712 signature (r + s + v) |
Returns: true if the signature is valid, nonce matches, and deadline has not passed.
execute¶
function execute(ForwardRequest calldata req, bytes calldata signature)
external payable returns (bool success, bytes memory returndata)
Execute a meta-transaction: verify the signature, increment the nonce, and forward the call to the target contract.
Parameters:
| Parameter | Type | Description |
|---|---|---|
req |
ForwardRequest |
The forward request to execute |
signature |
bytes |
65-byte EIP-712 signature |
Returns:
| Return | Type | Description |
|---|---|---|
success |
bool |
Whether the inner call succeeded |
returndata |
bytes |
Return data from the inner call |
Reverts if:
- Signature verification fails (
"MinimalForwarder: invalid signature") - The inner call reverts (the revert reason is bubbled up)
ERC-2771 Sender Appending
The execute function appends the real sender address (req.from) to the calldata before calling the target contract:
_msgSender() function reads the last 20 bytes of calldata to extract the real sender.
Events¶
Forwarded¶
event Forwarded(
address indexed from, // The real signer
address indexed to, // The target contract
uint256 nonce, // The nonce used
bool success // Whether the inner call succeeded
);
Emitted after every execute() call, regardless of whether the inner call succeeded.
Security Properties¶
Replay Protection¶
Each signer has an auto-incrementing nonce. After a successful execute(), the nonce is incremented:
A request with a stale nonce (already used) will fail the verify() check.
Deadline Support¶
Requests can include a deadline timestamp. If req.deadline != 0 and block.timestamp > req.deadline, the request is rejected. Setting deadline = 0 means the request never expires.
Signature Verification¶
The contract uses EIP-712 typed data hashing and ecrecover to verify signatures:
- The struct hash is computed from the request fields (with
keccak256(req.data)for the bytes field) - The digest is computed as
keccak256("\x19\x01" || domainSeparator || structHash) ecrecover(digest, v, r, s)must returnreq.from
Revert Reason Bubbling¶
If the inner call to the target contract fails, the revert reason is bubbled up using inline assembly:
This means callers see the original revert reason from the Agentenregister contract (e.g. "Agent not found", "Only owner").
Signature Format¶
Signatures must be exactly 65 bytes in the standard r || s || v format:
| Bytes | Content |
|---|---|
| 0-31 | r component |
| 32-63 | s component |
| 64 | v component (27 or 28) |
Receive Function¶
The contract includes a receive() function to accept ETH, enabling value-forwarding use cases (though the Agent Registry does not use this).
Gas Recommendations¶
| Operation | Recommended gas Field |
|---|---|
registerAgent |
800,000 |
attestCompliance |
200,000 |
reportRevenue |
300,000 |
updateCapability |
200,000 |
updateConstitution |
200,000 |
terminateSelf |
200,000 |
Gas Buffer
The relayer adds a buffer (typically +100,000) on top of the gas field to account for the forwarder's own overhead when calling execute().