Solidity Meta Transactions
A meta-transaction is a signed intent from a user that someone else submits. The user signs what should happen, a relayer sends it, and the contract runs it as the user.
A relayer call changes the user state
The relayer is the sender of the transaction. The value still lands on the user, not on the relayer.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MetaRegistry {
mapping(address => uint256) public nonces;
mapping(address => uint256) public values;
function signedDigest(address user, uint256 newValue, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), user, newValue, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function execute(address user, uint256 newValue, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[user], "bad nonce");
require(ecrecover(signedDigest(user, newValue, nonce), v, r, s) == user, "bad signature");
nonces[user] += 1;
values[user] = newValue;
}
}A bad signature reverts
The signature must come from the user named in the intent. A signature from anyone else reverts the relayer call.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MetaRegistry {
mapping(address => uint256) public nonces;
mapping(address => uint256) public values;
function signedDigest(address user, uint256 newValue, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), user, newValue, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function execute(address user, uint256 newValue, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[user], "bad nonce");
require(ecrecover(signedDigest(user, newValue, nonce), v, r, s) == user, "bad signature");
nonces[user] += 1;
values[user] = newValue;
}
}A tampered value reverts
The signed value is part of the digest. Submitting a different value breaks the signature and reverts.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MetaRegistry {
mapping(address => uint256) public nonces;
mapping(address => uint256) public values;
function signedDigest(address user, uint256 newValue, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), user, newValue, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function execute(address user, uint256 newValue, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[user], "bad nonce");
require(ecrecover(signedDigest(user, newValue, nonce), v, r, s) == user, "bad signature");
nonces[user] += 1;
values[user] = newValue;
}
}Example
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;
contract MetaCounter {
mapping(address => uint256) public nonces;
mapping(address => uint256) public counts;
function signedDigest(address user, uint256 newCount, uint256 nonce) public view returns (bytes32) {
bytes32 dataHash = keccak256(abi.encodePacked(address(this), user, newCount, nonce));
return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash));
}
function execute(address user, uint256 newCount, uint256 nonce, uint8 v, bytes32 r, bytes32 s) public {
require(nonce == nonces[user], "bad nonce");
require(ecrecover(signedDigest(user, newCount, nonce), v, r, s) == user, "bad signature");
nonces[user] += 1;
counts[user] = newCount;
}
}Key points
- Meta-transaction: a user-signed intent submitted by a relayer instead of by the user.
- Relayer: the account that sends the transaction and pays for its submission.
- Intent: the signed instruction saying which user state should change and to what.
- Use a meta-transaction to let a user act without sending the transaction themselves.
- Signature check: the contract recovers the signer and only then changes that signer's state.
In this example, a relayer submits two signed intents for a user, the user count moves from 5 to 9, and the relayer count stays 0 throughout.
Note
the relayer gains no power over the intent, because changing any field breaks the signature.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Remix IDE
Exercise
A relayer submits a signed intent for a user. Whose state does the contract change?