-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial draft for "EIP-3009: transferWithAuthorization"
- Loading branch information
Showing
1 changed file
with
317 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,317 @@ | ||
--- | ||
eip: 3009 | ||
title: `transferWithAuthorization` - Gas-Abstracted ERC20 transactions with EIP-712 | ||
author: Peter Jihoon Kim (@petejkim), Kevin Britz (@kbrizzle), David Knott (@DavidLKnott) | ||
discussions-to: https://github.com/ethereum/EIPs/issues/3010 | ||
status: Draft | ||
type: Standards Track | ||
category: ERC | ||
created: 2020-09-28 | ||
requires: 20, 712 | ||
--- | ||
|
||
## Simple Summary | ||
|
||
A function called `transferWithAuthorization` to enable gas-abstracted and atomic interactions with [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token contracts via signatures conforming to the [EIP-712](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-712.md) typed message signing specification. | ||
|
||
This enables the user to: | ||
* delegate the gas payment to someone else, | ||
* pay for gas in the token itself rather than in ETH, | ||
* perform one or more token transfers and other operations in a single atomic transaction, | ||
* transfer ERC-20 tokens to another address, and have the recipient submit the transaction, | ||
* 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](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md) spec for maximum compatibility with existing applications. | ||
|
||
## Abstract | ||
|
||
This is a standard that extends the ERC-20 spec by introducing a new function, `transferWithAuthorization`. This function enables users to transfer tokens without also having to submit a transaction and pay for gas themselves. This function also can also be used to transfer tokens and perform another action in a single atomic transaction. | ||
|
||
## Motivation | ||
|
||
There is an existing spec, [EIP-2612](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2612.md), that also allows gas-abstracted transactions. 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. | ||
|
||
The biggest issue with the use of sequential nonces is that it does not allow users to perform more than one transaction at time without risking their transactions failing, because: | ||
|
||
* 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. | ||
|
||
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. | ||
|
||
The deficiencies of the ERC-20 allowance pattern brought about the development of alternative token standards such as the [ERC-777](https://eips.ethereum.org/EIPS/eip-777) and [ERC-677](https://github.com/ethereum/EIPs/issues/677). However, they haven't been able to gain much adoption due to compatibility and potential security issues. | ||
|
||
## 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 | ||
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 | ||
/** | ||
* @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, | ||
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](https://eips.ethereum.org/EIPS/eip-712) typed message signing spec. EIP-712 ensures that the signatures generated are valid only for this specific instance of the token contract and cannot be replayed on networks with different chain IDs by incorporating the contract address and the chain ID in a Keccak-256 hash digest called the domain separator. The actual set of parameters used to derive the domain separator is up to the implementing contract. | ||
|
||
**Example:** | ||
``` | ||
DomainSeparator := Keccak256(ABIEncode( | ||
Keccak256( | ||
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" | ||
), | ||
Keccak256("USD Coin"), // name | ||
Keccak256("2"), // version | ||
1, // chainId | ||
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 // verifyingContract | ||
)) | ||
``` | ||
|
||
With the domain separator, the typehash, which is used to identify the type of the EIP712 message being used, and the values of the parameters, you are able to derive a Keccak-256 hash digest which can then be signed using the token holder's private key. | ||
|
||
**Example:** | ||
|
||
``` | ||
TypeHash := Keccak256( | ||
"TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" | ||
) | ||
Params := { From, To, Value, ValidAfter, ValidBefore, Nonce } | ||
// "‖" denotes concatenation. | ||
Digest := Keecak256( | ||
0x1901 ‖ DomainSeparator ‖ Keccak256(ABIEncode(TypeHash, Params...)) | ||
) | ||
{ 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`. | ||
|
||
**Example:** | ||
```solidity | ||
// keccak256("transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,uint8,bytes32,bytes32)")[0:4] | ||
bytes4 private constant _TRANSFER_WITH_AUTHORIZATION_SELECTOR = 0xe3ee160e; | ||
function deposit(address token, bytes calldata transferAuthorization) | ||
external | ||
nonReentrant | ||
{ | ||
(address from, address to, uint256 amount) = abi.decode( | ||
transferAuthorization[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 | ||
) | ||
); | ||
require(success, "Failed to transfer tokens"); | ||
... | ||
} | ||
``` | ||
|
||
### Use with web3 providers | ||
|
||
The signature for an authorization can be obtained using a web3 provider with the `eth_signTypedData{_v4}` method. | ||
|
||
**Example (TransferWithAuthorization):** | ||
```javascript | ||
const data = { | ||
types: { | ||
EIP712Domain: [ | ||
{ name: "name", type: "string" }, | ||
{ name: "version", type: "string" }, | ||
{ name: "chainId", type: "uint256" }, | ||
{ name: "verifyingContract", type: "address" }, | ||
], | ||
TransferWithAuthorization: [ | ||
{ name: "from", type: "address" }, | ||
{ name: "to", type: "address" }, | ||
{ name: "value", type: "uint256" }, | ||
{ name: "validAfter", type: "uint256" }, | ||
{ name: "validBefore", type: "uint256" }, | ||
{ name: "nonce", type: "bytes32" }, | ||
], | ||
}, | ||
domain: { | ||
name: tokenName, | ||
version: tokenVersion, | ||
chainId: selectedChainId, | ||
verifyingContract: tokenAddress, | ||
}, | ||
primaryType: "TransferWithAuthorization", | ||
message: { | ||
from: userAddress, | ||
to: recipientAddress, | ||
value: amountBN.toString(10), | ||
validAfter: 0, | ||
validBefore: Math.floor(Date.now() / 1000) + 3600, // Valid for an hour | ||
nonce: Web3.utils.randomHex(32), | ||
}, | ||
}; | ||
|
||
const signature = await ethereum.request({ | ||
method: "eth_signTypedData_v4", | ||
params: [userAddress, JSON.stringify(data)], | ||
}); | ||
|
||
const v = "0x" + signature.slice(130,132); | ||
const r = signature.slice(0, 66); | ||
const s = "0x" + signature.slice(66, 130); | ||
``` | ||
|
||
## Rationale | ||
|
||
_WIP_ | ||
|
||
## 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`). | ||
|
||
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: | ||
|
||
1. Extract the user and deposit amount from the authorization | ||
2. Call `transferWithAuthorization` 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 | ||
|
||
**Example:** | ||
```solidity | ||
interface IDeFiToken { | ||
function deposit(uint256 amount) external returns (uint256); | ||
function transfer(address account, uint256 amount) | ||
external | ||
returns (bool); | ||
} | ||
contract DepositForwarder { | ||
bytes4 private constant _TRANSFER_WITH_AUTHORIZATION_SELECTOR = 0xe3ee160e; | ||
IDeFiToken private _parent; | ||
IERC20 private _token; | ||
constructor(IDeFiToken parent, IERC20 token) public { | ||
_parent = parent; | ||
_token = token; | ||
} | ||
function deposit(bytes calldata transferAuthorization) | ||
external | ||
nonReentrant | ||
returns (uint256) | ||
{ | ||
(address from, address to, uint256 amount) = abi.decode( | ||
transferAuthorization[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 | ||
) | ||
); | ||
require(success, "Failed to transfer to the forwarder"); | ||
require( | ||
_token.approve(address(_parent), amount), | ||
"Failed to set the allowance" | ||
); | ||
uint256 tokensMinted = _parent.deposit(amount); | ||
require( | ||
_parent.transfer(from, tokensMinted), | ||
"Failed to transfer the minted tokens" | ||
); | ||
uint256 remainder = _token.balanceOf(address(this); | ||
if (remainder > 0) { | ||
require( | ||
_token.transfer(from, remainder), | ||
"Failed to refund the remainder" | ||
); | ||
} | ||
return tokensMinted; | ||
} | ||
} | ||
``` | ||
|
||
## Test Cases | ||
|
||
_WIP_ | ||
|
||
## Implementation | ||
|
||
An example implementation can be found in [this repostiory](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009.sol). | ||
In addition to that, [this file](https://github.com/CoinbaseStablecoin/eip-3009/blob/master/contracts/lib/EIP3009Expanded.sol) shows an expanded form this spec, as implemented by the USDC smart contract. | ||
|
||
## Security Considerations | ||
|
||
Transactions with the same nonce may be submitted to the network but only the first transaction that’s confirmed by the network will be successful. Applications and services utilizing this should always wait for an adequate number of confirmations before treating the transaction as completed. | ||
|
||
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. | ||
|
||
## Copyright | ||
Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). |