From 3be028b4dd9b6e9ea28ef0079fdc9251560819f3 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Mon, 16 Dec 2024 14:23:27 +0100 Subject: [PATCH 1/7] =?UTF-8?q?=E2=9C=A8()=20Permit=20on=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/token/Token.sol | 22 +- contracts/token/TokenPermit.sol | 121 +++++++++++ contracts/utils/EIP712Upgradeable.sol | 277 ++++++++++++++++++++++++++ contracts/utils/NoncesUpgradeable.sol | 58 ++++++ test/token/token-permit.ts | 6 + 5 files changed, 477 insertions(+), 7 deletions(-) create mode 100644 contracts/token/TokenPermit.sol create mode 100644 contracts/utils/EIP712Upgradeable.sol create mode 100644 contracts/utils/NoncesUpgradeable.sol create mode 100644 test/token/token-permit.ts diff --git a/contracts/token/Token.sol b/contracts/token/Token.sol index 949f6865..1fbc539c 100755 --- a/contracts/token/Token.sol +++ b/contracts/token/Token.sol @@ -63,14 +63,15 @@ pragma solidity 0.8.27; -import "./IToken.sol"; import "@onchain-id/solidity/contracts/interface/IIdentity.sol"; -import "./TokenStorage.sol"; -import "../roles/AgentRoleUpgradeable.sol"; +import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "../roles/IERC173.sol"; -import "../errors/InvalidArgumentErrors.sol"; +import "./IToken.sol"; +import "./TokenPermit.sol"; +import "./TokenStorage.sol"; import "../errors/CommonErrors.sol"; -import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import "../errors/InvalidArgumentErrors.sol"; +import "../roles/AgentRoleUpgradeable.sol"; /// errors @@ -119,7 +120,13 @@ error DefaultAllowanceAlreadyDisabled(address _user); error DefaultAllowanceAlreadySet(address _target); -contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 { +contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPermit { + + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); /// modifiers @@ -172,6 +179,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 { , EmptyString()); require(0 <= _decimals && _decimals <= 18, DecimalsOutOfRange(_decimals)); __Ownable_init(); + __TokenPermit_init(_name); _tokenName = _name; _tokenSymbol = _symbol; _tokenDecimals = _decimals; @@ -728,7 +736,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165 { address _owner, address _spender, uint256 _amount - ) internal virtual { + ) internal virtual override { require(_owner != address(0), ERC20InvalidSender(_owner)); require(_spender != address(0), ERC20InvalidSpender(_spender)); diff --git a/contracts/token/TokenPermit.sol b/contracts/token/TokenPermit.sol new file mode 100644 index 00000000..d94e187f --- /dev/null +++ b/contracts/token/TokenPermit.sol @@ -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 . + */ + +pragma solidity 0.8.27; + +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +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; + +} \ No newline at end of file diff --git a/contracts/utils/EIP712Upgradeable.sol b/contracts/utils/EIP712Upgradeable.sol new file mode 100644 index 00000000..0d46db44 --- /dev/null +++ b/contracts/utils/EIP712Upgradeable.sol @@ -0,0 +1,277 @@ +// 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 . + */ + +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 + 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"); + + 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 + } + } +} \ No newline at end of file diff --git a/contracts/utils/NoncesUpgradeable.sol b/contracts/utils/NoncesUpgradeable.sol new file mode 100644 index 00000000..bf827cda --- /dev/null +++ b/contracts/utils/NoncesUpgradeable.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.0.0) (utils/Nonces.sol) +pragma solidity 0.8.27; + +error InvalidAccountNonce(address account, uint256 currentNonce); + +/** + * @dev Provides tracking nonces for addresses. Nonces will only increment. + */ +abstract contract NoncesUpgradeable { + + /// @custom:storage-location erc7201:openzeppelin.storage.Nonces + struct NoncesStorage { + mapping(address account => uint256) _nonces; + } + + // keccak256(abi.encode(uint256(keccak256("openzeppelin.storage.Nonces")) - 1)) & ~bytes32(uint256(0xff)) + bytes32 private constant _NONCES_STORAGE_LOCATION = 0x5ab42ced628888259c08ac98db1eb0cf702fc1501344311d8b100cd1bfe4bb00; + + /** + * @dev Returns the next unused nonce for an address. + */ + function nonces(address owner) public view virtual returns (uint256) { + NoncesStorage storage $ = _getNoncesStorage(); + return $._nonces[owner]; + } + + /** + * @dev Consumes a nonce. + * + * Returns the current value and increments nonce. + */ + function _useNonce(address owner) internal virtual returns (uint256) { + NoncesStorage storage $ = _getNoncesStorage(); + // For each account, the nonce has an initial value of 0, can only be incremented by one, and cannot be + // decremented or reset. This guarantees that the nonce never overflows. + unchecked { + // It is important to do x++ and not ++x here. + return $._nonces[owner]++; + } + } + + /** + * @dev Same as {_useNonce} but checking that `nonce` is the next valid for `owner`. + */ + function _useCheckedNonce(address owner, uint256 nonce) internal virtual { + uint256 current = _useNonce(owner); + if (nonce != current) { + revert InvalidAccountNonce(owner, current); + } + } + + function _getNoncesStorage() private pure returns (NoncesStorage storage $) { + assembly { + $.slot := _NONCES_STORAGE_LOCATION + } + } +} \ No newline at end of file diff --git a/test/token/token-permit.ts b/test/token/token-permit.ts new file mode 100644 index 00000000..ecef610b --- /dev/null +++ b/test/token/token-permit.ts @@ -0,0 +1,6 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; +import { deployFullSuiteFixture, deploySuiteWithModularCompliancesFixture } from '../fixtures/deploy-full-suite.fixture'; + +describe('Token - Permit', () => {}); From bd7b7e2c756432da0fe9eda1622efe14a24edb94 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Tue, 17 Dec 2024 11:51:17 +0100 Subject: [PATCH 2/7] =?UTF-8?q?=E2=9C=85(test)=20bt-235=20add=20Permit=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/utils/EIP712Upgradeable.sol | 64 +----------- test/token/token-permit.ts | 137 +++++++++++++++++++++++++- 2 files changed, 136 insertions(+), 65 deletions(-) diff --git a/contracts/utils/EIP712Upgradeable.sol b/contracts/utils/EIP712Upgradeable.sol index 0d46db44..2168a9ba 100644 --- a/contracts/utils/EIP712Upgradeable.sol +++ b/contracts/utils/EIP712Upgradeable.sol @@ -1,65 +1,5 @@ -// 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 . - */ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.1.0) (utils/cryptography/EIP712.sol) pragma solidity 0.8.27; diff --git a/test/token/token-permit.ts b/test/token/token-permit.ts index ecef610b..66d156a9 100644 --- a/test/token/token-permit.ts +++ b/test/token/token-permit.ts @@ -1,6 +1,137 @@ -import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; -import { deployFullSuiteFixture, deploySuiteWithModularCompliancesFixture } from '../fixtures/deploy-full-suite.fixture'; +import { deployFullSuiteFixture } from '../fixtures/deploy-full-suite.fixture'; +import { Token } from '../../typechain-types'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; -describe('Token - Permit', () => {}); +describe('Token - Permit', () => { + const value = 42n; + const nonce = 0n; + const maxDeadline = ethers.MaxUint256; + + async function getDomain(token: Token) { + return { + chainId: (await ethers.provider.getNetwork()).chainId, + verifyingContract: token.target.toString(), + name: await token.name(), + version: '1', + }; + } + + async function buildData(token: Token, owner: SignerWithAddress, spender: SignerWithAddress, deadline = maxDeadline) { + const domain = await getDomain(token); + const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }; + const message = { + owner: owner.address, + spender: spender.address, + value, + nonce, + deadline, + }; + + return { domain, types, message }; + } + + describe('Initial state', () => { + it('initial nonce is 0', async () => { + const { + suite: { token }, + accounts: { aliceWallet, bobWallet, anotherWallet }, + } = await loadFixture(deployFullSuiteFixture); + + expect(await token.nonces(aliceWallet)).to.equal(0n); + expect(await token.nonces(bobWallet)).to.equal(0n); + expect(await token.nonces(anotherWallet)).to.equal(0n); + }); + + it('domain separator', async () => { + const { + suite: { token }, + } = await loadFixture(deployFullSuiteFixture); + + const hashedDomain = ethers.TypedDataEncoder.hashDomain(await getDomain(token)); + + expect(await token.DOMAIN_SEPARATOR()).to.equal(hashedDomain); + }); + }); + + describe('Permit', () => { + it('accepts owner signature', async () => { + const { + suite: { token }, + accounts: { aliceWallet, bobWallet }, + } = await loadFixture(deployFullSuiteFixture); + + const { v, r, s } = await buildData(token, aliceWallet, bobWallet) + .then(({ domain, types, message }) => aliceWallet.signTypedData(domain, types, message)) + .then(ethers.Signature.from); + + await token.permit(aliceWallet, bobWallet, value, maxDeadline, v, r, s); + + expect(await token.nonces(aliceWallet)).to.equal(1n); + expect(await token.allowance(aliceWallet, bobWallet)).to.equal(value); + }); + + it('rejects reused signature', async () => { + const { + suite: { token }, + accounts: { aliceWallet, bobWallet }, + } = await loadFixture(deployFullSuiteFixture); + + const { v, r, s, serialized } = await buildData(token, aliceWallet, bobWallet) + .then(({ domain, types, message }) => aliceWallet.signTypedData(domain, types, message)) + .then(ethers.Signature.from); + + await token.permit(aliceWallet, bobWallet, value, maxDeadline, v, r, s); + + const recovered = await buildData(token, aliceWallet, bobWallet).then(({ domain, types, message }) => + ethers.verifyTypedData(domain, types, { ...message, nonce: nonce + 1n, deadline: maxDeadline }, serialized), + ); + + await expect(token.permit(aliceWallet, bobWallet, value, maxDeadline, v, r, s)) + .to.be.revertedWithCustomError(token, 'ERC2612InvalidSigner') + .withArgs(recovered, aliceWallet); + }); + + it('rejects other signature', async () => { + const { + suite: { token }, + accounts: { aliceWallet, bobWallet }, + } = await loadFixture(deployFullSuiteFixture); + + const { v, r, s } = await buildData(token, aliceWallet, bobWallet) + .then(({ domain, types, message }) => bobWallet.signTypedData(domain, types, message)) + .then(ethers.Signature.from); + + await expect(token.permit(aliceWallet, bobWallet, value, maxDeadline, v, r, s)) + .to.be.revertedWithCustomError(token, 'ERC2612InvalidSigner') + .withArgs(bobWallet, aliceWallet); + }); + + it('rejects expired permit', async () => { + const { + suite: { token }, + accounts: { aliceWallet, bobWallet }, + } = await loadFixture(deployFullSuiteFixture); + + const deadline = (await time.latest().then(ethers.toBigInt)) - BigInt(time.duration.weeks(1)); + + const { v, r, s } = await buildData(token, aliceWallet, bobWallet, deadline) + .then(({ domain, types, message }) => aliceWallet.signTypedData(domain, types, message)) + .then(ethers.Signature.from); + + await expect(token.permit(aliceWallet, bobWallet, value, deadline, v, r, s)) + .to.be.revertedWithCustomError(token, 'ERC2612ExpiredSignature') + .withArgs(deadline); + }); + }); +}); From d22ebbc78156f20c7e90493b17a128950da4e647 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Tue, 17 Dec 2024 11:58:31 +0100 Subject: [PATCH 3/7] =?UTF-8?q?=E2=9C=85(test)=20lint=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/token/token-permit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/token/token-permit.ts b/test/token/token-permit.ts index 66d156a9..1f9dd2e5 100644 --- a/test/token/token-permit.ts +++ b/test/token/token-permit.ts @@ -1,9 +1,9 @@ import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; +import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { expect } from 'chai'; import { ethers } from 'hardhat'; import { deployFullSuiteFixture } from '../fixtures/deploy-full-suite.fixture'; import { Token } from '../../typechain-types'; -import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; describe('Token - Permit', () => { const value = 42n; From dd48e3cdb3d8347d0c1b41896ee8fa307cc6c5d8 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Tue, 31 Dec 2024 10:05:06 +0100 Subject: [PATCH 4/7] =?UTF-8?q?=E2=99=BB()=20bt-235=20update=20after=20rev?= =?UTF-8?q?iew?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/token/Token.sol | 3 ++- contracts/token/TokenPermit.sol | 8 ++++---- contracts/utils/EIP712Upgradeable.sol | 5 +++-- contracts/utils/InterfaceIdCalculator.sol | 9 +++++++++ test/token/token-information.test.ts | 11 +++++++++++ 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/contracts/token/Token.sol b/contracts/token/Token.sol index 1fbc539c..6fb97d93 100755 --- a/contracts/token/Token.sol +++ b/contracts/token/Token.sol @@ -682,7 +682,8 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm interfaceId == type(IToken).interfaceId || interfaceId == type(IERC173).interfaceId || interfaceId == type(IERC165).interfaceId || - interfaceId == type(IERC3643).interfaceId; + interfaceId == type(IERC3643).interfaceId || + interfaceId == type(IERC20Permit).interfaceId; } /** diff --git a/contracts/token/TokenPermit.sol b/contracts/token/TokenPermit.sol index d94e187f..1d039bab 100644 --- a/contracts/token/TokenPermit.sol +++ b/contracts/token/TokenPermit.sol @@ -63,10 +63,10 @@ pragma solidity 0.8.27; -import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; -import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import "../utils/EIP712Upgradeable.sol"; -import "../utils/NoncesUpgradeable.sol"; +import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { EIP712Upgradeable } from "../utils/EIP712Upgradeable.sol"; +import { NoncesUpgradeable } from "../utils/NoncesUpgradeable.sol"; error ERC2612ExpiredSignature(uint256 deadline); error ERC2612InvalidSigner(address signer, address owner); diff --git a/contracts/utils/EIP712Upgradeable.sol b/contracts/utils/EIP712Upgradeable.sol index 2168a9ba..33efbe53 100644 --- a/contracts/utils/EIP712Upgradeable.sol +++ b/contracts/utils/EIP712Upgradeable.sol @@ -6,6 +6,7 @@ pragma solidity 0.8.27; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol"; +error EIP712Uninitialized(); /** * @dev https://eips.ethereum.org/EIPS/eip-712[EIP-712] is a standard for hashing and signing of typed structured data. @@ -52,7 +53,7 @@ abstract contract EIP712Upgradeable is IERC5267 { * @dev See {IERC-5267}. */ function eip712Domain() - public + external view virtual returns ( @@ -68,7 +69,7 @@ abstract contract EIP712Upgradeable is IERC5267 { 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"); + require($._hashedName == 0 && $._hashedVersion == 0, EIP712Uninitialized()); return ( hex"0f", // 01111 diff --git a/contracts/utils/InterfaceIdCalculator.sol b/contracts/utils/InterfaceIdCalculator.sol index 0f015a1d..a16df780 100644 --- a/contracts/utils/InterfaceIdCalculator.sol +++ b/contracts/utils/InterfaceIdCalculator.sol @@ -65,6 +65,7 @@ pragma solidity 0.8.27; import "@openzeppelin/contracts/utils/introspection/IERC165.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import "../roles/IERC173.sol"; import "../token/IToken.sol"; import "../proxy/authority/ITREXImplementationAuthority.sol"; @@ -88,6 +89,14 @@ contract InterfaceIdCalculator { return type(IERC20).interfaceId; } + /** + * @dev Returns the interface ID for the IERC20Permit interface. + * IERC20Permit interface ID is 0x0b4c7e4d + */ + function getIERC20PermitInterfaceId() external pure returns (bytes4) { + return type(IERC20Permit).interfaceId; + } + /** * @dev Returns the interface ID for the IERC3643 interface. * IERC3643 interface ID is 0xb97d944c diff --git a/test/token/token-information.test.ts b/test/token/token-information.test.ts index 0de50cf7..edc2c49f 100644 --- a/test/token/token-information.test.ts +++ b/test/token/token-information.test.ts @@ -398,5 +398,16 @@ describe('Token - Information', () => { const ierc165InterfaceId = await interfaceIdCalculator.getIERC165InterfaceId(); expect(await token.supportsInterface(ierc165InterfaceId)).to.equal(true); }); + + it('should correctly identify the IERC20Permit interface ID', async () => { + const { + suite: { token }, + } = await loadFixture(deployFullSuiteFixture); + const InterfaceIdCalculator = await ethers.getContractFactory('InterfaceIdCalculator'); + const interfaceIdCalculator = await InterfaceIdCalculator.deploy(); + + const ierc20PermitInterfaceId = await interfaceIdCalculator.getIERC20PermitInterfaceId(); + expect(await token.supportsInterface(ierc20PermitInterfaceId)).to.equal(true); + }); }); }); From ea1429d0e85a41539270f63f4c2cebc8d07ef3a3 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Fri, 7 Feb 2025 15:10:02 +0100 Subject: [PATCH 5/7] =?UTF-8?q?=E2=99=BB()=20Update=20permit=20with=20no?= =?UTF-8?q?=20initialization?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/token/Token.sol | 5 +- contracts/token/TokenPermit.sol | 48 +++++- contracts/utils/EIP712Upgradeable.sol | 218 -------------------------- test/token/token-permit.ts | 4 +- 4 files changed, 45 insertions(+), 230 deletions(-) delete mode 100644 contracts/utils/EIP712Upgradeable.sol diff --git a/contracts/token/Token.sol b/contracts/token/Token.sol index 6fb97d93..581bc140 100755 --- a/contracts/token/Token.sol +++ b/contracts/token/Token.sol @@ -179,7 +179,6 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm , EmptyString()); require(0 <= _decimals && _decimals <= 18, DecimalsOutOfRange(_decimals)); __Ownable_init(); - __TokenPermit_init(_name); _tokenName = _name; _tokenSymbol = _symbol; _tokenDecimals = _decimals; @@ -505,7 +504,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm /** * @dev See {IToken-name}. */ - function name() external view override returns (string memory) { + function name() public view override(IERC20Metadata, TokenPermit) returns (string memory) { return _tokenName; } @@ -526,7 +525,7 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm /** * @dev See {IToken-version}. */ - function version() external pure override returns (string memory) { + function version() public pure override(IERC3643, TokenPermit) returns (string memory) { return _TOKEN_VERSION; } diff --git a/contracts/token/TokenPermit.sol b/contracts/token/TokenPermit.sol index 1d039bab..0e84d831 100644 --- a/contracts/token/TokenPermit.sol +++ b/contracts/token/TokenPermit.sol @@ -65,18 +65,21 @@ pragma solidity 0.8.27; import { IERC20Permit } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol"; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { EIP712Upgradeable } from "../utils/EIP712Upgradeable.sol"; +import { IERC5267 } from "@openzeppelin/contracts/interfaces/IERC5267.sol"; import { NoncesUpgradeable } from "../utils/NoncesUpgradeable.sol"; error ERC2612ExpiredSignature(uint256 deadline); error ERC2612InvalidSigner(address signer, address owner); -abstract contract TokenPermit is IERC20Permit, EIP712Upgradeable, NoncesUpgradeable { +abstract contract TokenPermit is IERC20Permit, IERC5267, NoncesUpgradeable { bytes32 private constant _PERMIT_TYPEHASH = keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 private constant _TYPE_HASH = + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + /// @inheritdoc IERC20Permit function permit( address owner, @@ -91,7 +94,7 @@ abstract contract TokenPermit is IERC20Permit, EIP712Upgradeable, NoncesUpgradea bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); - bytes32 hash = _hashTypedDataV4(structHash); + bytes32 hash = ECDSA.toTypedDataHash(_domainSeparatorV4(), structHash); address signer = ECDSA.recover(hash, v, r, s); require(signer == owner, ERC2612InvalidSigner(signer, owner)); @@ -110,12 +113,41 @@ abstract contract TokenPermit is IERC20Permit, EIP712Upgradeable, NoncesUpgradea return super.nonces(owner); } - // solhint-disable-next-line func-name-mixedcase - function __TokenPermit_init(string memory name) internal { - __EIP712_init_unchained(name, "1"); + // @dev Returns the domain separator for the current chain. + function _domainSeparatorV4() internal view returns (bytes32) { + return keccak256(abi.encode(_TYPE_HASH, keccak256(bytes(name())), keccak256(bytes(version())), block.chainid, address(this))); } /// @dev Implemented in Token.sol - function _approve(address owner, address spender, uint256 value) internal virtual; - + function _approve(address _owner, address _spender, uint256 _value) internal virtual; + + function name() public virtual view returns (string memory); + + function version() public virtual view returns (string memory); + + /// @inheritdoc IERC5267 + function eip712Domain() + external + view + virtual + returns ( + bytes1 _fields, + string memory _name, + string memory _version, + uint256 _chainId, + address _verifyingContract, + bytes32 _salt, + uint256[] memory _extensions + ) + { + return ( + hex"0f", // 01111 + name(), + version(), + block.chainid, + address(this), + bytes32(0), + new uint256[](0) + ); + } } \ No newline at end of file diff --git a/contracts/utils/EIP712Upgradeable.sol b/contracts/utils/EIP712Upgradeable.sol deleted file mode 100644 index 33efbe53..00000000 --- a/contracts/utils/EIP712Upgradeable.sol +++ /dev/null @@ -1,218 +0,0 @@ -// 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"; - -error EIP712Uninitialized(); - -/** - * @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() - 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, EIP712Uninitialized()); - - 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 - } - } -} \ No newline at end of file diff --git a/test/token/token-permit.ts b/test/token/token-permit.ts index 1f9dd2e5..a78843fc 100644 --- a/test/token/token-permit.ts +++ b/test/token/token-permit.ts @@ -15,7 +15,7 @@ describe('Token - Permit', () => { chainId: (await ethers.provider.getNetwork()).chainId, verifyingContract: token.target.toString(), name: await token.name(), - version: '1', + version: await token.version(), }; } @@ -51,6 +51,8 @@ describe('Token - Permit', () => { expect(await token.nonces(aliceWallet)).to.equal(0n); expect(await token.nonces(bobWallet)).to.equal(0n); expect(await token.nonces(anotherWallet)).to.equal(0n); + + console.log(await token.eip712Domain()); }); it('domain separator', async () => { From fe94ad8030c91d8588da9e2bbce0e972c622b61a Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Fri, 7 Feb 2025 15:17:08 +0100 Subject: [PATCH 6/7] =?UTF-8?q?=F0=9F=94=A7()=20lint=20issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- contracts/token/Token.sol | 28 ++++++++++---------- contracts/token/TokenPermit.sol | 47 ++++++++++++++++++++------------- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/contracts/token/Token.sol b/contracts/token/Token.sol index 581bc140..34029a6e 100755 --- a/contracts/token/Token.sol +++ b/contracts/token/Token.sol @@ -501,13 +501,6 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm return _tokenDecimals; } - /** - * @dev See {IToken-name}. - */ - function name() public view override(IERC20Metadata, TokenPermit) returns (string memory) { - return _tokenName; - } - /** * @dev See {IToken-onchainID}. */ @@ -522,13 +515,6 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm return _tokenSymbol; } - /** - * @dev See {IToken-version}. - */ - function version() public pure override(IERC3643, TokenPermit) returns (string memory) { - return _TOKEN_VERSION; - } - /** * @notice ERC-20 overridden function that include logic to check for trade validity. * Require that the msg.sender and to addresses are not frozen. @@ -672,6 +658,13 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm return _agentsRestrictions[agent]; } + /** + * @dev See {IToken-name}. + */ + function name() public view override(IERC20Metadata, TokenPermit) returns (string memory) { + return _tokenName; + } + /** * @dev See {IERC165-supportsInterface}. */ @@ -685,6 +678,13 @@ contract Token is IToken, AgentRoleUpgradeable, TokenStorage, IERC165, TokenPerm interfaceId == type(IERC20Permit).interfaceId; } + /** + * @dev See {IToken-version}. + */ + function version() public pure override(IERC3643, TokenPermit) returns (string memory) { + return _TOKEN_VERSION; + } + /** * @dev See {ERC20-_transfer}. */ diff --git a/contracts/token/TokenPermit.sol b/contracts/token/TokenPermit.sol index 0e84d831..7741551e 100644 --- a/contracts/token/TokenPermit.sol +++ b/contracts/token/TokenPermit.sol @@ -108,24 +108,7 @@ abstract contract TokenPermit is IERC20Permit, IERC5267, NoncesUpgradeable { return _domainSeparatorV4(); } - /// @inheritdoc IERC20Permit - function nonces(address owner) public view override(IERC20Permit, NoncesUpgradeable) returns (uint256) { - return super.nonces(owner); - } - - // @dev Returns the domain separator for the current chain. - function _domainSeparatorV4() internal view returns (bytes32) { - return keccak256(abi.encode(_TYPE_HASH, keccak256(bytes(name())), keccak256(bytes(version())), block.chainid, address(this))); - } - - /// @dev Implemented in Token.sol - function _approve(address _owner, address _spender, uint256 _value) internal virtual; - - function name() public virtual view returns (string memory); - - function version() public virtual view returns (string memory); - - /// @inheritdoc IERC5267 + /// @inheritdoc IERC5267 function eip712Domain() external view @@ -150,4 +133,32 @@ abstract contract TokenPermit is IERC20Permit, IERC5267, NoncesUpgradeable { new uint256[](0) ); } + + /// @inheritdoc IERC20Permit + function nonces(address owner) public view override(IERC20Permit, NoncesUpgradeable) returns (uint256) { + return super.nonces(owner); + } + + /// @dev Implemented in Token.sol + function name() public virtual view returns (string memory); + + /// @dev Implemented in Token.sol + function version() public virtual view returns (string memory); + + /// @dev Implemented in Token.sol + function _approve(address _owner, address _spender, uint256 _value) internal virtual; + + // @dev Returns the domain separator for the current chain. + function _domainSeparatorV4() internal view returns (bytes32) { + return keccak256( + abi.encode( + _TYPE_HASH, + keccak256(bytes(name())), + keccak256(bytes(version())), + block.chainid, + address(this) + ) + ); + } + } \ No newline at end of file From 78ac80f535407be616f61816b390ffd41bdd6bf3 Mon Sep 17 00:00:00 2001 From: Philippe Gonday Date: Fri, 7 Feb 2025 15:24:46 +0100 Subject: [PATCH 7/7] =?UTF-8?q?=F0=9F=94=A7()=20Deprecation=20of=20github?= =?UTF-8?q?=20artifact=20v3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/push_checking.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push_checking.yml b/.github/workflows/push_checking.yml index 9e66d322..c9d131c9 100644 --- a/.github/workflows/push_checking.yml +++ b/.github/workflows/push_checking.yml @@ -48,7 +48,7 @@ jobs: - name: Run test coverage run: npm run coverage - name: Upload coverage to action results - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: path: | coverage/