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 16 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.
Jump to
Jump to file
Failed to load files.
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
2 changes: 2 additions & 0 deletions contracts/cryptography/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ This collection of libraries provides simple and safe ways to use different cryp
{{ECDSA}}

{{MerkleProof}}

{{EIP712}}
105 changes: 105 additions & 0 deletions contracts/drafts/EIP712.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

/**
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP 712] is a standard for hashing and signing of typed structured data.
*
* The encoding specified in the EIP is very generic, and such a generic implementation in Solidity is not feasible,
* thus this contract does not implement the encoding itself. Protocols need to implement the type-specific encoding
* they need in their contracts using a combination of `abi.encode` and `keccak256`.
*
* This contract implements the EIP 712 domain separator ({_domainSeparatorV4}) that is used as part of the encoding
* scheme, and the final step of the encoding to obtain the message digest that is then signed via ECDSA
* ({_hashTypedDataV4}).
*
* The implementation of the domain separator was designed to be as efficient as possible while still properly updating
* the chain id to protect against replay attacks on an eventual fork of the chain.
*
* NOTE: This contract implements the version of the encoding known as "v4", as implemented by the JSON RPC method
* https://docs.metamask.io/guide/signing-data.html[`eth_signTypedDataV4` in MetaMask].
*/
abstract contract EIP712 {
/* solhint-disable var-name-mixedcase */
// Cache the domain separator as an immutable value, but also store the chain id that it corresponds to, in order to
// invalidate the cached domain separator if the chain id changes.
bytes32 private immutable _CACHED_DOMAIN_SEPARATOR;
uint256 private immutable _CACHED_CHAIN_ID;

bytes32 private immutable _HASHED_NAME;
bytes32 private immutable _HASHED_VERSION;
bytes32 private immutable _TYPE_HASH;
/* solhint-enable var-name-mixedcase */

/**
* @dev Initializes the domain separator and parameter caches.
*
* The meaning of `name` and `version` is specified in
* https://eips.ethereum.org/EIPS/eip-712#definition-of-domainseparator[EIP 712]:
*
* - `name`: the user readable name of the signing domain, i.e. the name of the DApp or the protocol.
* - `version`: the current major version of the signing domain.
*
* NOTE: These parameters cannot be changed except through a xref:learn::upgrading-smart-contracts.adoc[smart
* contract upgrade].
*/
constructor(string memory name, string memory version) internal {
bytes32 hashedName = keccak256(bytes(name));
bytes32 hashedVersion = keccak256(bytes(version));
bytes32 typeHash = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
_HASHED_NAME = hashedName;
_HASHED_VERSION = hashedVersion;
_CACHED_CHAIN_ID = _getChainId();
_CACHED_DOMAIN_SEPARATOR = _buildDomainSeparator(typeHash, hashedName, hashedVersion);
_TYPE_HASH = typeHash;
}

/**
* @dev Returns the domain separator for the current chain.
*/
function _domainSeparatorV4() internal view returns (bytes32) {
if (_getChainId() == _CACHED_CHAIN_ID) {
return _CACHED_DOMAIN_SEPARATOR;
} else {
return _buildDomainSeparator(_TYPE_HASH, _HASHED_NAME, _HASHED_VERSION);
}
}

function _buildDomainSeparator(bytes32 typeHash, bytes32 name, bytes32 version) private view returns (bytes32) {
return keccak256(
abi.encode(
typeHash,
name,
version,
_getChainId(),
address(this)
)
);
}

/**
* @dev Given an already https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct[hashed struct], this
* function returns the hash of the fully encoded EIP712 message for this domain.
*
* This hash can be used together with {ECDSA-recover} to obtain the signer of a message. For example:
*
* ```solidity
* bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
* keccak256("Mail(address to,string contents)"),
* mailTo,
* keccak256(bytes(mailContents))
* )));
* address signer = ECDSA.recover(digest, signature);
* ```
*/
function _hashTypedDataV4(bytes32 structHash) internal view returns (bytes32) {
return keccak256(abi.encodePacked("\x19\x01", _domainSeparatorV4(), structHash));
}

function _getChainId() private pure returns (uint256 chainId) {
// solhint-disable-next-line no-inline-assembly
assembly {
chainId := chainid()
}
}
}
73 changes: 73 additions & 0 deletions contracts/drafts/ERC20Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.5 <0.8.0;

import "../token/ERC20/ERC20.sol";
import "./IERC2612Permit.sol";
import "../cryptography/ECDSA.sol";
import "../utils/Counters.sol";
import "./EIP712.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, EIP712 {
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)");

constructor() internal EIP712(name(), "1") {
}

/**
* @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 structHash = keccak256(
abi.encode(
_PERMIT_TYPEHASH,
owner,
spender,
amount,
_nonces[owner].current(),
deadline
)
);

bytes32 hash = _hashTypedDataV4(structHash);

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();
}

/**
* @dev See {IERC2612Permit-DOMAIN_SEPARATOR}.
*/
function DOMAIN_SEPARATOR() external view override returns (bytes32) {
return _domainSeparatorV4();
}
}
52 changes: 52 additions & 0 deletions contracts/drafts/IERC2612Permit.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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}).
spalladino marked this conversation as resolved.
Show resolved Hide resolved
*
* 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
frangio marked this conversation as resolved.
Show resolved Hide resolved
* prevents a signature from being used multiple times.
*/
function nonces(address owner) external view returns (uint256);

/**
* @dev Returns the domain separator used in the encoding of the signature for `permit`, as defined by {EIP712}.
*/
function DOMAIN_SEPARATOR() external view returns (bytes32);
}
9 changes: 9 additions & 0 deletions contracts/drafts/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
= Draft EIPS

This directory contains implementations of EIPs that are still in Draft status.

Due to their nature as drafts, the details of these contracts may change and we cannot guarantee their xref:ROOT:releases-stability.adoc[stability]. Minor releases of OpenZeppelin Contracts may contain breaking changes for the contracts in this directory, which will be duly announced in the https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/CHANGELOG.md[changelog]. The EIPs included here are used by projects in production and this may make them less likely to change significantly.

== Cryptography

{{EIP712}}
31 changes: 31 additions & 0 deletions contracts/mocks/EIP712External.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT

pragma solidity >=0.6.0 <0.8.0;

import "../drafts/EIP712.sol";
import "../cryptography/ECDSA.sol";

contract EIP712External is EIP712 {
constructor(string memory name, string memory version) public EIP712(name, version) {}

function domainSeparator() external view returns (bytes32) {
return _domainSeparatorV4();
}

function verify(bytes memory signature, address signer, address mailTo, string memory mailContents) external view {
bytes32 digest = _hashTypedDataV4(keccak256(abi.encode(
keccak256("Mail(address to,string contents)"),
mailTo,
keccak256(bytes(mailContents))
)));
address recoveredSigner = ECDSA.recover(digest, signature);
require(recoveredSigner == signer);
}

function getChainId() external pure returns (uint256 chainId) {
// solhint-disable-next-line no-inline-assembly
assembly {
chainId := chainid()
}
}
}
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 "../drafts/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);
}
}
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}}