erc2280 v1.3.0
This repository contains the interface for the ERC2280 standard, the initial EIP Draft content and an implementation called mTKN.
The discussions about the standards are on Github and on Ethereum Magicians.
Simple Summary
Use ERC-712 signatures to trigger transfer, transferFrom or approve methods on an ERC-20 token.
Motivation
The popularity of meta transactions is rising thanks to actors of the community (Austin Thomas Griffith, the MetaCartel, and many more). ERC-1776 was published in February 2019 by Ronan Sandford to start a standardization process around implementations of native meta transactions as extensions of well-known standards. I introduce today the specification for an ERC-20 (only) extension adding native meta transactions support; the ERC-2280 standard. This standard should focus on simplicity, and is a major step in the development of standardized relay networks.
Goal
- Create a standard for wallets and web3 browsers to properly understand these types of signatures and adapt their UI
- Create a standard for Relays to properly integrate any new tokens following the standard
Example Use Cases
- Transfer Tokens without paying for ETH gas: this is a better practice than giving eth to your users so they can make the transactions.
- Pay for gas with the token: the procotol supports a reward argument that redirects funds to the relayer when signature is properly uploaded.
- Make your tokens $ETH independent: Setup a network of relayers (accounts that want to submit signatures for you in exchange of the reward) and the token users will never have to buy ETH to use your tokens.
Abstract
The following standard describe a set of functions to trigger the transfer, approve and transferFrom mechanism with meta transactions. It introduces 7 new functions:
signedTransfer& its verifierverifyTransfertake the same arguments. The first one is a state-modifying method while the latter is a constant method meant for argument / signature verification.signedApprove& its verifierverifyApprovetake the same arguments. The first one is a state-modifying method while the latter is a constant method meant for argument / signature verification.signedTransferFrom& its verifierverifyTransferFromtake the same arguments. The first one is a state-modifying method while the latter is a constant method meant for argument / signature verification.nonceOfis a constant method used to retrieve the current nonce of an account.
Specification
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119.
EIP-712 Types
EIP712Domain
This mandatory type MUST be included in the signed payload.
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}mActors
This type describes the extra actors involved in the meta transaction: the Signer and the Relayer.
struct mActors {
address signer; // Used to check signature validity
address relayer; // Used to enforce a relayer address. Set to address(0) if no restrictions required
}mTxParams
This type describes transaction parameters & reward parameters.
struct mTxParams {
uint256 nonce; // Nonce of the Signer
uint256 gasLimit; // Wanted gas limit
uint256 gasPrice; // Wanted gas price
uint256 reward; // Amount of token provided as reward to the relayer
}mTransfer
This types describe the main type used to trigger a transfer.
struct mTransfer {
address recipient; // Destination address of the transfer
uint256 amount; // Token amount to transfer
mActors actors;
mTxParams txparams;
}mApprove
This type describe the main type used to trigger an approve.
struct mApprove {
address spender; // Target address that would benefit from the approval
uint256 amount; // Amount of token that the address can manipulate
mActors actors;
mTxParams txparams;
}mTransferFrom
This type describe the main type used to trigger a transferFrom.
struct mTransferFrom {
address sender; // Balance that transfers the tokens
address recipient; // Destination of the transfer
uint256 amount; // Amount to move from sender to recipient
mActors actors;
mTxParams txparams;
}ERC-165 supported interfaces
| Name | bytes4 value |
|---|---|
ERC-165 | 0x01ffc9a7 |
ERC-20 | 0x36372b07 |
ERC-20::name | 0x06fdde03 |
ERC-20::symbol | 0x95d89b41 |
ERC-20::decimals | 0x313ce567 |
ERC-2280 | 0x25961920 |
ERC2280 interface
nonceOf
function nonceOf(address account) external view returns (uint256);Notes
The nonce is used to prevent attackers from organizing replay attacks.
Main Mechanism
Return the current expected nonce for given account.
verifyTransfer
function verifyTransfer(
address[3] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external view returns (bool);Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])recipientis the address receiving the token transfer (actors[2])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens transferred fromsignertorecipient(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0],relayerasactors[1],recipientasactors[2]in this precise order.txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Verifies that a transfer to recipient from signer of amount tokens is possible with the provided signature and with current contract state.
Checklist
The function MUST throw if the
mTransferpayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themTransferpayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themTransferpayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themTransferpayload (tx.gasprice >= gasPrice).The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function MUST throw if the
signer’s account balance does not have enough tokens to spend on transfer and on reward (balanceOf(signer) >= amount + reward).
signedTransfer
function signedTransfer(
address[3] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external returns (bool);Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])recipientis the address receiving the token transfer (actors[2])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens transferred fromsignertorecipient(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0],relayerasactors[1],recipientasactors[2]in this precise order.txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Transfers amount amount of tokens from signer's balance to address recipient, and MUST fire the Transfer event.
Checklist
The function MUST throw if the
mTransferpayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themTransferpayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themTransferpayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themTransferpayload (tx.gasprice >= gasPrice),The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function MUST throw if the
signer’s account balance does not have enough tokens to spend on transfer and on reward (balanceOf(signer) >= amount + reward).
verifyApprove
function verifyApprove(
address[3] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external view returns (bool);Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])spenderis the address being approved bysigner(actors[2])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens approved bysignertospender(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0],relayerasactors[1],spenderasactors[2]in this precise order.txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Verifies that an approval for spender of amount tokens on signer's balance is possible with the provided signature and with current contract state.
Checklist
The function MUST throw if the
mApprovepayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themApprovepayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themApprovepayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themApprovepayload (tx.gasprice >= gasPrice).The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function MUST throw if the
signer’s account balance does not have enough tokens to spend on allowance and on reward (balanceOf(signer) >= amount + reward).
signedApprove
function signedApprove(
address[3] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external returns (bool);Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])spenderis the address being approved bysigner(actors[2])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens approved bysignertospender(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0],relayerasactors[1],spenderasactors[2]in this precise order.txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Approves amount amount of tokens from signer's balance to address spender, and MUST fire the Approve event.
Checklist
The function MUST throw if the
mApprovepayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themApprovepayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themApprovepayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themApprovepayload (tx.gasprice >= gasPrice).The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function MUST throw if the
signer’s account balance does not have enough tokens to spend on allowance and on reward (balanceOf(signer) >= amount + reward).
verifyTransferFrom
function verifyTransferFrom(
address[4] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external view returns (bool);Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])senderis the account sending the tokens, and should have approvedsigner(actors[2])recipientis the account receiving the tokens (actors[3])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens transferred fromsendertorecipient(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0]andrelayerasactors[1],senderasactors[2]andrecipientasactors[3]txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Verifies that a transfer from sender to recipient of amount tokens and that signer has at least amount allowance from sender is possible with the provided signature and with current contract state.
Checklist
The function MUST throw if the
mTransferFrompayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themTransferFrompayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themTransferFrompayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themTransferFrompayload (tx.gasprice >= gasPrice).The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function SHOULD throw if the
signer’s account balance does not have enough tokens to spend on reward (balanceOf(signer) >= reward).The function MUST throw if the
signer’s account allowance fromsenderis at leastamount(allowance(sender, signer) >= amount).
signedTransferFrom
function signedTransferFrom(
address[4] calldata actors, uint256[5] calldata txparams, bytes calldata signature
) external returns (bool);
}Notes
signeris the address signing the meta transaction (actors[0])relayeris the address posting the meta transaction to the contract (actors[1])senderis the account sending the tokens, and should have approvedsigner(actors[2])recipientis the account receiving the tokens (actors[3])
nonceis the meta transaction count on this specific token forsigner(txparams[0])gasLimitis the wanted gas limit, set bysigner, should be respected byrelayer(txparams[1])gasPriceis the wanted gas price, set bysigner, should be respected byrelayer(txparams[2])rewardis the amount of tokens that are given torelayerfromsigner(txparams[3])amountis the amount of tokens transferred fromsendertorecipient(txparams[4])
Arguments
actorsArray ofaddresses that containssignerasactors[0]andrelayerasactors[1],senderasactors[2]andrecipientasactors[3]txparamsArray ofuint256that MUST containnonceastxparams[0],gasLimitastxparams[1],gasPriceastxparams[2],rewardastxparams[3]andamountastxparams[4].
Main Mechanism
Triggers transfer from sender to recipient of amount tokens. signer MUST have at least amount allowance from sender. It MUST trigger a Transfer event.
Checklist
The function MUST throw if the
mTransferFrompayload signature is invalid (resulting signer is different than providedsigner).The function MUST throw if real
nonceis different than thenonceprovided in themTransferFrompayload.The function MUST throw if provided
gasis not high enough to match thegasLimitprovided in themTransferFrompayload. This should be checked as soon as the function starts (gasleft() >= gasLimit).The function MUST throw if provided
gasPriceis not high enough to match thegasLimitprovided in themTransferFrompayload (tx.gasprice >= gasPrice).The function MUST throw if provided
relayeris notaddress(0)ANDrelayeris different thanmsg.sender.The function MUST throw if the
signer’s account balance does not have enough tokens to spend on reward (balanceOf(signer) >= reward).The function MUST throw if the
signer’s account allowance fromsenderis at leastamount(allowance(sender, signer) >= amount).
Rationale
The specification was designed with simplicity in mind, and respect to current tools and standards (EIP-712, ERC-165). The 7 new methods can be divided into 4 categories.
nonceOfis the newcomer. This constant method is required to give a nonce to the signers and prevent replay attacks.verifyTransferandsignedTransferacceptrecipientin theactorsargument andamountin thetxparamsargument.verifyApproveandsignedApproveacceptspenderin theactorsargument andamountin thetxparamsargument.verifyTransferFromandsignedTransferFromacceptsenderandrecipientin theactorsargument andamountin thetxparamsargument.
The mActors and mTxParams type have been introduced to factorize arguments in two logical types. Multiple meta-transactions could get packed into the arrays in that way.
The signature is a bytes argument, making it easier to use as most off-chain tools and implementations return hex strings or buffers as results of ECDSA.
Important tools & implementation indications
On-Chain
Specification Interface
The following interface can be found here.
pragma solidity >=0.5.0 <0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/// @title ERC-2280: ERC-20 extension for native meta transactions support
///
/// "Why is this a contract and not an interface ?" - We cannot inherit from interfaces ... If we
/// want to enforce ERC2280 implementations to also implement ERC20 interfaces, then this is the
/// only solution (for the moment, I hope ...)
contract IERC2280 is IERC20 {
// @notice Return the current expected nonce for given `account`.
//
// @return The current nonce for `account`
//
function nonceOf(address account) external view returns (uint256);
// @notice Verifies that a transfer to `recipient` from `signer` of `amount` tokens
// is possible with the provided signature and with current contract state.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough
// tokens to spend on transfer and on reward (`balanceOf(signer) >= amount + reward`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `recipient` is the address receiving the token transfer (`actors[2]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens transferred from `signer` to `recipient` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]`, `relayer` as `actors[1]`,
// `recipient` as `actors[2]` in this precise order.
//
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function verifyTransfer(
address[3] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external view returns (bool);
// @notice Transfers `amount` amount of tokens to address `recipient`, and fires the Transfer event.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough
// tokens to spend on transfer and on reward (`balanceOf(signer) >= amount + reward`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `recipient` is the address receiving the token transfer (`actors[2]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens transferred from `signer` to `recipient` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]`, `relayer` as `actors[1]`,
// `recipient` as `actors[2]` in this precise order.
//
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function signedTransfer(
address[3] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external returns (bool);
// @notice Verifies that an approval for `spender` of `amount` tokens on
// `signer`'s balance is possible with the provided signature and with current contract state.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough tokens
// to spend on allowance and on reward (`balanceOf(signer) >= amount + reward`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `spender` is the address being approved by `signer` (`actors[2]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens approved by `signer` to `spender` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]`, `relayer` as `actors[1]`,
// `spender` as `actors[2]` in this precise order.
//
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function verifyApprove(
address[3] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external view returns (bool);
// @notice Approves `amount` amount of tokens from `signer`'s balance to address `spender`, and
// MUST fire the Approve event.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough tokens
// to spend on allowance and on reward (`balanceOf(signer) >= amount + reward`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `spender` is the address being approved by `signer` (`actors[2]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens approved by `signer` to `spender` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]`, `relayer` as `actors[1]`,
// `spender` as `actors[2]` in this precise order.
//
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function signedApprove(
address[3] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external returns (bool);
// @notice Verifies that a transfer from `sender` to `recipient` of `amount` tokens and that
// `signer` has at least `amount` allowance from `sender` is possible with the
// provided signature and with current contract state.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough tokens to spend
// on reward (`balanceOf(signer) >= reward`).
//
// @dev The function SHOULD throw if the `signer`’s account allowance from `sender` is at least `amount`
// (`allowance(sender, signer) >= amount`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `sender` is the account sending the tokens, and should have approved `signer` (`actors[2]`)
// @dev `recipient` is the account receiving the tokens (`actors[3]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens transferred from `sender` to `recipient` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]` and `relayer` as `actors[1]`, `sender`
// as `actors[2]` and `recipient` as `actors[3]`
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function verifyTransferFrom(
address[4] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external view returns (bool);
// @notice Triggers transfer from `sender` to `recipient` of `amount` tokens. `signer`
// MUST have at least `amount` allowance from `sender`.
// It MUST trigger a Transfer event.
//
// @dev The function MUST throw if the `mTransfer` payload signature is
// invalid (resulting signer is different than provided `signer`).
//
// @dev The function MUST throw if real `nonce` is not high enough to
// match the `nonce` provided in the `mTransfer` payload.
//
// @dev The function MUST throw if provided `gas` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload.
// This should be checked as soon as the function starts. (`gasleft() >= gasLimit`)
//
// @dev The function MUST throw if provided `gasPrice` is not high enough
// to match the `gasLimit` provided in the `mTransfer` payload. (`tx.gasprice >= gasPrice`)
//
// @dev The function MUST throw if provided `relayer` is not `address(0)` AND `relayer`
// is different than `msg.sender`.
//
// @dev The function SHOULD throw if the `signer`’s account balance does not have enough tokens to spend
// on reward (`balanceOf(signer) >= reward`).
//
// @dev The function SHOULD throw if the `signer`’s account allowance from `sender` is at least `amount`
// (`allowance(sender, signer) >= amount`).
//
// @dev `signer` is the address signing the meta transaction (`actors[0]`)
// @dev `relayer` is the address posting the meta transaction to the contract (`actors[1]`)
// @dev `sender` is the account sending the tokens, and should have approved `signer` (`actors[2]`)
// @dev `recipient` is the account receiving the tokens (`actors[3]`)
// @dev `nonce` is the meta transaction count on this specific token for `signer` (`txparams[0]`)
// @dev `gasLimit` is the wanted gas limit, set by `signer`, should be respected by `relayer` (`txparams[1]`)
// @dev `gasPrice` is the wanted gas price, set by `signer`, should be respected by `relayer` (`txparams[2]`)
// @dev `reward` is the amount of tokens that are given to `relayer` from `signer` (`txparams[3]`)
// @dev `amount` is the amount of tokens transferred from `sender` to `recipient` (`txparams[4]`)
//
// @param actors Array of `address`es that contains `signer` as `actors[0]` and `relayer` as `actors[1]`, `sender`
// as `actors[2]` and `recipient` as `actors[3]`
// @param txparams Array of `uint256` that MUST contain `nonce` as `txparams[0]`, `gasLimit` as `txparams[1]`,
// `gasPrice` as `txparams[2]`, `reward` as `txparams[3]` and `amount` as `txparams[4]`.
//
function signedTransferFrom(
address[4] calldata actors,
uint256[5] calldata txparams,
bytes calldata signature
) external returns (bool);
}Splitting bytes signature into r, s & v
To lower the amount of arguments for the call, we prefer taking the signature as a bytes argument. This functions splits the given bytes into uint8 v, bytes32 r and bytes32 s.
function _splitSignature(bytes memory signature) private pure returns (uint8 v, bytes32 r, bytes32 s) {
require(signature.length == 65, "Invalid signature length");
assembly {
r := mload(add(signature, 32))
s := mload(add(signature, 64))
v := and(mload(add(signature, 65)), 255)
}
if (v < 27) {
v += 27;
}
require(v == 27 || v == 28, "Invalid v argument");
}Build the EIP-712 Domain
In order to verify the signatures, the ERC-2280 compliant contract should use the following method:
- Define the types as structs (
EIP712Domain,mActors,mTxParams,mTransfer,mApprove,mTransferFrom). - Define a
hashhelper function with parametric polymorphism for each type defined above. - Define a
verifyhelper function with parametric polymorphism for each type defined above that is going to be signed (onlymTransfer,mApproveandmTransferFrom).
The EIP712Domain should always have the following values for your implementation:
nameshould be the same as the one returned by thenamemethod from theERC20optional specification.versionshould be defined as you want.chainIdshould be the proper ID of the current chain.verifyingContractshould be the address of theERC-2280 contract (address(this)`).
This is an example implementation of the ERC2280Domain, that can also be found here.
pragma solidity >=0.5.0 <0.6.0;
contract ERC2280Domain {
struct Signature {
uint8 v;
bytes32 r;
bytes32 s;
}
struct EIP712Domain {
string name;
string version;
uint256 chainId;
address verifyingContract;
}
struct mTxParams {
uint256 nonce;
uint256 gasLimit;
uint256 gasPrice;
uint256 reward;
}
struct mActors {
address signer;
address relayer;
}
struct mTransfer {
address recipient;
uint256 amount;
mActors actors;
mTxParams txparams;
}
struct mApprove {
address spender;
uint256 amount;
mActors actors;
mTxParams txparams;
}
struct mTransferFrom {
address sender;
address recipient;
uint256 amount;
mActors actors;
mTxParams txparams;
}
bytes32 constant MACTORS_TYPEHASH = keccak256(
"mActors(address signer,address relayer)"
);
bytes32 constant MTXPARAMS_TYPEHASH = keccak256(
"mTxParams(uint256 nonce,uint256 gasLimit,uint256 gasPrice,uint256 reward)"
);
bytes32 constant EIP712DOMAIN_TYPEHASH = keccak256(
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
);
bytes32 constant MTRANSFER_TYPEHASH = keccak256(
// solhint-disable-next-line max-line-length
"mTransfer(address recipient,uint256 amount,mActors actors,mTxParams txparams)mActors(address signer,address relayer)mTxParams(uint256 nonce,uint256 gasLimit,uint256 gasPrice,uint256 reward)"
);
bytes32 constant MAPPROVE_TYPEHASH = keccak256(
// solhint-disable-next-line max-line-length
"mApprove(address spender,uint256 amount,mActors actors,mTxParams txparams)mActors(address signer,address relayer)mTxParams(uint256 nonce,uint256 gasLimit,uint256 gasPrice,uint256 reward)"
);
bytes32 constant MTRANSFERFROM_TYPEHASH = keccak256(
// solhint-disable-next-line max-line-length
"mTransferFrom(address sender,address recipient,uint256 amount,mActors actors,mTxParams txparams)mActors(address signer,address relayer)mTxParams(uint256 nonce,uint256 gasLimit,uint256 gasPrice,uint256 reward)"
);
bytes32 DOMAIN_SEPARATOR;
constructor (string memory domain_name, string memory version, uint256 chainId) public {
DOMAIN_SEPARATOR = hash(EIP712Domain({
name: domain_name,
version: version,
chainId: chainId,
verifyingContract: address(this)
}));
}
function hash(EIP712Domain memory eip712Domain) internal pure returns (bytes32) {
return keccak256(abi.encode(
EIP712DOMAIN_TYPEHASH,
keccak256(bytes(eip712Domain.name)),
keccak256(bytes(eip712Domain.version)),
eip712Domain.chainId,
eip712Domain.verifyingContract
));
}
function hash(mTransfer memory transfer) internal pure returns (bytes32) {
return keccak256(abi.encode(
MTRANSFER_TYPEHASH,
transfer.recipient,
transfer.amount,
hash(transfer.actors),
hash(transfer.txparams)
));
}
function hash(mApprove memory approve) internal pure returns (bytes32) {
return keccak256(abi.encode(
MAPPROVE_TYPEHASH,
approve.spender,
approve.amount,
hash(approve.actors),
hash(approve.txparams)
));
}
function hash(mActors memory actors) internal pure returns (bytes32) {
return keccak256(abi.encode(
MACTORS_TYPEHASH,
actors.signer,
actors.relayer
));
}
function hash(mTxParams memory txparams) internal pure returns (bytes32) {
return keccak256(abi.encode(
MTXPARAMS_TYPEHASH,
txparams.nonce,
txparams.gasLimit,
txparams.gasPrice,
txparams.reward
));
}
function hash(mTransferFrom memory transfer_from) internal pure returns (bytes32) {
return keccak256(abi.encode(
MTRANSFERFROM_TYPEHASH,
transfer_from.sender,
transfer_from.recipient,
transfer_from.amount,
hash(transfer_from.actors),
hash(transfer_from.txparams)
));
}
function verify(mTransfer memory transfer, Signature memory signature) internal view returns (bool) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(transfer)
));
return ecrecover(digest, signature.v, signature.r, signature.s) == transfer.actors.signer;
}
function verify(mApprove memory approve, Signature memory signature) internal view returns (bool) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(approve)
));
return ecrecover(digest, signature.v, signature.r, signature.s) == approve.actors.signer;
}
function verify(mTransferFrom memory transfer_from, Signature memory signature) internal view returns (bool) {
bytes32 digest = keccak256(abi.encodePacked(
"\x19\x01",
DOMAIN_SEPARATOR,
hash(transfer_from)
));
return ecrecover(digest, signature.v, signature.r, signature.s) == transfer_from.actors.signer;
}
}Off-Chain
Generate proper EIP-712 signatures with @ticket721/e712 in Javascript / Typescript
The @ticket721/e712 npm module can be used to properly generate signatures to make the calls.
Complete usage examples of signedTransfer, signedApprove, signedTransferFrom, verifyTransfer, verifyApprove and verifyTransferFrom can be found in the example implementation tests.
The module exposes a helper class, ERC2280Signer. Build it with your domain arguments and quickly generate signatures with your private key, or payloads for your web3 browser. You can also verify proofs.
Example: with private key available
import { ERC2280Signer, EIP712Signature } from '@ticket721/e712';
import { Wallet } from 'ethers';
import { BN } from 'bn.js';
const domain_name = 'my ERC2280';
const domain_version = '1';
const domain_chain_id = 1;
const domain_contract = '0xd0a21D06befee2C5851EbafbcB1131d35B135e87';
const transfer_recipient = '0x19C8239E04ceA1B1C0342E6da5cF3a5Ca54874e1';
const address_zero = '0x0000000000000000000000000000000000000000';
// Build helper class
const ERC2280 = new ERC2280Signer(domain_name, domain_version, domain_chain_id, domain_contract);
// Use your own private keys
const wallet = Wallet.createRandom();
// Generate proof
const sig: EIP712Signature = await ERC2280.transfer(transfer_recipient, new BN(1000), {
signer: wallet.address,
relayer: address_zero
}, {
nonce: new BN(0),
gasLimit: new BN(1000000),
gasPrice: 1000000,
reward: 500
}, wallet.privateKey) as EIP712Signature;
// Verify proofs
const verification = await ERC2280.verifyTransfer(transfer_recipient, new BN(1000), {
signer: wallet.address,
relayer: address_zero
}, {
nonce: new BN(0),
gasLimit: new BN(1000000),
gasPrice: 1000000,
reward: 500
}, sig.hex);Example: sign with web3 browser
import { ERC2280Signer, EIP712Payload } from '@ticket721/e712';
import { BN } from 'bn.js';
const domain_name = 'my ERC2280';
const domain_version = '1';
const domain_chain_id = 1;
const domain_contract = '0xd0a21D06befee2C5851EbafbcB1131d35B135e87';
const transfer_recipient = '0x19C8239E04ceA1B1C0342E6da5cF3a5Ca54874e1';
const address_zero = '0x0000000000000000000000000000000000000000';
const my_web3_browser_address = '0x19C8239E04ceA1B1C0342E6da5cF3a5Ca54874e1';
// Build helper class
const ERC2280 = new ERC2280Signer(domain_name, domain_version, domain_chain_id, domain_contract);
// Generate ready-to-sign payload
const payload: EIP712Payload = await ERC2280.transfer(transfer_recipient, new BN(1000), {
signer: my_web3_browser_address,
relayer: address_zero
}, {
nonce: new BN(0),
gasLimit: new BN(1000000),
gasPrice: 1000000,
reward: 500
}) as EIP712Payload;
// Sign with web3
web3.currentProvider.sendAsync({
method: 'eth_signTypedData_v3',
params: [
my_web3_browser_address,
JSON.stringify(payload)
],
from: my_web3_browser_address},
(error, result) => {
// do your stuff, signature is in result.result (if no errors)
});Implementations
History
- Vitalik discussing it on ethresear.ch (https://ethresear.ch/t/layer-2-gas-payment-abstraction/4513).
- Austin Thomas Griffith implementation on github (https://github.com/austintgriffith/native-meta-transactions).
- ERC-865 (https://github.com/ethereum/EIPs/issues/865).
- ERC-965 (https://github.com/ethereum/EIPs/issues/965).
- ERC-1776 Native Meta Transactions (https://github.com/ethereum/EIPs/issues/1776).
Discussion
My goal is to make a specification that can get broadly adopted in order to start working on relay networks for ERC2280 implementations. There is no point in developing these relays if I'm the only one agreeing with this spec. So please, help this standard come to life by giving your point of view, ideas and remarks here.
Copyright
Copyright and related rights waived via CC0.