Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP-3005: Batched meta transactions (ERC-20 extension function) #3005

Merged
merged 6 commits into from
Sep 30, 2020

Conversation

defifuture
Copy link
Contributor

@defifuture defifuture commented Sep 25, 2020

Simple Summary

Defines an extension function for ERC-20 (and other fungible token standards), which allows receiving and processing a batch of meta transactions.

Abstract

This EIP defines a new function called processMetaBatch() that extends any fungible token standard, and enables batched meta transactions coming from many senders in one on-chain transaction.

The function must be able to receive multiple meta transactions data and process it. This means validating the data and the signature, before proceeding with token transfers based on the data.

The function enables senders to make gasless transactions, while reducing the relayer's gas cost due to batching.

Motivation

Meta transactions have proven useful as a solution for Ethereum accounts that don't have any ether, but hold ERC-20 tokens and would like to transfer them (gasless transactions).

The current meta transaction relayer implementations only allow relaying one meta transaction at a time. Some also allow batched meta transactions from the same sender. But none offers batched meta transactions from multiple senders.

The motivation behind this EIP is to find a way to allow relaying batched meta transactions from many senders in one on-chain transaction, which also reduces the total gas cost that a relayer needs to cover.

Specification

Meta transaction data

In order to successfully validate and transfer tokens, the processMetaBatch() function needs to process the following data about a meta transaction:

  • sender address
  • receiver address
  • token amount
  • relayer fee
  • a (meta tx) nonce
  • a timestamp or a block number (which represents a due date to process a meta tx)
  • a token address
  • a relayer address
  • a signature

Not all of the data needs to be sent to the function by the relayer. Some of the data can be deduced or extracted from other sources (from transaction data and contract state).

Meta transaction nonce

The token smart contract must keep track of a meta transaction nonce for each token holder.

Meta transaction validation

Validation requirements:

  • sender and receiver addresses cannot be 0x0
  • timestamp or block number must not be expired
  • the sender's balance be equal or greater than the sum of the token amount and the relayer fee
  • all of the data described in Meta transaction data (except the signature) must be hashed. The signed hash must be validated in the function.

Token transfers

If validation is successful, the meta nonce can be increased by 1 and the token transfers can occur:

  • The specified token amount goes to the receiver
  • The relayer fee goes to the relayer (msg.sender)

Implementation

The reference implementation adds a couple of functions to the existing ERC-20 token standard:

  • processMetaBatch()
  • nonceOf()

You can see the implementation of both functions in this file: ERC20MetaBatch.sol. This is an extended ERC-20 contract with added meta transaction batch transfer capabilities.

processMetaBatch()

The processMetaBatch() function is responsible for receiving and processing a batch of meta transactions that change token balances.

function processMetaBatch(address[] memory senders,
                          address[] memory recipients,
                          uint256[] memory amounts,
                          uint256[] memory relayerFees,
                          uint256[] memory blocks,
                          uint8[] memory sigV,
                          bytes32[] memory sigR,
                          bytes32[] memory sigS) public returns (bool) {
    
    address sender;
    uint256 newNonce;
    uint256 relayerFeesSum = 0;
    bytes32 msgHash;
    uint256 i;

    // loop through all meta txs
    for (i = 0; i < senders.length; i++) {
        sender = senders[i];
        newNonce = _metaNonces[sender] + 1;

        if(sender == address(0) || recipients[i] == address(0)) {
            continue; // sender or recipient is 0x0 address, skip this meta tx
        }

        // the meta tx should be processed until (including) the specified block number, otherwise it is invalid
        if(block.number > blocks[i]) {
            continue; // if current block number is bigger than the requested number, skip this meta tx
        }

        // check if meta tx sender's balance is big enough
        if(_balances[sender] < (amounts[i] + relayerFees[i])) {
            continue; // if sender's balance is less than the amount and the relayer fee, skip this meta tx
        }

        // check if the signature is valid
        msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));
        if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
            continue; // if sig is not valid, skip to the next meta tx
        }

        // set a new nonce for the sender
        _metaNonces[sender] = newNonce;

        // transfer tokens
        _balances[sender] -= (amounts[i] + relayerFees[i]);
        _balances[recipients[i]] += amounts[i];
        relayerFeesSum += relayerFees[i];
    }

	// give the relayer the sum of all relayer fees
    _balances[msg.sender] += relayerFeesSum;

    return true;
}

nonceOf()

Nonces are needed due to the replay protection (see Replay attacks under Security Considerations).

mapping (address => uint256) private _metaNonces;

// ...

function nonceOf(address account) public view returns (uint256) {
    return _metaNonces[account];
}

The link to the complete implementation (along with gas usage results) is here: https://github.com/defifuture/erc20-batched-meta-transactions.

Note that the OpenZeppelin ERC-20 implementation was used here. Some other implementation may have named the _balances mapping differently, which would require minor changes in the processMetaBatch() function.

Rationale

All-in-one

Alternative implementations (like GSN) use multiple smart contracts to enable meta transactions, although this increases gas usage. This implementation (EIP-3005) intentionally keeps everything within one function which reduces complexity and gas cost.

The processMetaBatch() function thus does the job of receiving a batch of meta transactions, validating them, and then transferring tokens from one address to another.

Function parameters

As you can see, the processMetaBatch() function in the reference implementation takes the following parameters:

  • an array of sender addresses (meta txs senders, not relayers)
  • an array of receiver addresses
  • an array of amounts
  • an array of relayer fees (relayer is msg.sender)
  • an array of block numbers (a due "date" for meta tx to be processed)
  • Three arrays that represent parts of a signature (v, r, s)

Each item in these arrays represents data of one meta transaction. That's why the correct order in the arrays is very important.

If a relayer gets the order wrong, the processMetaBatch() function would notice that (when validating a signature), because the hash of the meta transaction values would not match the signed hash. A meta transaction with an invalid signature is skipped.

The alternative way of passing meta transaction data into the function

The reference implementation takes parameters as arrays. There's a separate array for each meta transaction data category (the ones that cannot be deduced or extracted from other sources).

A different approach would be to bitpack all data of a meta transaction into one value and then unpack it within the smart contract. The data for a batch of meta transactions would be sent in an array, but there would need to be only one array (of packed data), instead of multiple arrays.

Why is nonce not one of the parameters in the reference implementation?

Meta nonce is used for constructing a signed hash (see the msgHash line where a keccak256 hash is constructed - you'll find a nonce there).

Since a new nonce has to always be bigger than the previous one by exactly 1, there's no need to include it as a parameter array in the processMetaBatch() function, because its value can be deduced.

This also helps avoid the "Stack too deep" error.

Can EIP-2612 nonces mapping be re-used?

The EIP-2612 (permit() function) also requires a nonce mapping. At this point, I'm not sure yet if this mapping should be re-used in case a smart contract implements both EIP-3005 and EIP-2612.

At the first glance, it seems the nonces mapping from EIP-2612 could be re-used, but this should be thought through (and tested) for possible security implications.

Token transfers

Token transfers in the reference implementation could alternatively be done by calling the _transfer() function (part of the OpenZeppelin ERC-20 implementation), but it would increase the gas usage and it would also revert the whole batch if some meta transaction was invalid (the current implementation just skips it).

Another gas usage optimization is to assign total relayer fees to the relayer at the end of the function, and not with every token transfer inside the for loop (thus avoiding multiple SSTORE calls that cost 5'000 gas).

Backwards Compatibility

The code implementation of batched meta transactions is backwards compatible with any fungible token standard, for example, ERC-20 (it only extends it with one function).

Test Cases

Link to tests: https://github.com/defifuture/erc20-batched-meta-transactions/tree/master/test.

Security Considerations

Here is a list of potential security issues and how are they addressed in this implementation.

Forging a meta transaction

The solution against a relayer forging a meta transaction is for a user to sign the meta transaction with their private key.

The processMetaBatch() function then verifies the signature using ecrecover().

Replay attacks

The processMetaBatch() function is secure against two types of a replay attack:

Using the same meta transaction twice in the same token smart contract

A nonce prevents a replay attack where a relayer would send the same meta transaction more than once.

Using the same meta transaction twice in different token smart contracts

A token smart contract address must be added into the signed hash (of a meta transaction).

This address does not need to be sent as a parameter into the processMetaBatch() function. Instead, the function uses address(this) when constructing a hash in order to verify the signature. This way a meta transaction not intended for the token smart contract would be rejected (skipped).

Signature validation

Signing a meta transaction and validating the signature is crucial for this whole scheme to work.

The processMetaBatch() function validates a meta transaction signature, and if it's invalid, the meta transaction is skipped (but the whole on-chain transaction is not reverted).

msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));

if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
    continue; // if sig is not valid, skip to the next meta tx
}

Why not reverting the whole on-chain transaction? Because there could be only one problematic meta transaction, and the others should not be dropped just because of one rotten apple.

That said, it is expected of relayers to validate meta transactions in advance before relaying them. That's why relayers are not entitled to a relayer fee for an invalid meta transaction.

Malicious relayer forcing a user into over-spending

A malicious relayer could delay sending some user's meta transaction until the user would decide to make the token transaction on-chain.

After that, the relayer would relay the delayed meta transaction which would mean that the user would have made two token transactions (over-spending).

Solution: Each meta transaction should have an "expiry date". This is defined in a form of a block number by which the meta transaction must be relayed on-chain.

function processMetaBatch(...
                          uint256[] memory blocks,
                          ...) public returns (bool) {
    
    //...

	// loop through all meta txs
    for (i = 0; i < senders.length; i++) {

        // the meta tx should be processed until (including) the specified block number, otherwise it is invalid
        if(block.number > blocks[i]) {
            continue; // if current block number is bigger than the requested number, skip this meta tx
        }

        //...

Front-running attack

A malicious relayer could scout the Ethereum mempool to steal meta transactions and front-run the original relayer.

Solution: The protection that processMetaBatch() function uses is that it requires the meta transaction sender to add the relayer's Ethereum address as one of the values in the hash (which is then signed).

When the processMetaBatch() function generates a hash it includes the msg.sender address in it:

msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));

if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
    continue; // if sig is not valid, skip to the next meta tx
}

If the meta transaction was "stolen", the signature check would fail because the msg.sender address would not be the same as the intended relayer's address.

A malicious (or too impatient) user sending a meta transaction with the same nonce through multiple relayers at once

A user that is either malicious or just impatient could submit a meta transaction with the same nonce (for the same token contract) to various relayers. Only one of them would get the relayer fee (the first one on-chain), while the others would get an invalid meta transaction.

Solution: Relayers could share a list of their pending meta transactions between each other (sort of an info mempool).

The relayers don't have to fear that someone would steal their respective pending transactions, due to the front-running protection (see above).

If relayers see meta transactions from a certain sender address that have the same nonce and are supposed to be relayed to the same token smart contract, they can decide that only the first registered meta transaction goes through and others are dropped (or in case meta transactions were registered at the same time, the remaining meta transaction could be randomly picked).

At a minimum, relayers need to share this meta transaction data (in order to detect meta transaction collision):

  • sender address
  • token address
  • nonce

Too big due block number

The relayer could trick the meta transaction sender into adding too big due block number - this means a block by which the meta transaction must be processed. The block number could be far in the future, for example, 10 years in the future. This means that the relayer would have 10 years to submit the meta transaction.

One way to solve this problem is by adding an upper bound constraint for a block number within the smart contract. For example, we could say that the specified due block number must not be bigger than 100'000 blocks from the current one (this is around 17 days in the future if we assume 15 seconds block time).

// the meta tx should be processed until (including) the specified block number, otherwise it is invalid
if(block.number > blocks[i] || blocks[i] > (block.number + 100000)) {
    // If current block number is bigger than the requested due block number, skip this meta tx.
    // Also skip if the due block number is too big (bigger than 100'000 blocks in the future).
    continue;
}

This addition could open new security implications, that's why it is left out of this proof-of-concept. But anyone who wishes to implement it should know about this potential constraint, too.

The other way is to keep the processMetaBatch() function as it is and rather check for the too big due block number on the relayer level. In this case, the user could be notified about the problem and could issue a new meta transaction with another relayer that would have a much lower block parameter (and the same nonce).

Copyright

Copyright and related rights are waived via CC0.

Copy link
Contributor

@MicahZoltu MicahZoltu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EIPs repository is a place for standards, not "good ideas". In this case, no standard is being specified so I recommend instead writing this up as an Ethereum Magicians thread, blog post, etc. instead of an EIP.

@defifuture
Copy link
Contributor Author

defifuture commented Sep 27, 2020

The EIPs repository is a place for standards, not "good ideas". In this case, no standard is being specified so I recommend instead writing this up as an Ethereum Magicians thread, blog post, etc. instead of an EIP.

Hi @MicahZoltu !

I admit being a bit internally conflicted with 2 views on how to continue with this EIP:

View A)

I started this whole project as a new standard proposal for a function called processMetaBatch(). In this case the EIP would be similar to permit() EIP-2612 in a sense that it would propose a function that extends the ERC-20 standard.

The reason why I then changed my mind and decided to publish it as an Informational EIP (instead of Standards Track) is that the gas cost tests that I conducted did not turn out the way I wished they would. Meaning there were not such meaningful gas reductions (or none at all) due to batching meta transactions, as opposed to doing normal on-chain token transactions.

View B)

On the other hand, my opinion on whether gas reductions of this implementation are "good enough" is obviously a subjective opinion. I imagine someone else would see the gas usage results differently.

Further more, someone could say that gas usage costs don't even matter for them, because they value the feature of gas-less transactions over the gas cost reduction. And that the code should be agnostic to how people use it or perceive its value.

In this case, the EIP should be changed to Standards Track and issued as an ERC-20 extension proposal (in a similar fashion as the already mentioned EIP-2612).


I'm looking forward to other opinions to clear the dilemma that I have. I have opened two topics on Ethereum Magicians, one before issuing this pull request (link), and the other one after (link).

@MicahZoltu
Copy link
Contributor

If you decide to go forward with an ERC (specify some standard function that contracts would implement) then a pull request to this repository is a very reasonable course of action. If you decide to go with a suggestion on how people implement a thing, then other avenues would be superior.

A lot of people have the misconception that you need to have an EIP to gain adoption of a thing in the Ethereum ecosystem, but in reality there are many EIPs that are never adopted (despite being good ideas) and there are many good ideas out there that are not EIPs but are widely used. I encourage you to not first decide "I want an EIP" and then try to shoehorn the idea into an EIP to meet that goal. Instead, figure out what an ideal solution to your problem would look like and then create an EIP if that is a natural part of the process, or don't if it isn't.

@defifuture
Copy link
Contributor Author

If you decide to go forward with an ERC (specify some standard function that contracts would implement) then a pull request to this repository is a very reasonable course of action. If you decide to go with a suggestion on how people implement a thing, then other avenues would be superior.

Yeah, good point. A new ERC proposal (or extension) should be separate from an opinion piece - I tried to cram these two things into one.

I'll rewrite this whole submission so that it's "only" an ERC proposal. And I'll write my opinion article (about the economics and gas usage) as a blog post somewhere else.

Thanks for your feedback, I appreciate it a lot!

@defifuture defifuture changed the title EIP: Economic viability of batched meta transactions EIP-3005: Batched meta transactions (ERC-20 extension function) Sep 27, 2020
@defifuture
Copy link
Contributor Author

@MicahZoltu I have updated the EIP, hope now it's more suitable.

Copy link
Contributor

@MicahZoltu MicahZoltu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The specification, abstract, and simple summary sections all contain information that should be in other sections. Please move the content of these sections elsewhere or remove the content.

Note: Generally speaking, a good EIP is a short EIP. This whole EIP could probably be quite short if you remove most of the non-normative content. In general, I recommend limiting non-normative content to the bare minimum required to understand the EIP and instead put that content in a blog article or something.

address[] memory recipients,
uint256[] memory amounts,
uint256[] memory relayerFees,
uint256[] memory blocks,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

                          uint256[] memory timestamps,

It is very uncommon that you actually want block numbers. Timestamps is almost always what is desired.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking from the point-of-view that timestamps can be manipulated (by block producers), while block numbers cannot. That said, I don't have a strong opinion regarding this, either way is probably okay.

EIPS/eip-3005.md Outdated
Comment on lines 15 to 17
A meta transaction is a cryptographically signed message that a user sends to a relayer who then makes an on-chain transaction based on the meta transaction data. A relayer effectively pays gas fees in Ether, while a meta tx sender can compensate the relayer in tokens (a "gas-less" transaction).

This proposal offers a solution to relay **multiple** meta transactions as a batch in one on-chain transaction. This reduces the gas cost that the relayer needs to pay, which in turn reduces the relayer fee that each meta tx sender pays in tokens.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A meta transaction is a cryptographically signed message that a user sends to a relayer who then makes an on-chain transaction based on the meta transaction data. A relayer effectively pays gas fees in Ether, while a meta tx sender can compensate the relayer in tokens (a "gas-less" transaction).
This proposal offers a solution to relay **multiple** meta transactions as a batch in one on-chain transaction. This reduces the gas cost that the relayer needs to pay, which in turn reduces the relayer fee that each meta tx sender pays in tokens.
Defines an ERC-20 extension for processing a batch of transfers by signature validation.

Simple summary should be quite short. Think email subject line or sub-title on a forum.

EIPS/eip-3005.md Outdated
Comment on lines 21 to 22
The current meta transaction implementations (such as Gas Station Network - [EIP-1613](https://eips.ethereum.org/EIPS/eip-1613)) only relay one meta transaction through one on-chain transaction (1-to-1: 1 sender, 1 receiver). Gnosis Safe does the same, but can also relay a batch of meta transactions coming from **the same** sender (a 1-to-M batch: 1 sender, many receivers).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The current meta transaction implementations (such as Gas Station Network - [EIP-1613](https://eips.ethereum.org/EIPS/eip-1613)) only relay one meta transaction through one on-chain transaction (1-to-1: 1 sender, 1 receiver). Gnosis Safe does the same, but can also relay a batch of meta transactions coming from **the same** sender (a 1-to-M batch: 1 sender, many receivers).

This is motivation. The abstract should be a fairly terse human readable description of the specification.

EIPS/eip-3005.md Outdated
Comment on lines 33 to 129
## Specification

### How the system works

A user sends a meta transaction to a relayer (through relayer's web app, for example). The relayer waits for multiple meta txs to arrive until the meta tx fees (paid in tokens) cover the cost of the on-chain gas fee (plus some margin that the relayer wants to earn).

Then the relayer relays a batch of meta transactions using one on-chain transaction to the token contract (triggering the `processMetaBatch()` function).

![](../assets/eip-3005/meta-txs-directly-to-token-smart-contract.png)

Technically, the implementation means **adding a couple of functions** to the existing **ERC-20** token standard:

- `processMetaBatch()`
- `nonceOf()`

You can see the proof-of-concept implementation in this file: [ERC20MetaBatch.sol](https://github.com/defifuture/erc20-batched-meta-transactions/blob/master/contracts/ERC20MetaBatch.sol). This is an extended ERC-20 contract with added meta tx batch transfer capabilities (see function `processMetaBatch()`).

### `processMetaBatch()`

The `processMetaBatch()` function is responsible for receiving and processing a batch of meta transactions that change token balances.

```solidity
function processMetaBatch(address[] memory senders,
address[] memory recipients,
uint256[] memory amounts,
uint256[] memory relayerFees,
uint256[] memory blocks,
uint8[] memory sigV,
bytes32[] memory sigR,
bytes32[] memory sigS) public returns (bool) {

address sender;
uint256 newNonce;
uint256 relayerFeesSum = 0;
bytes32 msgHash;
uint256 i;

// loop through all meta txs
for (i = 0; i < senders.length; i++) {
sender = senders[i];
newNonce = _metaNonces[sender] + 1;

if(sender == address(0) || recipients[i] == address(0)) {
continue; // sender or recipient is 0x0 address, skip this meta tx
}

// the meta tx should be processed until (including) the specified block number, otherwise it is invalid
if(block.number > blocks[i]) {
continue; // if current block number is bigger than the requested number, skip this meta tx
}

// check if meta tx sender's balance is big enough
if(_balances[sender] < (amounts[i] + relayerFees[i])) {
continue; // if sender's balance is less than the amount and the relayer fee, skip this meta tx
}

// check if the signature is valid
msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));
if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
continue; // if sig is not valid, skip to the next meta tx
}

// set a new nonce for the sender
_metaNonces[sender] = newNonce;

// transfer tokens
_balances[sender] -= (amounts[i] + relayerFees[i]);
_balances[recipients[i]] += amounts[i];
relayerFeesSum += relayerFees[i];
}

// give the relayer the sum of all relayer fees
_balances[msg.sender] += relayerFeesSum;

return true;
}
```

> Note that the OpenZeppelin ERC-20 implementation was used here. Some other implementation may have named the balances mapping differently, which would require minor changes in the `processMetaBatch()` function.

### `nonceOf()`

Nonces are needed due to the replay protection (see *Replay attacks* under *Security Considerations*).

```solidity
mapping (address => uint256) private _metaNonces;

// ...

function nonceOf(address account) public view returns (uint256) {
return _metaNonces[account];
}
```

> The EIP-2612 (`permit()` function) also requires a nonce mapping. At this point, I'm not sure yet if this mapping should be **re-used** in case a smart contract implements both EIP-3005 and EIP-2612.
>
> At the first glance, it seems the nonce mapping could be re-used, but this should be thought through (and tested) for possible security implications.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
## Specification
### How the system works
A user sends a meta transaction to a relayer (through relayer's web app, for example). The relayer waits for multiple meta txs to arrive until the meta tx fees (paid in tokens) cover the cost of the on-chain gas fee (plus some margin that the relayer wants to earn).
Then the relayer relays a batch of meta transactions using one on-chain transaction to the token contract (triggering the `processMetaBatch()` function).
![](../assets/eip-3005/meta-txs-directly-to-token-smart-contract.png)
Technically, the implementation means **adding a couple of functions** to the existing **ERC-20** token standard:
- `processMetaBatch()`
- `nonceOf()`
You can see the proof-of-concept implementation in this file: [ERC20MetaBatch.sol](https://github.com/defifuture/erc20-batched-meta-transactions/blob/master/contracts/ERC20MetaBatch.sol). This is an extended ERC-20 contract with added meta tx batch transfer capabilities (see function `processMetaBatch()`).
### `processMetaBatch()`
The `processMetaBatch()` function is responsible for receiving and processing a batch of meta transactions that change token balances.
```solidity
function processMetaBatch(address[] memory senders,
address[] memory recipients,
uint256[] memory amounts,
uint256[] memory relayerFees,
uint256[] memory blocks,
uint8[] memory sigV,
bytes32[] memory sigR,
bytes32[] memory sigS) public returns (bool) {
address sender;
uint256 newNonce;
uint256 relayerFeesSum = 0;
bytes32 msgHash;
uint256 i;
// loop through all meta txs
for (i = 0; i < senders.length; i++) {
sender = senders[i];
newNonce = _metaNonces[sender] + 1;
if(sender == address(0) || recipients[i] == address(0)) {
continue; // sender or recipient is 0x0 address, skip this meta tx
}
// the meta tx should be processed until (including) the specified block number, otherwise it is invalid
if(block.number > blocks[i]) {
continue; // if current block number is bigger than the requested number, skip this meta tx
}
// check if meta tx sender's balance is big enough
if(_balances[sender] < (amounts[i] + relayerFees[i])) {
continue; // if sender's balance is less than the amount and the relayer fee, skip this meta tx
}
// check if the signature is valid
msgHash = keccak256(abi.encode(sender, recipients[i], amounts[i], relayerFees[i], newNonce, blocks[i], address(this), msg.sender));
if(sender != ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", msgHash)), sigV[i], sigR[i], sigS[i])) {
continue; // if sig is not valid, skip to the next meta tx
}
// set a new nonce for the sender
_metaNonces[sender] = newNonce;
// transfer tokens
_balances[sender] -= (amounts[i] + relayerFees[i]);
_balances[recipients[i]] += amounts[i];
relayerFeesSum += relayerFees[i];
}
// give the relayer the sum of all relayer fees
_balances[msg.sender] += relayerFeesSum;
return true;
}
```
> Note that the OpenZeppelin ERC-20 implementation was used here. Some other implementation may have named the balances mapping differently, which would require minor changes in the `processMetaBatch()` function.
### `nonceOf()`
Nonces are needed due to the replay protection (see *Replay attacks* under *Security Considerations*).
```solidity
mapping (address => uint256) private _metaNonces;
// ...
function nonceOf(address account) public view returns (uint256) {
return _metaNonces[account];
}
```
> The EIP-2612 (`permit()` function) also requires a nonce mapping. At this point, I'm not sure yet if this mapping should be **re-used** in case a smart contract implements both EIP-3005 and EIP-2612.
>
> At the first glance, it seems the nonce mapping could be re-used, but this should be thought through (and tested) for possible security implications.

This whole specification section should be reduced down to just the public interface. The rest is implementation details and suggestions on how to use it. Implementations should be either included in the ## Implementation section as a reference implementation, or should be part of some other GitHub repository. Descriptions on how to use this at a high level should be a blog post, GitHub readme elsewhere, etc.

Technical specifications are often very short/terse. In this case, you need to describe the interface and what the parameters represent, but that is it. You will need to mention the nonces, though see my other comments on the nonce situation.

EIPS/eip-3005.md Outdated
type: Standards Track
category: ERC
created: 2020-09-25
requires: 20
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem to have a hard dependency on ERC-20. It could work with ERC-777 or any other token really. The only real requirement is that the contract has a fungible thing that can be transferred (needs to be fungible due to the relay fee mechanism). Leaving it as dependent on ERC-20 is probably fine if that is the easiest path forward, but I don't think it is actually necessary here.

@defifuture
Copy link
Contributor Author

@MicahZoltu I've updated the document based on your feedback.

A few things I'm not sure about:

  • Should I keep the image? I think it presents the topic clearly, but I don't know where in the EIP to place it (if at all)
  • The Rationale has some points that consider the implementation, not necessarily the specification (the way parameters are passed & token transfers). Should they be removed?

Copy link
Contributor

@MicahZoltu MicahZoltu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you fix the trailing space in the frontmatter the bot will green light this and we can approve as a draft. More work needs to be done to get this into a state that can move past draft like clearly specifying the interface and the expected behavior, but that can be iterated on during draft phase.

At the moment, this reads like a good idea/suggestion rather than a specification.

EIPS/eip-3005.md Outdated
eip: 3005
title: Batched meta transactions
author: Matt (@defifuture)
discussions-to: https://ethereum-magicians.org/t/eip-3005-the-economic-viability-of-batched-meta-transactions/4673
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
discussions-to: https://ethereum-magicians.org/t/eip-3005-the-economic-viability-of-batched-meta-transactions/4673
discussions-to: https://ethereum-magicians.org/t/eip-3005-the-economic-viability-of-batched-meta-transactions/4673

I think this is what the bot is complaining about.

EIPS/eip-3005.md Outdated
Comment on lines 16 to 17
![](../assets/eip-3005/meta-txs-directly-to-token-smart-contract.png)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Motivation is the best place I can think of for this image. It isn't necessary for the specification, which means it probably shouldn't be part of summary/abstract/specification, and rationale makes even less sense than Motivation. 😄

EIPS/eip-3005.md Outdated
- a relayer address
- a signature

Not all of these data needs to be sent to the function by the relayer. Some of the data can be deduced or extracted from other sources (from transaction data and contract state).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Not all of these data needs to be sent to the function by the relayer. Some of the data can be deduced or extracted from other sources (from transaction data and contract state).
Not all of the data needs to be sent to the function by the relayer. Some of the data can be deduced or extracted from other sources (from transaction data and contract state).

EIPS/eip-3005.md Outdated

### Meta transaction data

In order to successfully validate and transfer tokens, the proposed function needs to process the following data about a meta transaction:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the proposed function

EIPs don't propose, they assert. Write this assuming the person reading it has already decided to implement this and they are just looking to see what to implement. Think email addresses, when you read the RFC for them to correctly implement an email address parser you are expecting to see assertions rather than proposals.

Comment on lines +40 to +48
- sender address
- receiver address
- token amount
- relayer fee
- a (meta tx) nonce
- a timestamp or a block number (which represents a due date to process a meta tx)
- a token address
- a relayer address
- a signature
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order for this standard to be implementable, this will need to be much more strictly defined. Often people will include a Solidity interface definition for the new function, though you also could strictly define the full calldata layout if you preferred.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think I should write this the way it's defined in EIP-2612 (the solidity pseudocode in the Specification)? Or are there any better EIP examples to learn from (applicable to my case, of course)? 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm looking now at EIP-20 and EIP-777 - do these two have a good Specification section?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EIP-20 and EIP-777 would be a reasonable place to start. The goal is to make it so someone can read the Specification section only and get exactly enough information required to implement something that is compatible with the standard. "Here are the things you must do to be considered compatible."

@defifuture
Copy link
Contributor Author

defifuture commented Sep 29, 2020

@MicahZoltu I have fixed the trailing space error, moved the image to the Motivation section and did some other minor text fixes. The automated checking system has successfully completed.

I guess the main issue now remains making the Specification section better. Can you recommend some EIPs as good practice examples regarding this?

(P.S.: I'm very grateful for your feedback and help! 🙂)

@MicahZoltu MicahZoltu merged commit d7413c1 into ethereum:master Sep 30, 2020
tkstanczak pushed a commit to tkstanczak/EIPs that referenced this pull request Nov 7, 2020
…reum#3005)

Defines an extension function for ERC-20 (and other fungible token standards), which allows receiving and processing a batch of meta transactions.
Arachnid pushed a commit to Arachnid/EIPs that referenced this pull request Mar 6, 2021
…reum#3005)

Defines an extension function for ERC-20 (and other fungible token standards), which allows receiving and processing a batch of meta transactions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants