-
Notifications
You must be signed in to change notification settings - Fork 122
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
BT-235: ERC2612 - Add ERC-20 Permit function compatibility #236
base: develop
Are you sure you want to change the base?
Changes from all commits
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,121 @@ | ||
// SPDX-License-Identifier: GPL-3.0 | ||
// | ||
// :+#####%%%%%%%%%%%%%%+ | ||
// .-*@@@%+.:+%@@@@@%%#***%@@%= | ||
// :=*%@@@#=. :#@@% *@@@%= | ||
// .-+*%@%*-.:+%@@@@@@+. -*+: .=#. :%@@@%- | ||
// :=*@@@@%%@@@@@@@@@%@@@- .=#@@@%@%= =@@@@#. | ||
// -=+#%@@%#*=:. :%@@@@%. -*@@#*@@@@@@@#=:- *@@@@+ | ||
// =@@%=:. :=: *@@@@@%#- =%*%@@@@#+-. =+ :%@@@%- | ||
// -@@%. .+@@@ =+=-. @@#- +@@@%- =@@@@%: | ||
// :@@@. .+@@#%: : .=*=-::.-%@@@+*@@= +@@@@#. | ||
// %@@: +@%%* =%@@@@@@@@@@@#. .*@%- +@@@@*. | ||
// #@@= .+@@@@%:=*@@@@@- :%@%: .*@@@@+ | ||
// *@@* +@@@#-@@%-:%@@* +@@#. :%@@@@- | ||
// -@@% .:-=++*##%%%@@@@@@@@@@@@*. :@+.@@@%: .#@@+ =@@@@#: | ||
// .@@@*-+*#%%%@@@@@@@@@@@@@@@@%%#**@@%@@@. *@=*@@# :#@%= .#@@@@#- | ||
// -%@@@@@@@@@@@@@@@*+==-:-@@@= *@# .#@*-=*@@@@%= -%@@@* =@@@@@%- | ||
// -+%@@@#. %@%%= -@@:+@: -@@* *@@*-:: -%@@%=. .*@@@@@# | ||
// *@@@* +@* *@@##@@- #@*@@+ -@@= . :+@@@#: .-+@@@%+- | ||
// +@@@%*@@:..=@@@@* .@@@* .#@#. .=+- .=%@@@*. :+#@@@@*=: | ||
// =@@@@%@@@@@@@@@@@@@@@@@@@@@@%- :+#*. :*@@@%=. .=#@@@@%+: | ||
// .%@@= ..... .=#@@+. .#@@@*: -*%@@@@%+. | ||
// +@@#+===---:::... .=%@@*- +@@@+. -*@@@@@%+. | ||
// -@@@@@@@@@@@@@@@@@@@@@@%@@@@= -@@@+ -#@@@@@#=. | ||
// ..:::---===+++***###%%%@@@#- .#@@+ -*@@@@@#=. | ||
// @@@@@@+. +@@*. .+@@@@@%=. | ||
// -@@@@@= =@@%: -#@@@@%+. | ||
// +@@@@@. =@@@= .+@@@@@*: | ||
// #@@@@#:%@@#. :*@@@@#- | ||
// @@@@@%@@@= :#@@@@+. | ||
// :@@@@@@@#.:#@@@%- | ||
// +@@@@@@-.*@@@*: | ||
// #@@@@#.=@@@+. | ||
// @@@@+-%@%= | ||
// :@@@#%@%= | ||
// +@@@@%- | ||
// :#%%= | ||
// | ||
|
||
/** | ||
* NOTICE | ||
* | ||
* The T-REX software is licensed under a proprietary license or the GPL v.3. | ||
* If you choose to receive it under the GPL v.3 license, the following applies: | ||
* T-REX is a suite of smart contracts implementing the ERC-3643 standard and | ||
* developed by Tokeny to manage and transfer financial assets on EVM blockchains | ||
* | ||
* Copyright (C) 2023, Tokeny sàrl. | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the GNU General Public License as published by | ||
* the Free Software Foundation, either version 3 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* GNU General Public License for more details. | ||
* | ||
* You should have received a copy of the GNU General Public License | ||
* along with this program. If not, see <https://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
pragma solidity 0.8.27; | ||
|
||
import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; | ||
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. Named imports are preferred when possible 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. Yes, that as been done for token-factory, but in trex all import are "classical"... But ok to update it. |
||
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import "../utils/EIP712Upgradeable.sol"; | ||
import "../utils/NoncesUpgradeable.sol"; | ||
|
||
error ERC2612ExpiredSignature(uint256 deadline); | ||
error ERC2612InvalidSigner(address signer, address owner); | ||
|
||
|
||
abstract contract TokenPermit is IERC20Permit, EIP712Upgradeable, NoncesUpgradeable { | ||
|
||
bytes32 private constant _PERMIT_TYPEHASH = | ||
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); | ||
|
||
/// @inheritdoc IERC20Permit | ||
function permit( | ||
address owner, | ||
address spender, | ||
uint256 value, | ||
uint256 deadline, | ||
uint8 v, | ||
bytes32 r, | ||
bytes32 s | ||
) external { | ||
require(block.timestamp <= deadline, ERC2612ExpiredSignature(deadline)); | ||
|
||
bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); | ||
|
||
bytes32 hash = _hashTypedDataV4(structHash); | ||
|
||
address signer = ECDSA.recover(hash, v, r, s); | ||
require(signer == owner, ERC2612InvalidSigner(signer, owner)); | ||
|
||
_approve(owner, spender, value); | ||
} | ||
|
||
/// @inheritdoc IERC20Permit | ||
// solhint-disable-next-line func-name-mixedcase | ||
function DOMAIN_SEPARATOR() external view returns (bytes32) { | ||
return _domainSeparatorV4(); | ||
} | ||
|
||
/// @inheritdoc IERC20Permit | ||
function nonces(address owner) public view override(IERC20Permit, NoncesUpgradeable) returns (uint256) { | ||
return super.nonces(owner); | ||
} | ||
|
||
// solhint-disable-next-line func-name-mixedcase | ||
function __TokenPermit_init(string memory name) internal { | ||
__EIP712_init_unchained(name, "1"); | ||
} | ||
|
||
/// @dev Implemented in Token.sol | ||
function _approve(address owner, address spender, uint256 value) internal virtual; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
// SPDX-License-Identifier: MIT | ||
// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/EIP712.sol) | ||
|
||
pragma solidity 0.8.27; | ||
|
||
import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; | ||
import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol"; | ||
|
||
|
||
/** | ||
* @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data. | ||
* | ||
* The encoding scheme specified in the EIP requires a domain separator and a hash of the typed structured data, whose | ||
* encoding is very generic and therefore its 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 order to | ||
* produce the hash of their typed data 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]. | ||
* | ||
* NOTE: In the upgradeable version of this contract, the cached values will correspond to the address, and the domain | ||
* separator of the implementation contract. This will cause the {_domainSeparatorV4} function to always rebuild the | ||
* separator from the immutable values, which is cheaper than accessing a cached version in cold storage. | ||
*/ | ||
abstract contract EIP712Upgradeable is IERC5267 { | ||
|
||
/// @custom:storage-location erc7201:openzeppelin.storage.EIP712 | ||
struct EIP712Storage { | ||
/// @custom:oz-renamed-from _HASHED_NAME | ||
bytes32 _hashedName; | ||
/// @custom:oz-renamed-from _HASHED_VERSION | ||
bytes32 _hashedVersion; | ||
|
||
string _name; | ||
string _version; | ||
} | ||
|
||
bytes32 private constant _TYPE_HASH = | ||
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); | ||
|
||
// keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.EIP712")) - 1)) & ~bytes32(uint256(0xff)) | ||
bytes32 private constant _EIP712_STORAGE_LOCATION = 0xa16a46d94261c7517cc8ff89f61c0ce93598e3c849801011dee649a6a557d100; | ||
|
||
/** | ||
* @dev See {IERC-5267}. | ||
*/ | ||
function eip712Domain() | ||
public | ||
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. Is there a reason why this function is public and not external? It seems to be external in the specification and I haven't seen it being explicitly called internally within the contract 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. It's public in OZ implementation (https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/release-v5.2/contracts/utils/cryptography/EIP712Upgradeable.sol) but yes it can be just external. |
||
view | ||
virtual | ||
returns ( | ||
bytes1 fields, | ||
string memory name, | ||
string memory version, | ||
uint256 chainId, | ||
address verifyingContract, | ||
bytes32 salt, | ||
uint256[] memory extensions | ||
) | ||
{ | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
// If the hashed name and version in storage are non-zero, the contract hasn't been properly initialized | ||
// and the EIP712 domain is not reliable, as it will be missing name and version. | ||
require($._hashedName == 0 && $._hashedVersion == 0, "EIP712: Uninitialized"); | ||
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. Custom error instead of string could help to save some gas here 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. In fact I have just copy/paste OpenZeppelin contract to be sure to not add regression (just reorder function for solhint). At first I try to just import the file, but I haven't succeed to manage several version into the package.json. I need the last version of contract-upgradeable (for the custom storage), but it have dependencies on last OpenZeppelin contracts which is not the one we are using... |
||
|
||
return ( | ||
hex"0f", // 01111 | ||
_EIP712Name(), | ||
_EIP712Version(), | ||
block.chainid, | ||
address(this), | ||
bytes32(0), | ||
new uint256[](0) | ||
); | ||
} | ||
|
||
/** | ||
* @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]. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function __EIP712_init(string memory name, string memory version) internal { | ||
__EIP712_init_unchained(name, version); | ||
} | ||
|
||
// solhint-disable-next-line func-name-mixedcase | ||
function __EIP712_init_unchained(string memory name, string memory version) internal { | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
$._name = name; | ||
$._version = version; | ||
|
||
// Reset prior values in storage if upgrading | ||
$._hashedName = 0; | ||
$._hashedVersion = 0; | ||
} | ||
|
||
/** | ||
* @dev The name parameter for the EIP712 domain. | ||
* | ||
* NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs | ||
* are a concern. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function _EIP712Name() internal view virtual returns (string memory) { | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
return $._name; | ||
} | ||
|
||
/** | ||
* @dev The version parameter for the EIP712 domain. | ||
* | ||
* NOTE: This function reads from storage by default, but can be redefined to return a constant value if gas costs | ||
* are a concern. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function _EIP712Version() internal view virtual returns (string memory) { | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
return $._version; | ||
} | ||
|
||
/** | ||
* @dev The hash of the name parameter for the EIP712 domain. | ||
* | ||
* NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Name` instead. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function _EIP712NameHash() internal view returns (bytes32) { | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
string memory name = _EIP712Name(); | ||
if (bytes(name).length > 0) { | ||
return keccak256(bytes(name)); | ||
} else { | ||
// If the name is empty, the contract may have been upgraded without initializing the new storage. | ||
// We return the name hash in storage if non-zero, otherwise we assume the name is empty by design. | ||
bytes32 hashedName = $._hashedName; | ||
if (hashedName != 0) { | ||
return hashedName; | ||
} else { | ||
return keccak256(""); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @dev The hash of the version parameter for the EIP712 domain. | ||
* | ||
* NOTE: In previous versions this function was virtual. In this version you should override `_EIP712Version` instead. | ||
*/ | ||
// solhint-disable-next-line func-name-mixedcase | ||
function _EIP712VersionHash() internal view returns (bytes32) { | ||
EIP712Storage storage $ = _getEIP712Storage(); | ||
string memory version = _EIP712Version(); | ||
if (bytes(version).length > 0) { | ||
return keccak256(bytes(version)); | ||
} else { | ||
// If the version is empty, the contract may have been upgraded without initializing the new storage. | ||
// We return the version hash in storage if non-zero, otherwise we assume the version is empty by design. | ||
bytes32 hashedVersion = $._hashedVersion; | ||
if (hashedVersion != 0) { | ||
return hashedVersion; | ||
} else { | ||
return keccak256(""); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* @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 virtual returns (bytes32) { | ||
return ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); | ||
} | ||
|
||
/** | ||
* @dev Returns the domain separator for the current chain. | ||
*/ | ||
function _domainSeparatorV4() internal view returns (bytes32) { | ||
return _buildDomainSeparator(); | ||
} | ||
|
||
function _buildDomainSeparator() private view returns (bytes32) { | ||
return keccak256(abi.encode(_TYPE_HASH, _EIP712NameHash(), _EIP712VersionHash(), block.chainid, address(this))); | ||
} | ||
|
||
function _getEIP712Storage() private pure returns (EIP712Storage storage $) { | ||
assembly { | ||
$.slot := _EIP712_STORAGE_LOCATION | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we are implementing ERC165, therefore we should add IERC20Permit to the supported interfaces