🎓 A1Learner
🔍
0 of 45 lessons completed

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?