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

Update EIP-2612: Move to Last Call #5506

Merged
merged 19 commits into from Sep 22, 2022
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 22 additions & 23 deletions EIPS/eip-2612.md
@@ -1,41 +1,42 @@
---
eip: 2612
title: permit – 712-signed approvals
frangio marked this conversation as resolved.
Show resolved Hide resolved
description: ERC-20 approvals via secp256k1 signatures
description: EIP-20 approvals via secp256k1 signatures
author: Martin Lundfall (@Mrchico)
discussions-to: https://github.com/ethereum/EIPs/issues/2613
Copy link
Member

Choose a reason for hiding this comment

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

Would you mind creating a new thread on Ethereum magicians?

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 explicitly asked about this at the beginning of this process and I was told it was okay to keep this link. Please do not put unnecessary hurdles on this PR that has been open for over a month already.

Copy link
Member

@Pandapip1 Pandapip1 Sep 22, 2022

Choose a reason for hiding this comment

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

This isn't a hurdle. This was truly a suggestion that would speed up the process later on (hence why I used a comment instead of a request for changes) but one that is optional.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, sorry that I misunderstood.

status: Review
status: Last Call
last-call-deadline: 2022-09-13
type: Standards Track
category: ERC
created: 2020-04-13
requires: 20, 712
---

## Abstract
Arguably one of the main reasons for the success of [ERC-20](./eip-20.md) tokens lies in the interplay between `approve` and `transferFrom`, which allows for tokens to not only be transferred between externally owned accounts (EOA), but to be used in other contracts under application specific conditions by abstracting away `msg.sender` as the defining mechanism for token access control.
Arguably one of the main reasons for the success of [EIP-20](./eip-20.md) tokens lies in the interplay between `approve` and `transferFrom`, which allows for tokens to not only be transferred between externally owned accounts (EOA), but to be used in other contracts under application specific conditions by abstracting away `msg.sender` as the defining mechanism for token access control.

However, a limiting factor in this design stems from the fact that the ERC-20 `approve` function itself is defined in terms of `msg.sender`. This means that user's _initial action_ involving ERC-20 tokens must be performed by an EOA (_but see Note below_). If the user needs to interact with a smart contract, then they need to make 2 transactions (`approve` and the smart contract call which will internally call `transferFrom`). Even in the simple use case of paying another person, they need to hold ETH to pay for transaction gas costs.
However, a limiting factor in this design stems from the fact that the EIP-20 `approve` function itself is defined in terms of `msg.sender`. This means that user's _initial action_ involving EIP-20 tokens must be performed by an EOA (_but see Note below_). If the user needs to interact with a smart contract, then they need to make 2 transactions (`approve` and the smart contract call which will internally call `transferFrom`). Even in the simple use case of paying another person, they need to hold ETH to pay for transaction gas costs.

This ERC extends the ERC-20 standard with a new function `permit`, which allows users to modify the `allowance` mapping using a signed message, instead of through `msg.sender`.
This ERC extends the EIP-20 standard with a new function `permit`, which allows users to modify the `allowance` mapping using a signed message, instead of through `msg.sender`.

For an improved user experience, the signed data is structured following [ERC-712](./eip-712.md), which already has wide spread adoption in major RPC providers.
For an improved user experience, the signed data is structured following [EIP-712](./eip-712.md), which already has wide spread adoption in major RPC providers.

**_Note:_** ERC-20 must be performed by an EOA unless the address owning the token is actually a contract wallet. Although contract wallets solves many of the same problems that motivates this EIP, they are currently only scarcely adopted in the ecosystem. Contract wallets suffer from a UX problem -- since they separate the EOA `owner` of the contract wallet from the contract wallet itself (which is meant to carry out actions on the `owner`s behalf and holds all of their funds), user interfaces need to be specifically designed to support them. The `permit` pattern reaps many of the same benefits while requiring little to no change in user interfaces.
**_Note:_** EIP-20 must be performed by an EOA unless the address owning the token is actually a contract wallet. Although contract wallets solves many of the same problems that motivates this EIP, they are currently only scarcely adopted in the ecosystem. Contract wallets suffer from a UX problem -- since they separate the EOA `owner` of the contract wallet from the contract wallet itself (which is meant to carry out actions on the `owner`s behalf and holds all of their funds), user interfaces need to be specifically designed to support them. The `permit` pattern reaps many of the same benefits while requiring little to no change in user interfaces.

## Motivation
While ERC-20 tokens have become ubiquitous in the Ethereum ecosystem, their status remains that of second class tokens from the perspective of the protocol. The ability for users to interact with Ethereum without holding any ETH has been a [long outstanding goal](./eip-101.md) and the [subject](./eip-1077.md) [of](./eip-777.md) [many](https://github.com/ethereum/EIPs/issues/1776) [EIPs](./eip-1271.md).
While EIP-20 tokens have become ubiquitous in the Ethereum ecosystem, their status remains that of second class tokens from the perspective of the protocol. The ability for users to interact with Ethereum without holding any ETH has been a long outstanding goal and the subject of many EIPs.

So far, many of these proposals have seen very little adoption, and the ones that have been adopted (such as [ERC-777](./eip-777.md)), introduce a lot of additional functionality, causing [unexpected behavior in mainstream contracts](https://medium.com/consensys-diligence/uniswap-audit-b90335ac007).
So far, many of these proposals have seen very little adoption, and the ones that have been adopted (such as [EIP-777](./eip-777.md)), introduce a lot of additional functionality, causing unexpected behavior in mainstream contracts.

This ERC proposes an alternative solution which is designed to be as minimal as possible and to only address _one problem_: the lack of abstraction in the ERC-20 `approve` method.
This ERC proposes an alternative solution which is designed to be as minimal as possible and to only address _one problem_: the lack of abstraction in the EIP-20 `approve` method.

While it may be tempting to introduce `*_by_signature` counterparts for every ERC-20 function, they are intentionally left out of this ERC-20 for two reasons:
While it may be tempting to introduce `*_by_signature` counterparts for every EIP-20 function, they are intentionally left out of this EIP-20 for two reasons:

- the desired specifics of such functions, such as decision regarding fees for `transfer_by_signature`, possible batching algorithms, varies depending on the use case, and,
- they can be implemented using a combination of `permit` and additional helper contracts without loss of generality.

## Specification
Three new functions are added to the ERC-20 ABI:
Three new functions are added to the EIP-20 ABI:
```sol
function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external
function nonces(address owner) external view returns (uint)
Expand Down Expand Up @@ -85,7 +86,7 @@ DOMAIN_SEPARATOR = keccak256(
));
```

In other words, the message is the ERC-712 typed structure:
In other words, the message is the EIP-712 typed structure:

```js
{
Expand Down Expand Up @@ -150,16 +151,13 @@ Note that nowhere in this definition we refer to `msg.sender`. The caller of the


## Rationale
The `permit` function is sufficient for enabling any operation involving ERC-20 tokens to be paid for using the token itself, rather than using ETH.
An example of a contract which enables gasless token transactions can be found [here](https://github.com/dapphub/ds-dach).

It avoids any calls to unknown code.
The `permit` function is sufficient for enabling any operation involving EIP-20 tokens to be paid for using the token itself, rather than using ETH.

The `nonces` mapping is given for replay protection.

A common use case of `permit` has a relayer submit a `Permit` on behalf of the `owner`. In this scenario, the relaying party is essentially given a free option to submit or withhold the `Permit`. If this is a cause of concern, the `owner` can limit the time a `Permit` is valid for by setting `deadline` to a value in the near future. The `deadline` argument can be set to `uint(-1)` to create `Permit`s that effectively never expire.

ERC-712 typed messages are included because of its wide spread adoption in many wallet providers.
EIP-712 typed messages are included because of its wide spread adoption in many wallet providers.


## Backwards Compatibility
Expand All @@ -169,18 +167,19 @@ Its implementation differs slightly from the presentation here in that:
- instead of taking a `value` argument, it takes a bool `allowed`, setting approval to 0 or `uint(-1)`.
- the `deadline` argument is instead called `expiry`. This is not just a syntactic change, as it effects the contents of the signed message.

There is also an implementation in the token [`Stake`](https://etherscan.io/address/0x0Ae055097C6d159879521C384F1D2123D1f195e6#code) with the same ABI as `dai` but with different semantics: it lets users issue "expiring approvals", that only allow `transferFrom` to occur while `expiry >= block.timestamp`.
There is also an implementation in the token `Stake` (Ethereum address `0x0Ae055097C6d159879521C384F1D2123D1f195e6`) with the same ABI as `dai` but with different semantics: it lets users issue "expiring approvals", that only allow `transferFrom` to occur while `expiry >= block.timestamp`.

The specification presented here is in line with the implementation in [Uniswap-v2](https://github.com/uniswap/uniswap-v2-core).
The specification presented here is in line with the implementation in Uniswap V2.

The requirement to revert if the permit is invalid was added when the EIP was already widely deployed, but at the moment it was consistent with all found implementations.

## Test Cases

Some basic tests can be found here https://github.com/Uniswap/uniswap-v2-core/blob/master/test/UniswapV2ERC20.spec.ts.
Some basic tests can be found on [`Uniswap V2`](../assets/eip-2612/UniswapV2ERC20.spec.ts).
frangio marked this conversation as resolved.
Show resolved Hide resolved

## Reference Implementation
[UniswapV2ERC20.sol](https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol)

[`UniswapV2ERC20.sol`](../assets/eip-2612/UniswapV2ERC20.sol)
frangio marked this conversation as resolved.
Show resolved Hide resolved

## Security Considerations

Expand All @@ -190,7 +189,7 @@ Since the ecrecover precompile fails silently and just returns the zero address

Signed `Permit` messages are censorable. The relaying party can always choose to not submit the `Permit` after having received it, withholding the option to submit it. The `deadline` parameter is one mitigation to this. If the signing party holds ETH they can also just submit the `Permit` themselves, which can render previously signed `Permit`s invalid.

The standard [ERC-20 race condition for approvals](https://swcregistry.io/docs/SWC-114) applies to `permit` as well.
The standard EIP-20 race condition for approvals (SWC-114) applies to `permit` as well.

If the `DOMAIN_SEPARATOR` contains the `chainId` and is defined at contract deployment instead of reconstructed for every signature, there is a risk of possible replay attacks between chains in the event of a future chain split.

Expand Down
94 changes: 94 additions & 0 deletions assets/eip-2612/UniswapV2ERC20.sol
@@ -0,0 +1,94 @@
pragma solidity =0.5.16;

import './interfaces/IUniswapV2ERC20.sol';
import './libraries/SafeMath.sol';

contract UniswapV2ERC20 is IUniswapV2ERC20 {
using SafeMath for uint;

string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;

bytes32 public DOMAIN_SEPARATOR;
// keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");
bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9;
mapping(address => uint) public nonces;

event Approval(address indexed owner, address indexed spender, uint value);
event Transfer(address indexed from, address indexed to, uint value);

constructor() public {
uint chainId;
assembly {
chainId := chainid
}
DOMAIN_SEPARATOR = keccak256(
abi.encode(
keccak256('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)'),
keccak256(bytes(name)),
keccak256(bytes('1')),
chainId,
address(this)
)
);
}

function _mint(address to, uint value) internal {
totalSupply = totalSupply.add(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(address(0), to, value);
}

function _burn(address from, uint value) internal {
balanceOf[from] = balanceOf[from].sub(value);
totalSupply = totalSupply.sub(value);
emit Transfer(from, address(0), value);
}

function _approve(address owner, address spender, uint value) private {
allowance[owner][spender] = value;
emit Approval(owner, spender, value);
}

function _transfer(address from, address to, uint value) private {
balanceOf[from] = balanceOf[from].sub(value);
balanceOf[to] = balanceOf[to].add(value);
emit Transfer(from, to, value);
}

function approve(address spender, uint value) external returns (bool) {
_approve(msg.sender, spender, value);
return true;
}

function transfer(address to, uint value) external returns (bool) {
_transfer(msg.sender, to, value);
return true;
}

function transferFrom(address from, address to, uint value) external returns (bool) {
if (allowance[from][msg.sender] != uint(-1)) {
allowance[from][msg.sender] = allowance[from][msg.sender].sub(value);
}
_transfer(from, to, value);
return true;
}

function permit(address owner, address spender, uint value, uint deadline, uint8 v, bytes32 r, bytes32 s) external {
require(deadline >= block.timestamp, 'UniswapV2: EXPIRED');
bytes32 digest = keccak256(
abi.encodePacked(
'\x19\x01',
DOMAIN_SEPARATOR,
keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, nonces[owner]++, deadline))
)
);
address recoveredAddress = ecrecover(digest, v, r, s);
require(recoveredAddress != address(0) && recoveredAddress == owner, 'UniswapV2: INVALID_SIGNATURE');
_approve(owner, spender, value);
}
}
116 changes: 116 additions & 0 deletions assets/eip-2612/UniswapV2ERC20.spec.ts
@@ -0,0 +1,116 @@
import chai, { expect } from 'chai'
import { Contract } from 'ethers'
import { MaxUint256 } from 'ethers/constants'
import { bigNumberify, hexlify, keccak256, defaultAbiCoder, toUtf8Bytes } from 'ethers/utils'
import { solidity, MockProvider, deployContract } from 'ethereum-waffle'
import { ecsign } from 'ethereumjs-util'

import { expandTo18Decimals, getApprovalDigest } from './shared/utilities'

import ERC20 from '../build/ERC20.json'

chai.use(solidity)

const TOTAL_SUPPLY = expandTo18Decimals(10000)
const TEST_AMOUNT = expandTo18Decimals(10)

describe('UniswapV2ERC20', () => {
const provider = new MockProvider({
hardfork: 'istanbul',
mnemonic: 'horn horn horn horn horn horn horn horn horn horn horn horn',
gasLimit: 9999999
})
const [wallet, other] = provider.getWallets()

let token: Contract
beforeEach(async () => {
token = await deployContract(wallet, ERC20, [TOTAL_SUPPLY])
})

it('name, symbol, decimals, totalSupply, balanceOf, DOMAIN_SEPARATOR, PERMIT_TYPEHASH', async () => {
const name = await token.name()
expect(name).to.eq('Uniswap V2')
expect(await token.symbol()).to.eq('UNI-V2')
expect(await token.decimals()).to.eq(18)
expect(await token.totalSupply()).to.eq(TOTAL_SUPPLY)
expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY)
expect(await token.DOMAIN_SEPARATOR()).to.eq(
keccak256(
defaultAbiCoder.encode(
['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'],
[
keccak256(
toUtf8Bytes('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')
),
keccak256(toUtf8Bytes(name)),
keccak256(toUtf8Bytes('1')),
1,
token.address
]
)
)
)
expect(await token.PERMIT_TYPEHASH()).to.eq(
keccak256(toUtf8Bytes('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)'))
)
})

it('approve', async () => {
await expect(token.approve(other.address, TEST_AMOUNT))
.to.emit(token, 'Approval')
.withArgs(wallet.address, other.address, TEST_AMOUNT)
expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT)
})

it('transfer', async () => {
await expect(token.transfer(other.address, TEST_AMOUNT))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, other.address, TEST_AMOUNT)
expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT))
expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT)
})

it('transfer:fail', async () => {
await expect(token.transfer(other.address, TOTAL_SUPPLY.add(1))).to.be.reverted // ds-math-sub-underflow
await expect(token.connect(other).transfer(wallet.address, 1)).to.be.reverted // ds-math-sub-underflow
})

it('transferFrom', async () => {
await token.approve(other.address, TEST_AMOUNT)
await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, other.address, TEST_AMOUNT)
expect(await token.allowance(wallet.address, other.address)).to.eq(0)
expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT))
expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT)
})

it('transferFrom:max', async () => {
await token.approve(other.address, MaxUint256)
await expect(token.connect(other).transferFrom(wallet.address, other.address, TEST_AMOUNT))
.to.emit(token, 'Transfer')
.withArgs(wallet.address, other.address, TEST_AMOUNT)
expect(await token.allowance(wallet.address, other.address)).to.eq(MaxUint256)
expect(await token.balanceOf(wallet.address)).to.eq(TOTAL_SUPPLY.sub(TEST_AMOUNT))
expect(await token.balanceOf(other.address)).to.eq(TEST_AMOUNT)
})

it('permit', async () => {
const nonce = await token.nonces(wallet.address)
const deadline = MaxUint256
const digest = await getApprovalDigest(
token,
{ owner: wallet.address, spender: other.address, value: TEST_AMOUNT },
nonce,
deadline
)

const { v, r, s } = ecsign(Buffer.from(digest.slice(2), 'hex'), Buffer.from(wallet.privateKey.slice(2), 'hex'))

await expect(token.permit(wallet.address, other.address, TEST_AMOUNT, deadline, v, hexlify(r), hexlify(s)))
.to.emit(token, 'Approval')
.withArgs(wallet.address, other.address, TEST_AMOUNT)
expect(await token.allowance(wallet.address, other.address)).to.eq(TEST_AMOUNT)
expect(await token.nonces(wallet.address)).to.eq(bigNumberify(1))
})
})