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

Add ERC20 Permit (EIP-2612) #2237

Merged
merged 38 commits into from
Dec 11, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
19a413a
Initial storage-based implementation
nventuro May 15, 2020
48c41df
Improve gas efficiency and docs
nventuro May 15, 2020
d1e0f4b
Initial sketch for tests
nventuro May 15, 2020
cf718ee
Fix encoding before hash
nventuro May 26, 2020
53516bc
Implement nonce using Counter
nventuro Jun 5, 2020
be37397
Merge branch 'master' into erc20-permit
frangio Nov 20, 2020
743fe9a
adjust pragma and add license
frangio Nov 20, 2020
0c02fdc
adapt test to buidler
frangio Nov 20, 2020
255e11a
disable eslint in wip file
frangio Nov 20, 2020
d5037d2
add solhint exceptions
frangio Nov 20, 2020
b11d7ab
use a cheaper strategy for domain separator caching
frangio Nov 27, 2020
58c7098
add DOMAIN_SEPARATOR function
frangio Dec 1, 2020
1bd9922
Merge branch 'master' into erc20-permit
frangio Dec 1, 2020
cb00f8a
add eip712 from #2418
frangio Dec 1, 2020
c5d01e9
move to drafts directory
frangio Dec 1, 2020
aacea0c
use EIP712 contract
frangio Dec 1, 2020
b57f655
fix test
frangio Dec 2, 2020
0560ea8
emit contract in api/drafts page
frangio Dec 2, 2020
4552f52
fix api/drafts page title
frangio Dec 2, 2020
9a916f7
add constructor documentation
frangio Dec 2, 2020
91e98a4
lint
frangio Dec 2, 2020
e331820
extract eip712 helpers
frangio Dec 4, 2020
ade3af2
test DOMAIN_SEPARATOR function
frangio Dec 4, 2020
9a661c9
test permit postconditions
frangio Dec 4, 2020
f4b7f93
use different account for nonce test
frangio Dec 4, 2020
8f7f47c
add test for rejected transaction
frangio Dec 4, 2020
b7b5abd
test expired permit
frangio Dec 4, 2020
24e4582
remove note about domain separator and chain id
frangio Dec 4, 2020
737fa3b
Merge remote-tracking branch 'upstream/master' into erc20-permit
frangio Dec 4, 2020
f410734
ensure deadline is before next block timestamp
frangio Dec 4, 2020
9d60a71
Update contracts/token/ERC20/README.adoc
frangio Dec 9, 2020
3beaf01
add test that reused signature is rejected
frangio Dec 9, 2020
837586d
use valid signature when testing expired deadline
frangio Dec 9, 2020
add4067
Merge remote-tracking branch 'upstream/master' into erc20-permit
frangio Dec 9, 2020
f26f0e3
rename IERC2612Permit to IERC20Permit
frangio Dec 9, 2020
acd82cd
review documentation
frangio Dec 9, 2020
4444d3b
Merge branch 'master' into erc20-permit
frangio Dec 11, 2020
71c9e3d
add changelog entry
frangio Dec 11, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions contracts/cryptography/ECDSA.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@ library ECDSA {
v := byte(0, mload(add(signature, 0x60)))
}

return recover(hash, v, r, s);
}

/**
* @dev Overload of {ECDSA-recover-bytes32-bytes-} that receives the `v`,
* `r` and `s` signature fields separately.
*/
function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) {
// EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature
// unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines
// the valid range for s in (281): 0 < s < secp256k1n ÷ 2 + 1, and for v in (282): v ∈ {27, 28}. Most
Expand Down
16 changes: 16 additions & 0 deletions contracts/mocks/ERC20PermitMock.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "../token/ERC20/ERC20Permit.sol";

contract ERC20PermitMock is ERC20Permit {
constructor (
string memory name,
string memory symbol,
address initialAccount,
uint256 initialBalance
) public payable ERC20(name, symbol) {
_mint(initialAccount, initialBalance);
}
}
114 changes: 114 additions & 0 deletions contracts/token/ERC20/ERC20Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.5 <0.8.0;

import "./ERC20.sol";
import "./IERC2612Permit.sol";
import "../../cryptography/ECDSA.sol";
import "../../utils/Counters.sol";

/**
* @dev Extension of {ERC20} that allows token holders to use their tokens
* without sending any transactions by setting {IERC20-allowance} with a
* signature using the {permit} method, and then spend them via
* {IERC20-transferFrom}.
*
* The {permit} signature mechanism conforms to the {IERC2612Permit} interface.
*/
abstract contract ERC20Permit is ERC20, IERC2612Permit {
using Counters for Counters.Counter;

mapping (address => Counters.Counter) private _nonces;

// solhint-disable-next-line var-name-mixedcase
bytes32 private immutable _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

// Mapping of ChainID to domain separators. This is a very gas efficient way
// to not recalculate the domain separator on every call, while still
// automatically detecting ChainID changes.
mapping (uint256 => bytes32) private _domainSeparators;
frangio marked this conversation as resolved.
Show resolved Hide resolved

constructor() internal {
_updateDomainSeparator();
}

/**
* @dev See {IERC2612Permit-permit}.
*
* If https://eips.ethereum.org/EIPS/eip-1344[ChainID] ever changes, the
* EIP712 Domain Separator is automatically recalculated.
*/
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public virtual override {
// solhint-disable-next-line not-rely-on-time
require(block.timestamp <= deadline, "ERC20Permit: expired deadline");

bytes32 hashStruct = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
amount,
_nonces[owner].current(),
deadline
)
);

bytes32 hash = keccak256(
abi.encodePacked(
uint16(0x1901),
_domainSeparator(),
hashStruct
)
);

address signer = ECDSA.recover(hash, v, r, s);
require(signer == owner, "ERC20Permit: invalid signature");

_nonces[owner].increment();
_approve(owner, spender, amount);
}

/**
* @dev See {IERC2612Permit-nonces}.
*/
function nonces(address owner) public view override returns (uint256) {
return _nonces[owner].current();
}

function _updateDomainSeparator() private returns (bytes32) {
uint256 chainID = _chainID();

bytes32 newDomainSeparator = keccak256(
abi.encode(
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
frangio marked this conversation as resolved.
Show resolved Hide resolved
keccak256(bytes(name())),
keccak256(bytes("1")), // Version
Copy link
Contributor

Choose a reason for hiding this comment

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

It seems we should use pure internal method version() or permitVersion() for this.

Copy link
Contributor Author

@nventuro nventuro Jun 30, 2020

Choose a reason for hiding this comment

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

Are you suggesting an internal setter for users to set their own version field? I'm actually not sure what all the use cases for version are, if you have insights on this that'd be extremely helpful.

That said we do need to provide a way for either the version or domain separator to be queried, as noted here.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe version makes sense when behaviour changes without address change (upgradablity?). So I mean version method could be overrided by user, or maybe should be even abstract.

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 see. It'd be great to have some examples to show in the docs.

Regarding implementation, we could have a setter and a getter, where the setter also calls _updateDomainSeparator and not increase gas costs of permit that way.

Copy link
Contributor

Choose a reason for hiding this comment

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

@k06a In this case though the semantics of the signature are defined by the EIP, so I don't see a reason for version to change.

chainID,
address(this)
)
);

_domainSeparators[chainID] = newDomainSeparator;

return newDomainSeparator;
}

// Returns the domain separator, updating it if chainID changes
function _domainSeparator() private returns (bytes32) {
bytes32 domainSeparator = _domainSeparators[_chainID()];
if (domainSeparator != 0x00) {
return domainSeparator;
} else {
return _updateDomainSeparator();
}
}

function _chainID() private pure returns (uint256) {
uint256 chainID;
// solhint-disable-next-line no-inline-assembly
assembly {
chainID := chainid()
}
return chainID;
}
}
47 changes: 47 additions & 0 deletions contracts/token/ERC20/IERC2612Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

/**
* @dev Interface of the ERC2612 standard as defined in the EIP.
*
* Adds the {permit} method, which can be used to change one's
* {IERC20-allowance} without having to send a transaction, by signing a
* message. This allows users to spend tokens without having to hold Ether.
*
* See https://eips.ethereum.org/EIPS/eip-2612.
*/
interface IERC2612Permit {
/**
* @dev Sets `amount` as the allowance of `spender` over `owner`'s tokens,
* given `owner`'s signed approval.
*
* IMPORTANT: The same issues {IERC20-approve} has related to transaction
* ordering also apply here.
*
* Emits an {Approval} event.
*
* Requirements:
*
* - `owner` cannot be the zero address.
* - `spender` cannot be the zero address.
* - `deadline` must be a timestamp in the future.
* - `v`, `r` and `s` must be a valid `secp256k1` signature from `owner`
* over the EIP712-formatted function arguments.
* - the signature must use ``owner``'s current nonce (see {nonces}).
*
* For more information on the signature format, see the
* https://eips.ethereum.org/EIPS/eip-2612#specification[relevant EIP
* section].
*/
function permit(address owner, address spender, uint256 amount, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external;

/**
* @dev Returns the current ERC2612 nonce for `owner`. This value must be
* included whenever a signature is generated for {permit}.
*
* Every successful call to {permit} increases ``owner``'s nonce by one. This
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);
}
8 changes: 8 additions & 0 deletions contracts/token/ERC20/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ There a few core contracts that implement the behavior specified in the EIP:
Additionally there are multiple custom extensions, including:

* designation of addresses that can pause token transfers for all users ({ERC20Pausable}).
* usage of tokens without sending any transactions ({ERC20Permit}).
frangio marked this conversation as resolved.
Show resolved Hide resolved
* efficient storage of past token balances to be later queried at any point in time ({ERC20Snapshot}).
* destruction of own tokens ({ERC20Burnable}).
* enforcement of a cap to the total supply when minting tokens ({ERC20Capped}).
Expand All @@ -36,14 +37,21 @@ NOTE: This core set of contracts is designed to be unopinionated, allowing devel

{{ERC20Snapshot}}

{{ERC20Permit}}

{{ERC20Pausable}}

{{ERC20Burnable}}

{{ERC20Capped}}

== Standard Extensions

{{IERC2612Permit}}

== Utilities

{{SafeERC20}}

{{TokenTimelock}}

55 changes: 55 additions & 0 deletions test/token/ERC20/ERC20Permit.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* eslint-disable */

const { BN, constants, expectEvent, expectRevert, time } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');
const { MAX_UINT256, ZERO_ADDRESS } = constants;

const { keccakFromString, bufferToHex } = require('ethereumjs-util');

const ERC20PermitMock = artifacts.require('ERC20PermitMock');

contract('ERC20Permit', function (accounts) {
const [ initialHolder, spender, recipient, other ] = accounts;

const name = 'My Token';
const symbol = 'MTKN';

const initialSupply = new BN(100);

beforeEach(async function () {
this.token = await ERC20PermitMock.new(name, symbol, initialHolder, initialSupply);
});

it('initial nonce is 0', async function () {
expect(await this.token.nonces(spender)).to.be.bignumber.equal('0');
});

it('permits', async function () {
const amount = new BN(42);

console.log(EIP712DomainSeparator(await this.token.name(), '1', await web3.eth.net.getId(), this.token.address));

const receipt = await this.token.permit(initialHolder, spender, amount, MAX_UINT256, 0, '0x00000000000000000000000000000000', '0x00000000000000000000000000000000');
});
});

function EIP712DomainSeparator(name, version, chainID, address) {
return bufferToHex(keccakFromString(
bufferToHex(keccakFromString('EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)')) +
bufferToHex(keccakFromString(name)) +
bufferToHex(keccakFromString(version)) +
chainID.toString() +
address
));
}

function ERC2612StructHash(owner, spender, value, nonce, deadline) {
return bufferToHex(keccakFromString(
bufferToHex(keccakFromString('Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)')) +
owner +
spender +
value +
nonce +
deadline
));
}