Automatically merged updates to draft EIP(s) 3009 (#3105)

Hi, I'm a bot! This change was automatically merged because:

 - It only modifies existing Draft or Last Call EIP(s)
 - The PR was approved or written by at least one author of each modified EIP
 - The build is passing
This commit is contained in:
Peter Jihoon Kim 2020-11-06 17:52:48 -08:00 committed by GitHub
parent f1a807ee3a
commit 03b13e983a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -16,7 +16,7 @@ A contract interface that enables transferring of fungible assets via a signed a
## Abstract
A function called `transferWithAuthorization` to enable meta-transactions and atomic interactions with [ERC-20](./eip-20.md) token contracts via signatures conforming to the [EIP-712](./eip-712.md) typed message signing specification.
A set of functions to enable meta-transactions and atomic interactions with [ERC-20](./eip-20.md) token contracts via signatures conforming to the [EIP-712](./eip-712.md) typed message signing specification.
This enables the user to:
@ -27,11 +27,9 @@ This enables the user to:
- batch multiple transactions with minimal overhead, and
- create and perform multiple transactions without having to worry about them failing due to accidental nonce-reuse or improper ordering by the miner.
The popular USD-backed stablecoin [USDC v2](https://etherscan.io/token/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48) implements an expanded form of this spec. This can also be adopted alongside the [EIP-2612](./eip-2612.md) spec for maximum compatibility with existing applications.
## Motivation
There is an existing spec, [EIP-2612](./eip-2612.md), that also allows meta-transactions. The two primary differences between this spec and EIP-2612 are that:
There is an existing spec, [EIP-2612](./eip-2612), that also allows meta-transactions, and it is encouraged that a contract implements both for maximum compatibility. The two primary differences between this spec and EIP-2612 are that:
- EIP-2612 uses sequential nonces, but this uses random 32-byte nonces, and that
- EIP-2612 relies on the ERC-20 `approve`/`transferFrom` ("ERC-20 allowance") pattern.
@ -41,7 +39,7 @@ The biggest issue with the use of sequential nonces is that it does not allow us
- DApps may unintentionally reuse nonces that have not yet been processed in the blockchain.
- Miners may process the transactions in the incorrect order.
This is especially problematic now that gas prices have become very high and transactions often get queued up and remain unconfirmed for a long time. Non-sequential nonces allow users to create as many transactions as they want at the same time.
This can be especially problematic if the gas prices are very high and transactions often get queued up and remain unconfirmed for a long time. Non-sequential nonces allow users to create as many transactions as they want at the same time.
The ERC-20 allowance mechanism is susceptible to the [multiple withdrawal attack](https://blockchain-projects.readthedocs.io/multiple_withdrawal.html)/[SWC-114](https://swcregistry.io/docs/SWC-114), and encourages antipatterns such as the use of the "infinite" allowance. The wide-prevalence of upgradeable contracts have made the conditions favorable for these attacks to happen in the wild.
@ -49,19 +47,6 @@ The deficiencies of the ERC-20 allowance pattern brought about the development o
## Specification
### Storage
```solidity
mapping(address => mapping(bytes32 => bool)) authorizationStates;
function authorizationState(
address authorizer,
bytes32 nonce
) external view returns (bool);
```
This mapping keeps track of the nonces of the authorizations that have been used (`false` = Unused, `true` = Used). Nonces are randomly generated `bytes32` unique to the token holder's address.
### Event
```solidity
@ -69,30 +54,38 @@ event AuthorizationUsed(
address indexed authorizer,
bytes32 indexed nonce
);
```
This event is emitted when an authorization is used.
### Method
```solidity
// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
```
```solidity
// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
/**
* @notice Execute a transfer with a signed authorization
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
* @notice Returns the state of an authorization
* @dev Nonces are randomly generated 32-byte data unique to the authorizer's
* address
* @param authorizer Authorizer's address
* @param nonce Nonce of the authorization
* @return True if the nonce is used
*/
function authorizationState(
address authorizer,
bytes32 nonce
) external view returns (bool);
/**
* @notice Execute a transfer with a signed authorization
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function transferWithAuthorization(
address from,
address to,
@ -104,9 +97,65 @@ function transferWithAuthorization(
bytes32 r,
bytes32 s
) external;
/**
* @notice Receive a transfer with a signed authorization from the payer
* @dev This has an additional check to ensure that the payee's address matches
* the caller of this function to prevent front-running attacks. (See security
* considerations)
* @param from Payer's address (Authorizer)
* @param to Payee's address
* @param value Amount to be transferred
* @param validAfter The time after which this is valid (unix time)
* @param validBefore The time before which this is valid (unix time)
* @param nonce Unique nonce
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function receiveWithAuthorization(
address from,
address to,
uint256 value,
uint256 validAfter,
uint256 validBefore,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
```
The arguments `v`, `r`, and `s` can be obtained using the [EIP-712](./eip-712.md) typed message signing spec.
**Optional:**
```
event AuthorizationCanceled(
address indexed authorizer,
bytes32 indexed nonce
);
// keccak256("CancelAuthorization(address authorizer,bytes32 nonce)")
bytes32 public constant CANCEL_AUTHORIZATION_TYPEHASH = 0x158b0a9edf7a828aad02f63cd515c68ef2f50ba807396f6d12842833a1597429;
/**
* @notice Attempt to cancel an authorization
* @param authorizer Authorizer's address
* @param nonce Nonce of the authorization
* @param v v of the signature
* @param r r of the signature
* @param s s of the signature
*/
function cancelAuthorization(
address authorizer,
bytes32 nonce,
uint8 v,
bytes32 r,
bytes32 s
) external;
```
The arguments `v`, `r`, and `s` must be obtained using the [EIP-712](./eip-712.md) typed message signing spec.
**Example:**
@ -127,12 +176,26 @@ With the domain separator, the typehash, which is used to identify the type of t
**Example:**
```
// Transfer With Authorization
TypeHash := Keccak256(
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }
// ReceiveWithAuthorization
TypeHash := Keccak256(
"ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)"
)
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce }
// CancelAuthorization
TypeHash := Keccak256(
"CancelAuthorization(address authorizer,bytes32 nonce)"
)
Params := { Authorizer, Nonce }
```
```
// "‖" denotes concatenation.
Digest := Keecak256(
0x1901 ‖ DomainSeparator ‖ Keccak256(ABIEncode(TypeHash, Params...))
@ -141,28 +204,28 @@ Digest := Keecak256(
{ v, r, s } := Sign(Digest, PrivateKey)
```
Smart contract functions that wrap the `transferWithAuthorization` call may choose to reduce the number of arguments by accepting the full ABI-encoded set of arguments for the `transferWithAuthorization` call as a single argument of the type `bytes`.
Smart contract functions that wrap `receiveWithAuthorization` call may choose to reduce the number of arguments by accepting the full ABI-encoded set of arguments for the `receiveWithAuthorization` call as a single argument of the type `bytes`.
**Example:**
```solidity
// keccak256("transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")[0:4]
bytes4 private constant _TRANSFER_WITH_AUTHORIZATION_SELECTOR = 0xe3ee160e;
// keccak256("receiveWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")[0:4]
bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;
function deposit(address token, bytes calldata transferAuthorization)
function deposit(address token, bytes calldata receiveAuthorization)
external
nonReentrant
{
(address from, address to, uint256 amount) = abi.decode(
transferAuthorization[0:96],
receiveAuthorization[0:96],
(address, address, uint256)
);
require(to == address(this), "Recipient is not this contract");
(bool success, ) = token.call(
abi.encodePacked(
_TRANSFER_WITH_AUTHORIZATION_SELECTOR,
transferAuthorization
_RECEIVE_WITH_AUTHORIZATION_SELECTOR,
receiveAuthorization
)
);
require(success, "Failed to transfer tokens");
@ -232,7 +295,7 @@ One might say transaction ordering is one reason why sequential nonces are prefe
- However, for meta-transactions, when a transaction containing a sequential nonce value that is too high is submitted, instead of staying pending, it will revert and fail immediately, resulting in wasted gas.
- The fact that miners can also reorder transactions and include them in the block in the order they want (assuming each transaction was submitted to the network by different meta-transaction relayers) also makes it possible for the meta-transactions to fail even if the nonces used were correct. (e.g. User submits nonces 3, 4 and 5, but miner ends up including them in the block as 4,5,3, resulting in only 3 succeeding)
- Lastly, when using different applications simultaneously, in absence of some sort of an off-chain nonce-tracker, it is not possible to determine what the correct next nonce value is if there exists nonces that are used but haven't been submitted and confirmed by the network.
- This is especially problematic now that high gas price fluctuations often cause transactions to "get stuck" in the pool for a long time. Today, if you make a meta-transaction that uses a sequential nonce from one app, and switch to another app to make another meta-transaction before the previous one confirms, the same nonce will be used twice, resulting in one of the transactions failing.
- Under high gas price conditions, transactions can often "get stuck" in the pool for a long time. Under such a situation, it is much more likely for the same nonce to be unintentionally reused twice. For example, if you make a meta-transaction that uses a sequential nonce from one app, and switch to another app to make another meta-transaction before the previous one confirms, the same nonce will be used if the app relies purely on the data available on-chain, resulting in one of the transactions failing.
- In conclusion, the only way to guarantee transaction ordering is for relayers to submit transactions one at a time, waiting for confirmation between each submission (and the order in which they should be submitted can be part of some off-chain metadata), rendering sequential nonce irrelevant.
### Valid After and Valid Before
@ -247,12 +310,12 @@ One might say transaction ordering is one reason why sequential nonces are prefe
## Backwards Compatibility
New contracts benefit from being able to directly utilize `transferWithAuthorization` in order to create atomic transactions, but existing contracts may still rely on the conventional ERC-20 allowance pattern (`approve`/`transferFrom`).
New contracts benefit from being able to directly utilize EIP-3009 in order to create atomic transactions, but existing contracts may still rely on the conventional ERC-20 allowance pattern (`approve`/`transferFrom`).
In order to add support for `transferWithAuthorization` to existing contracts ("parent contract") that use the ERC-20 allowance pattern, a forwarding contract ("forwarder") can be constructed that takes an authorization and does the following:
In order to add support for EIP-3009 to existing contracts ("parent contract") that use the ERC-20 allowance pattern, a forwarding contract ("forwarder") can be constructed that takes an authorization and does the following:
1. Extract the user and deposit amount from the authorization
2. Call `transferWithAuthorization` to transfer specified funds from the user to the forwarder
2. Call `receiveWithAuthorization` to transfer specified funds from the user to the forwarder
3. Approve the parent contract to spend funds from the forwarder
4. Call the method on the parent contract that spends the allowance set from the forwarder
5. Transfer the ownership of any resulting tokens back to the user
@ -269,7 +332,7 @@ interface IDeFiToken {
}
contract DepositForwarder {
bytes4 private constant _TRANSFER_WITH_AUTHORIZATION_SELECTOR = 0xe3ee160e;
bytes4 private constant _RECEIVE_WITH_AUTHORIZATION_SELECTOR = 0xef55bec6;
IDeFiToken private _parent;
IERC20 private _token;
@ -279,21 +342,21 @@ contract DepositForwarder {
_token = token;
}
function deposit(bytes calldata transferAuthorization)
function deposit(bytes calldata receiveAuthorization)
external
nonReentrant
returns (uint256)
{
(address from, address to, uint256 amount) = abi.decode(
transferAuthorization[0:96],
receiveAuthorization[0:96],
(address, address, uint256)
);
require(to == address(this), "Recipient is not this contract");
(bool success, ) = address(_token).call(
abi.encodePacked(
_TRANSFER_WITH_AUTHORIZATION_SELECTOR,
transferAuthorization
_RECEIVE_WITH_AUTHORIZATION_SELECTOR,
receiveAuthorization
)
);
require(success, "Failed to transfer to the forwarder");
@ -324,7 +387,7 @@ contract DepositForwarder {
## Test Cases
_WIP_
See [EIP3009.test.ts](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/test/EIP3009.test.ts).
## Implementation
@ -334,6 +397,9 @@ abstract contract EIP3009 is IERC20Transfer, EIP712Domain {
// keccak256("TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = 0x7c7c6cdb67a18743f49ec6fa9b35f50d52ed05cbed4cc592e13b44501c1a2267;
// keccak256("ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)")
bytes32 public constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = 0xd099cc98ef71107a616c4f0f941f04c322d8e254fe26b3c6668db87aae413de8;
mapping(address => mapping(bytes32 => bool)) internal _authorizationStates;
event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce);
@ -455,14 +521,12 @@ library EIP712 {
}
```
A fully working implementation can be found in [this repository](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009.sol).
See also:
* [An expanded form of this spec](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009Expanded.sol), as implemented by the USDC smart contract
* [An implementation of EIP-2612](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EI32612.sol) that uses the same EIP-712 library code presented above.
A fully working implementation of EIP-3009 can be found in [this repository](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009.sol). The repository also includes [an implementation of EIP-2612](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EI32612.sol) that uses the EIP-712 library code presented above.
## Security Considerations
Use `receiveWithAuthorization` instead of `transferWithAuthorization` when calling from other smart contracts. It is possible for an attacker watching the transaction pool to extract the transfer authorization and front-run the `transferWithAuthorization` call to execute the transfer without invoking the wrapper function. This could potentially result in unprocessed, locked up deposits. `receiveWithAuthorization` prevents this by performing an additional check that ensures that the caller is the payee. Additionally, if there are multiple contract functions accepting receive authorizations, the app developer could dedicate some leading bytes of the nonce could as the identifier to prevent cross-use.
When submitting multiple transfers simultaneously, be mindful of the fact that relayers and miners will decide the order in which they are processed. This is generally not a problem if the transactions are not dependent on each other, but for transactions that are highly dependent on each other, it is recommended that the signed authorizations are submitted one at a time.
The zero address must be rejected when using `ecrecover` to prevent unauthorized transfers and approvals of funds from the zero address. The built-in `ecrecover` returns the zero address when a malformed signature is provided.