-
Notifications
You must be signed in to change notification settings - Fork 12.4k
Add ERC20TransferAuthorization following ERC-3009 #6354
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
base: master
Are you sure you want to change the base?
Changes from 9 commits
481470f
d66d1e8
7ed2271
3f448e6
c83f972
41131a1
7a72a6b
5dd8318
6931616
f3f1ffb
a3db8c7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'openzeppelin-solidity': minor | ||
| --- | ||
|
|
||
| `ERC20TransferAuthorization`: Add an ERC-20 extension implementing ERC-3009's transfer with authorization using {NoncesKeyed} for parallel nonces. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| // SPDX-License-Identifier: MIT | ||
|
|
||
| pragma solidity >=0.4.16; | ||
|
|
||
| /** | ||
| * @dev Interface of the ERC-3009 standard as defined in https://eips.ethereum.org/EIPS/eip-3009[ERC-3009]. | ||
| */ | ||
| interface IERC3009 { | ||
| /// @dev Emitted when an authorization is used. | ||
| event AuthorizationUsed(address indexed authorizer, bytes32 indexed nonce); | ||
|
|
||
| /** | ||
| * @dev Returns the state of an authorization. | ||
| * | ||
| * Nonces are randomly generated 32-byte values unique to the authorizer's address. | ||
| */ | ||
| function authorizationState(address authorizer, bytes32 nonce) external view returns (bool); | ||
|
Comment on lines
+12
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we be a bit more specific in the comments here about the boolean returned value? i.e, what does it mean the
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Without a clear explanation of the returned boolean, |
||
|
|
||
| /** | ||
| * @dev Executes a transfer with a signed authorization. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * * `validAfter` must be less than the current block timestamp. | ||
| * * `validBefore` must be greater than the current block timestamp. | ||
| * * `nonce` must not have been used by the `from` account. | ||
| * * the signature must be valid for the authorization. | ||
| */ | ||
| function transferWithAuthorization( | ||
| address from, | ||
| address to, | ||
| uint256 value, | ||
| uint256 validAfter, | ||
| uint256 validBefore, | ||
| bytes32 nonce, | ||
| uint8 v, | ||
| bytes32 r, | ||
| bytes32 s | ||
| ) external; | ||
|
|
||
| /** | ||
| * @dev Receives a transfer with a signed authorization from the payer. | ||
| * | ||
| * Includes an additional check to ensure that the payee's address (`to`) matches the caller | ||
| * to prevent front-running attacks. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * * `to` must be the caller of this function. | ||
| * * `validAfter` must be less than the current block timestamp. | ||
| * * `validBefore` must be greater than the current block timestamp. | ||
| * * `nonce` must not have been used by the `from` account. | ||
| * * the signature must be valid for the authorization. | ||
| */ | ||
| function receiveWithAuthorization( | ||
| address from, | ||
| address to, | ||
| uint256 value, | ||
| uint256 validAfter, | ||
| uint256 validBefore, | ||
| bytes32 nonce, | ||
| uint8 v, | ||
| bytes32 r, | ||
| bytes32 s | ||
| ) external; | ||
| } | ||
|
|
||
| /** | ||
| * @dev Extension of {IERC3009} that adds the ability to cancel authorizations. | ||
| */ | ||
| interface IERC3009Cancel { | ||
| /// @dev Emitted when an authorization is canceled. | ||
| event AuthorizationCanceled(address indexed authorizer, bytes32 indexed nonce); | ||
|
|
||
| /** | ||
| * @dev Cancels an authorization. | ||
| * | ||
| * Requirements: | ||
| * | ||
| * * `nonce` must not have been used by the `authorizer` account. | ||
| * * the signature must be valid for the cancellation. | ||
| */ | ||
| function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) external; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,212 @@ | ||||||||
| // SPDX-License-Identifier: MIT | ||||||||
| pragma solidity ^0.8.26; | ||||||||
|
|
||||||||
| import {ERC20} from "../ERC20.sol"; | ||||||||
| import {EIP712} from "../../../utils/cryptography/EIP712.sol"; | ||||||||
| import {SignatureChecker} from "../../../utils/cryptography/SignatureChecker.sol"; | ||||||||
| import {ECDSA} from "../../../utils/cryptography/ECDSA.sol"; | ||||||||
| import {IERC3009, IERC3009Cancel} from "../../../interfaces/draft-IERC3009.sol"; | ||||||||
| import {NoncesKeyed} from "../../../utils/NoncesKeyed.sol"; | ||||||||
|
|
||||||||
| /** | ||||||||
| * @dev Implementation of the ERC-3009 Transfer With Authorization extension allowing | ||||||||
| * transfers to be made via signatures, as defined in https://eips.ethereum.org/EIPS/eip-3009[ERC-3009]. | ||||||||
| * | ||||||||
| * Adds the {transferWithAuthorization} and {receiveWithAuthorization} methods, which | ||||||||
| * can be used to change an account's ERC-20 balance by presenting a message signed | ||||||||
| * by the account. By not relying on {IERC20-approve} and {IERC20-transferFrom}, the | ||||||||
| * token holder account doesn't need to send a transaction, and thus is not required | ||||||||
| * to hold native currency (e.g. ETH) at all. | ||||||||
| * | ||||||||
| * NOTE: This extension uses keyed sequential nonces following the | ||||||||
| * https://eips.ethereum.org/EIPS/eip-4337#semi-abstracted-nonce-support[ERC-4337 semi-abstracted nonce system]. | ||||||||
| * The {bytes32} nonce field is interpreted as a 192-bit key packed with a 64-bit sequence. Nonces with | ||||||||
| * different keys are independent and can be submitted in parallel without ordering constraints, while nonces | ||||||||
| * sharing the same key must be used sequentially. This is unlike {ERC20Permit} which uses a single global | ||||||||
| * sequential nonce. | ||||||||
| */ | ||||||||
| abstract contract ERC20TransferAuthorization is ERC20, EIP712, NoncesKeyed, IERC3009, IERC3009Cancel { | ||||||||
| /// @dev The signature is invalid | ||||||||
| error ERC3009InvalidSignature(); | ||||||||
|
|
||||||||
| /// @dev The authorization is not valid at the given time | ||||||||
| error ERC3009InvalidAuthorizationTime(uint256 validAfter, uint256 validBefore); | ||||||||
|
|
||||||||
| bytes32 private constant TRANSFER_WITH_AUTHORIZATION_TYPEHASH = | ||||||||
| keccak256( | ||||||||
| "TransferWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" | ||||||||
| ); | ||||||||
| bytes32 private constant RECEIVE_WITH_AUTHORIZATION_TYPEHASH = | ||||||||
| keccak256( | ||||||||
| "ReceiveWithAuthorization(address from,address to,uint256 value,uint256 validAfter,uint256 validBefore,bytes32 nonce)" | ||||||||
| ); | ||||||||
| bytes32 private constant CANCEL_AUTHORIZATION_TYPEHASH = | ||||||||
| keccak256("CancelAuthorization(address authorizer,bytes32 nonce)"); | ||||||||
|
|
||||||||
| /** | ||||||||
| * @dev Initializes the {EIP712} domain separator using the `name` parameter, and setting `version` to `"1"`. | ||||||||
| * | ||||||||
| * It's a good idea to use the same `name` that is defined as the ERC-20 token name. | ||||||||
| */ | ||||||||
| constructor(string memory name) EIP712(name, "1") {} | ||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we ever do
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed, it would cause
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Solutions I see:
|
||||||||
|
|
||||||||
| /** | ||||||||
| * @dev See {IERC3009-authorizationState}. | ||||||||
| * | ||||||||
| * NOTE: Returning `false` does not guarantee that the authorization is currently executable. | ||||||||
| * With keyed sequential nonces, a nonce may be blocked by a predecessor in the same key's sequence | ||||||||
| * that has not yet been consumed. | ||||||||
| */ | ||||||||
| function authorizationState(address authorizer, bytes32 nonce) public view virtual returns (bool) { | ||||||||
| return uint64(nonces(authorizer, uint192(uint256(nonce) >> 64))) > uint64(uint256(nonce)); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * @dev See {IERC3009-transferWithAuthorization}. | ||||||||
| * | ||||||||
| * NOTE: A signed authorization will only succeed if its nonce is the next expected sequence | ||||||||
| * for the given key. Authorizations sharing a key must be submitted in order. | ||||||||
| */ | ||||||||
| function transferWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce, | ||||||||
| uint8 v, | ||||||||
| bytes32 r, | ||||||||
| bytes32 s | ||||||||
| ) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4( | ||||||||
| keccak256(abi.encode(TRANSFER_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)) | ||||||||
| ); | ||||||||
| (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, v, r, s); | ||||||||
| require(err == ECDSA.RecoverError.NoError && recovered == from, ERC3009InvalidSignature()); | ||||||||
|
Comment on lines
+108
to
+109
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about using the ECDSA custom errors to be more explicit when the failure is caused by an improper signature format ?
Suggested change
|
||||||||
| _transferWithAuthorization(from, to, value, validAfter, validBefore, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Same as {transferWithAuthorization} but with a bytes signature. | ||||||||
| function transferWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce, | ||||||||
| bytes memory signature | ||||||||
| ) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4( | ||||||||
| keccak256(abi.encode(TRANSFER_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)) | ||||||||
| ); | ||||||||
| require(SignatureChecker.isValidSignatureNow(from, hash, signature), ERC3009InvalidSignature()); | ||||||||
| _transferWithAuthorization(from, to, value, validAfter, validBefore, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * @dev See {IERC3009-receiveWithAuthorization}. | ||||||||
| * | ||||||||
| * NOTE: A signed authorization will only succeed if its nonce is the next expected sequence | ||||||||
| * for the given key. Authorizations sharing a key must be submitted in order. | ||||||||
| */ | ||||||||
| function receiveWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce, | ||||||||
| uint8 v, | ||||||||
| bytes32 r, | ||||||||
| bytes32 s | ||||||||
| ) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4( | ||||||||
| keccak256(abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)) | ||||||||
| ); | ||||||||
| (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, v, r, s); | ||||||||
| require(err == ECDSA.RecoverError.NoError && recovered == from, ERC3009InvalidSignature()); | ||||||||
|
Comment on lines
+150
to
+151
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. discussion here : https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6354/changes#r2883684250
Suggested change
|
||||||||
| _receiveWithAuthorization(from, to, value, validAfter, validBefore, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Same as {receiveWithAuthorization} but with a bytes signature. | ||||||||
| function receiveWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce, | ||||||||
| bytes memory signature | ||||||||
| ) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4( | ||||||||
| keccak256(abi.encode(RECEIVE_WITH_AUTHORIZATION_TYPEHASH, from, to, value, validAfter, validBefore, nonce)) | ||||||||
| ); | ||||||||
| require(SignatureChecker.isValidSignatureNow(from, hash, signature), ERC3009InvalidSignature()); | ||||||||
| _receiveWithAuthorization(from, to, value, validAfter, validBefore, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * @dev See {IERC3009Cancel-cancelAuthorization}. | ||||||||
| * | ||||||||
| * NOTE: Due to the keyed sequential nonce model, only the next nonce in a given key's sequence | ||||||||
| * can be cancelled. It is not possible to directly cancel a future nonce whose predecessors in the | ||||||||
| * same key have not yet been consumed or cancelled. To invalidate a future authorization, all | ||||||||
| * preceding nonces in the same key must first be consumed or cancelled in order. | ||||||||
| */ | ||||||||
| function cancelAuthorization(address authorizer, bytes32 nonce, uint8 v, bytes32 r, bytes32 s) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce))); | ||||||||
| (address recovered, ECDSA.RecoverError err, ) = ECDSA.tryRecover(hash, v, r, s); | ||||||||
| require(err == ECDSA.RecoverError.NoError && recovered == authorizer, ERC3009InvalidSignature()); | ||||||||
|
Comment on lines
+182
to
+183
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. discussion here : https://github.com/OpenZeppelin/openzeppelin-contracts/pull/6354/changes#r2883684250
Suggested change
|
||||||||
| _cancelAuthorization(authorizer, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Same as {cancelAuthorization} but with a bytes signature. | ||||||||
| function cancelAuthorization(address authorizer, bytes32 nonce, bytes memory signature) public virtual { | ||||||||
| bytes32 hash = _hashTypedDataV4(keccak256(abi.encode(CANCEL_AUTHORIZATION_TYPEHASH, authorizer, nonce))); | ||||||||
| require(SignatureChecker.isValidSignatureNow(authorizer, hash, signature), ERC3009InvalidSignature()); | ||||||||
| _cancelAuthorization(authorizer, nonce); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Internal version of {transferWithAuthorization} that accepts a bytes signature. | ||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||
| function _transferWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce | ||||||||
| ) internal virtual { | ||||||||
| require( | ||||||||
| block.timestamp > validAfter && block.timestamp < validBefore, | ||||||||
| ERC3009InvalidAuthorizationTime(validAfter, validBefore) | ||||||||
| ); | ||||||||
| _useCheckedNonce(from, uint256(nonce)); | ||||||||
| emit AuthorizationUsed(from, nonce); | ||||||||
| _transfer(from, to, value); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Internal version of {receiveWithAuthorization} that accepts a bytes signature. | ||||||||
| function _receiveWithAuthorization( | ||||||||
| address from, | ||||||||
| address to, | ||||||||
| uint256 value, | ||||||||
| uint256 validAfter, | ||||||||
| uint256 validBefore, | ||||||||
| bytes32 nonce | ||||||||
| ) internal virtual { | ||||||||
| require(to == _msgSender(), ERC20InvalidReceiver(to)); | ||||||||
Amxx marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||
| require( | ||||||||
| block.timestamp > validAfter && block.timestamp < validBefore, | ||||||||
| ERC3009InvalidAuthorizationTime(validAfter, validBefore) | ||||||||
| ); | ||||||||
| _useCheckedNonce(from, uint256(nonce)); | ||||||||
| emit AuthorizationUsed(from, nonce); | ||||||||
| _transfer(from, to, value); | ||||||||
| } | ||||||||
|
|
||||||||
| /// @dev Internal version of {cancelAuthorization} that accepts a bytes signature. | ||||||||
| function _cancelAuthorization(address authorizer, bytes32 nonce) internal virtual { | ||||||||
| _useCheckedNonce(authorizer, uint256(nonce)); | ||||||||
| emit AuthorizationCanceled(authorizer, nonce); | ||||||||
| } | ||||||||
| } | ||||||||
Uh oh!
There was an error while loading. Please reload this page.