From a3c0c9c94ea192b5fc9fc1ed0fdf52d275075a87 Mon Sep 17 00:00:00 2001 From: Mauro Piazza Date: Mon, 12 Aug 2024 18:11:12 +0200 Subject: [PATCH] chore(PTokenV2): remove ERC777 & others deps entirely --- solidity/forge-dependencies.sh | 2 +- solidity/forge-dependencies.txt | 9 +- solidity/hardhat.config.cjs | 8 +- solidity/lib/forge-std | 2 +- solidity/lib/openzeppelin-contracts | 2 +- .../lib/openzeppelin-contracts-upgradeable | 2 +- solidity/lib/solidity-bytes-utils | 2 +- solidity/package.json | 1 + solidity/remappings.txt | 1 + solidity/src/Adapter.sol | 1 + solidity/src/interfaces/IOwnable.sol | 12 + solidity/src/interfaces/IPTokenV2.sol | 41 ++ solidity/src/interfaces/IXERC20.sol | 36 +- ...ERC777GSN.sol => ERC777GSNUpgradeable.sol} | 8 +- solidity/src/ptoken-v1/ERC777Upgradeable.sol | 18 +- .../ERC777WithAdminOperatorUpgradeable.sol | 2 +- solidity/src/ptoken-v1/PToken.sol | 6 +- solidity/src/ptoken-v1/PTokenNoGSN.sol | 4 +- .../src/ptoken-v2/IFeesManager-solc-0.6.sol | 107 --- solidity/src/ptoken-v2/IXERC20-solc-0.6.sol | 164 ----- .../src/ptoken-v2/PTokenV1NoGSNStorage.sol | 45 ++ solidity/src/ptoken-v2/PTokenV1Storage.sol | 48 ++ solidity/src/ptoken-v2/PTokenV2.sol | 455 ++++++++---- solidity/src/ptoken-v2/PTokenV2NoGSN.sol | 670 ++++++++++++++++++ solidity/src/xerc20/XERC20.sol | 19 +- solidity/test/hardhat/Adapter.test.js | 14 +- solidity/test/hardhat/PTokenV2.test.js | 315 ++++---- solidity/test/hardhat/utils/deploy-proxy.cjs | 4 +- .../test/hardhat/utils/get-upgrade-opts.cjs | 7 +- solidity/test/hardhat/utils/upgrade-proxy.cjs | 4 +- yarn.lock | 28 + 31 files changed, 1353 insertions(+), 684 deletions(-) create mode 100644 solidity/src/interfaces/IOwnable.sol create mode 100644 solidity/src/interfaces/IPTokenV2.sol rename solidity/src/ptoken-v1/{ERC777GSN.sol => ERC777GSNUpgradeable.sol} (93%) delete mode 100644 solidity/src/ptoken-v2/IFeesManager-solc-0.6.sol delete mode 100644 solidity/src/ptoken-v2/IXERC20-solc-0.6.sol create mode 100644 solidity/src/ptoken-v2/PTokenV1NoGSNStorage.sol create mode 100644 solidity/src/ptoken-v2/PTokenV1Storage.sol create mode 100644 solidity/src/ptoken-v2/PTokenV2NoGSN.sol diff --git a/solidity/forge-dependencies.sh b/solidity/forge-dependencies.sh index 0ccd2735..0b28b725 100755 --- a/solidity/forge-dependencies.sh +++ b/solidity/forge-dependencies.sh @@ -2,4 +2,4 @@ ls lib/ | \ xargs -I % bash -c \ -'cat ./lib/%/package.json | jq -r "@text \"\(.name)@\(.version)\""' +'cat ./lib/%/package.json' | jq -r '.version as $version | .repository.url|scan("([a-z-]+.git)") | @text "\(.[0])@\($version)"' diff --git a/solidity/forge-dependencies.txt b/solidity/forge-dependencies.txt index 919ebf36..402453ee 100644 --- a/solidity/forge-dependencies.txt +++ b/solidity/forge-dependencies.txt @@ -1,3 +1,6 @@ -forge-std@1.8.2 -openzeppelin-solidity@5.0.2 -openzeppelin-solidity@3.4.2 +forge-std.git@1.9.2 +openzeppelin-contracts.git@5.0.2 +openzeppelin-contracts-upgradeable.git@4.9.6 +openzeppelin-contracts.git@3.4.2 +git@git@0.8.2 +solidity-bytes-utils.git@0.8.2 diff --git a/solidity/hardhat.config.cjs b/solidity/hardhat.config.cjs index 87a9c9f8..33d58054 100644 --- a/solidity/hardhat.config.cjs +++ b/solidity/hardhat.config.cjs @@ -4,6 +4,7 @@ require('@openzeppelin/hardhat-upgrades') require('@nomicfoundation/hardhat-toolbox') require('@nomicfoundation/hardhat-foundry') require('@nomicfoundation/hardhat-network-helpers') +require('hardhat-storage-layout') require('hardhat-gas-reporter') require('solidity-coverage') require('hardhat-tracer') @@ -32,7 +33,10 @@ module.exports = { }, }, }, - { version: '0.8.25' }, + { + version: '0.8.25', + settings: { evmVersion: 'cancun' }, + }, ], }, paths: { @@ -52,7 +56,7 @@ module.exports = { }, }, ethFork: { - url: getEnvironmentVariable('ETH_RPC_URL'), + url: getEnvironmentVariable('MAINNET_RPC_URL'), }, bscFork: { url: getEnvironmentVariable('BSC_RPC_URL'), diff --git a/solidity/lib/forge-std b/solidity/lib/forge-std index 978ac6fa..bf660614 160000 --- a/solidity/lib/forge-std +++ b/solidity/lib/forge-std @@ -1 +1 @@ -Subproject commit 978ac6fadb62f5f0b723c996f64be52eddba6801 +Subproject commit bf6606142994b1e47e2882ce0cd477c020d77623 diff --git a/solidity/lib/openzeppelin-contracts b/solidity/lib/openzeppelin-contracts index dbb6104c..c304b671 160000 --- a/solidity/lib/openzeppelin-contracts +++ b/solidity/lib/openzeppelin-contracts @@ -1 +1 @@ -Subproject commit dbb6104ce834628e473d2173bbc9d47f81a9eec3 +Subproject commit c304b6710b4b5fcf2a319ad28c36c49df6caef14 diff --git a/solidity/lib/openzeppelin-contracts-upgradeable b/solidity/lib/openzeppelin-contracts-upgradeable index 8be33850..2d081f24 160000 --- a/solidity/lib/openzeppelin-contracts-upgradeable +++ b/solidity/lib/openzeppelin-contracts-upgradeable @@ -1 +1 @@ -Subproject commit 8be33850225087203e482bfb19a2ae27cfa7345e +Subproject commit 2d081f24cac1a867f6f73d512f2022e1fa987854 diff --git a/solidity/lib/solidity-bytes-utils b/solidity/lib/solidity-bytes-utils index e0115c4d..df88556c 160000 --- a/solidity/lib/solidity-bytes-utils +++ b/solidity/lib/solidity-bytes-utils @@ -1 +1 @@ -Subproject commit e0115c4d231910df47ce3b60625ce562fe4af985 +Subproject commit df88556cbbc267b33a787a3a6eaa32fd7247b589 diff --git a/solidity/package.json b/solidity/package.json index d92bf966..1545d569 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -42,6 +42,7 @@ "ethers": "^6.13.1", "hardhat": "^2.22.6", "hardhat-gas-reporter": "^2.2.0", + "hardhat-storage-layout": "^0.1.7", "hardhat-tracer": "^3.0.1", "ramda": "^0.30.1", "solhint": "^5.0.2", diff --git a/solidity/remappings.txt b/solidity/remappings.txt index 718f8e30..e2b6d512 100644 --- a/solidity/remappings.txt +++ b/solidity/remappings.txt @@ -1,3 +1,4 @@ forge-std/=lib/forge-std/src/ @openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts +@openzeppelin/contracts-upgradeable-v3.4.2/=lib/openzeppelin-contracts-upgradeable-v3.4.2/contracts solidity-bytes-utils/contracts=lib/solidity-bytes-utils/contracts diff --git a/solidity/src/Adapter.sol b/solidity/src/Adapter.sol index d57d7857..b8a98478 100644 --- a/solidity/src/Adapter.sol +++ b/solidity/src/Adapter.sol @@ -8,6 +8,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IPAM} from "./interfaces/IPAM.sol"; import {IPAM} from "./interfaces/IPAM.sol"; import {IXERC20} from "./interfaces/IXERC20.sol"; +import {IPTokenV2} from "./interfaces/IPTokenV2.sol"; import {IAdapter} from "./interfaces/IAdapter.sol"; import {IPReceiver} from "./interfaces/IPReceiver.sol"; import {IFeesManager} from "./interfaces/IFeesManager.sol"; diff --git a/solidity/src/interfaces/IOwnable.sol b/solidity/src/interfaces/IOwnable.sol new file mode 100644 index 00000000..f186e428 --- /dev/null +++ b/solidity/src/interfaces/IOwnable.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.17; + +interface IOwnable { + event OwnershipTransferred( + address indexed previousOwner, + address indexed newOwner + ); + function owner() external returns (address); + function renounceOwnership() external; + function transferOwnership(address newOwner) external; +} diff --git a/solidity/src/interfaces/IPTokenV2.sol b/solidity/src/interfaces/IPTokenV2.sol new file mode 100644 index 00000000..11d82e03 --- /dev/null +++ b/solidity/src/interfaces/IPTokenV2.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.4 <0.9.0; + +interface IPTokenV2 { + /** + * @notice Returns if this token is local or not + */ + function isLocal() external returns (bool); + + /** + * @notice Returns the fees manager address + */ + function getLockbox() external returns (address); + + /** + * @notice Returns the PAM address + * + * @param adapter the relative adapter + */ + function getPAM(address adapter) external returns (address); + + /** + * @notice Returns the fees manager address + */ + function getFeesManager() external returns (address); + + /** + * @notice Set the fees manager address + * @param newAddress new fees manager address + */ + function setFeesManager(address newAddress) external; + + /** + * @notice Set the new PAM address + * @dev Be sure the API called by the adapter is respected + * + * @param adapterAddress the adapter address + * @param pamAddress the new PAM address + */ + function setPAM(address adapterAddress, address pamAddress) external; +} diff --git a/solidity/src/interfaces/IXERC20.sol b/solidity/src/interfaces/IXERC20.sol index c5776d5d..6734b106 100644 --- a/solidity/src/interfaces/IXERC20.sol +++ b/solidity/src/interfaces/IXERC20.sol @@ -1,7 +1,9 @@ -// SPDX-License-Identifier: MIT +// SPDX-License-Identifier: UNLICENSED pragma solidity >=0.8.4 <0.9.0; -interface IXERC20 { +import {IPTokenV2} from "../interfaces/IPTokenV2.sol"; + +interface IXERC20 is IPTokenV2 { /** * @notice Emits when a lockbox is set * @@ -55,7 +57,6 @@ interface IXERC20 { * @param maxLimit The max limit of the bridge * @param currentLimit The current limit of the bridge */ - // solhint-disable-next-line struct BridgeParameters { uint256 timestamp; uint256 ratePerSecond; @@ -144,33 +145,4 @@ interface IXERC20 { */ function burn(address _user, uint256 _amount) external; - - // TODO: for clarity, move everything below to an interface IPTokenv2 - /** - * @notice Returns the fees manager address - */ - function getLockbox() external returns (address); - - /** - * @notice Returns the PAM address - * - * @param adapter the relative adapter - */ - function getPAM(address adapter) external returns (address); - - /** - * @notice Returns if this token is local or not - */ - function isLocal() external returns (bool); - - /** - * @notice Returns the fees manager address - */ - function getFeesManager() external returns (address); - - /** - * @notice Set the fees manager address - * @param newAddress new fees manager address - */ - function setFeesManager(address newAddress) external; } diff --git a/solidity/src/ptoken-v1/ERC777GSN.sol b/solidity/src/ptoken-v1/ERC777GSNUpgradeable.sol similarity index 93% rename from solidity/src/ptoken-v1/ERC777GSN.sol rename to solidity/src/ptoken-v1/ERC777GSNUpgradeable.sol index a6162abd..d7bf8f29 100644 --- a/solidity/src/ptoken-v1/ERC777GSN.sol +++ b/solidity/src/ptoken-v1/ERC777GSNUpgradeable.sol @@ -1,9 +1,9 @@ pragma solidity ^0.6.2; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/cryptography/ECDSAUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/GSN/GSNRecipientUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/cryptography/ECDSAUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/GSN/GSNRecipientUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/access/OwnableUpgradeable.sol"; import "./ERC777Upgradeable.sol"; contract ERC777GSNUpgradeable is diff --git a/solidity/src/ptoken-v1/ERC777Upgradeable.sol b/solidity/src/ptoken-v1/ERC777Upgradeable.sol index 7930bef3..c2258cb8 100644 --- a/solidity/src/ptoken-v1/ERC777Upgradeable.sol +++ b/solidity/src/ptoken-v1/ERC777Upgradeable.sol @@ -2,15 +2,15 @@ pragma solidity >=0.6.0 <0.8.0; -import "@openzeppelin/contracts-upgradeable/utils/ContextUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC777/IERC777Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC777/IERC777RecipientUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC777/IERC777SenderUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/math/SafeMathUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/utils/AddressUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/introspection/IERC1820RegistryUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/utils/ContextUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/token/ERC777/IERC777Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/token/ERC777/IERC777RecipientUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/token/ERC777/IERC777SenderUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/token/ERC20/IERC20Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/math/SafeMathUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/utils/AddressUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/introspection/IERC1820RegistryUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/proxy/Initializable.sol"; /** * @dev Implementation of the {IERC777} interface. diff --git a/solidity/src/ptoken-v1/ERC777WithAdminOperatorUpgradeable.sol b/solidity/src/ptoken-v1/ERC777WithAdminOperatorUpgradeable.sol index c278ebfe..ec65bcd3 100644 --- a/solidity/src/ptoken-v1/ERC777WithAdminOperatorUpgradeable.sol +++ b/solidity/src/ptoken-v1/ERC777WithAdminOperatorUpgradeable.sol @@ -1,6 +1,6 @@ pragma solidity ^0.6.2; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/proxy/Initializable.sol"; import "./ERC777Upgradeable.sol"; contract ERC777WithAdminOperatorUpgradeable is diff --git a/solidity/src/ptoken-v1/PToken.sol b/solidity/src/ptoken-v1/PToken.sol index 1d4e1616..d84641a6 100644 --- a/solidity/src/ptoken-v1/PToken.sol +++ b/solidity/src/ptoken-v1/PToken.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.6.2; -import "./ERC777GSN.sol"; +import "./ERC777GSNUpgradeable.sol"; import "./ERC777WithAdminOperatorUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/access/AccessControlUpgradeable.sol"; contract PToken is Initializable, diff --git a/solidity/src/ptoken-v1/PTokenNoGSN.sol b/solidity/src/ptoken-v1/PTokenNoGSN.sol index 949ab36c..bbef3bde 100644 --- a/solidity/src/ptoken-v1/PTokenNoGSN.sol +++ b/solidity/src/ptoken-v1/PTokenNoGSN.sol @@ -2,8 +2,8 @@ pragma solidity ^0.6.2; import "./ERC777WithAdminOperatorUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/proxy/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable-v3.4.2/access/AccessControlUpgradeable.sol"; /** * @dev Note: Unfortunately we can't just refactor the original pToken to inherit most of this diff --git a/solidity/src/ptoken-v2/IFeesManager-solc-0.6.sol b/solidity/src/ptoken-v2/IFeesManager-solc-0.6.sol deleted file mode 100644 index 810b7ecc..00000000 --- a/solidity/src/ptoken-v2/IFeesManager-solc-0.6.sol +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: MIT - -pragma solidity ^0.6.2; - -/** - * @title IFeesManager - * @author pNetwork - * - */ -interface IFeesManager_solc_0_6 { - struct Fee { - uint256 minFee; - uint16 basisPoints; // 4 decimals representation i.e. 2500 => 25 basis points => 0.25% - bool defined; - } - - /** - * @dev Emitted when the max total supply changes - * - * @param maxTotalSupply The maximun total supply - */ - event MaxTotalSupplyChanged(uint256 maxTotalSupply); - - /** - * @dev Emitted when the token changes - * - * @param previousXERC20 the previous token - * @param newXERC20 the new token - */ - event TokenChanged(address previousXERC20, address newXERC20); - - /* - * Get expected fee for a swap. - * @param {address} xerc20 - The token address that will be swapped. - * @param {uint256} amount - The token amount that will be swapped. - * @param {bytes4} destinationChainId - The swap destination chain ID. - */ - function calculateFee( - address xerc20, - uint256 amount - ) external returns (uint256); - - /* - * Allows a staker to claim protocol fees for a specific token and epoch. - * @param {address} xerc20 - The token address for which fees are being claimed. - * @param {uint16} epoch - The epoch number for which fees are being claimed. - */ - function claimFeeByEpoch(address xerc20, uint16 epoch) external; - - /* - * Allows to deposit protocol fees for a certain token that will be distributed for the current epoch. - * @param {address} xerc20 - The token address for which fees are being deposited. - * @param {uint256} amount - The amount of fees being deposited for the specified token. - */ - function depositFee(address xerc20, uint256 amount) external; - - /* - * Allows to deposit protocol fees for a certain token that will be distributed for the current epoch. - * @from {address} from - The address where the tokens will be transfered from. - * @param {address} xerc20 - The token address for which fees are being deposited. - * @param {uint256} amount - The amount of fees being deposited for the specified token. - */ - function depositFeeFrom( - address from, - address xerc20, - uint256 amount - ) external; - - /* - * Allows to deposit protocol fees for a certain token that will be distributed for a specific epoch. - * @param {address} xerc20 - The token address for which fees are being deposited. - * @param {uint256} amount - The amount of fees being deposited for the specified token. - * * @param {uint16} epoch - The epoch number for which fees are being deposited. - */ - function depositFeeForEpoch( - address xerc20, - uint256 amount, - uint16 epoch - ) external; - - /* - * Allows to deposit protocol fees for a certain token that will be distributed for a specific epoch. - * @from {address} from - The address where the tokens will be transfered from. - * @param {address} xerc20 - The token address for which fees are being deposited. - * @param {uint256} amount - The amount of fees being deposited for the specified token. - * * @param {uint16} epoch - The epoch number for which fees are being deposited. - */ - function depositFeeForEpochFrom( - address from, - address xerc20, - uint256 amount, - uint16 epoch - ) external; - - /** - * Set the fees for the underlying xerc20 token - * - * @param xerc20 supported token where the fees are denominated to - * @param minAmount minimum amount of fees - * @param basisPoints basis points (4 decimals, i.e. 2 basis points => 0.2% => 2000) - */ - function setFee( - address xerc20, - uint256 minAmount, - uint16 basisPoints - ) external; -} diff --git a/solidity/src/ptoken-v2/IXERC20-solc-0.6.sol b/solidity/src/ptoken-v2/IXERC20-solc-0.6.sol deleted file mode 100644 index f2be93b4..00000000 --- a/solidity/src/ptoken-v2/IXERC20-solc-0.6.sol +++ /dev/null @@ -1,164 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.6.2; - -interface IXERC20_solc_0_6 { - /** - * @notice Emits when a lockbox is set - * - * @param _lockbox The address of the lockbox - */ - - event LockboxSet(address _lockbox); - - /** - * @notice Emits when a limit is set - * - * @param _mintingLimit The updated minting limit we are setting to the bridge - * @param _burningLimit The updated burning limit we are setting to the bridge - * @param _bridge The address of the bridge we are setting the limit too - */ - event BridgeLimitsSet( - uint256 _mintingLimit, - uint256 _burningLimit, - address indexed _bridge - ); - - /** - * @notice Contains the full minting and burning data for a particular bridge - * - * @param minterParams The minting parameters for the bridge - * @param burnerParams The burning parameters for the bridge - */ - // solhint-disable-next-line - struct Bridge { - BridgeParameters minterParams; - BridgeParameters burnerParams; - } - - /** - * @notice Contains the mint or burn parameters for a bridge - * - * @param timestamp The timestamp of the last mint/burn - * @param ratePerSecond The rate per second of the bridge - * @param maxLimit The max limit of the bridge - * @param currentLimit The current limit of the bridge - */ - // solhint-disable-next-line - struct BridgeParameters { - uint256 timestamp; - uint256 ratePerSecond; - uint256 maxLimit; - uint256 currentLimit; - } - - /** - * @notice Sets the lockbox address - * - * @param _lockbox The address of the lockbox - */ - - function setLockbox(address _lockbox) external; - - /** - * @notice Updates the limits of any bridge - * @dev Can only be called by the owner - * @param _mintingLimit The updated minting limit we are setting to the bridge - * @param _burningLimit The updated burning limit we are setting to the bridge - * @param _bridge The address of the bridge we are setting the limits too - */ - function setLimits( - address _bridge, - uint256 _mintingLimit, - uint256 _burningLimit - ) external; - - /** - * @notice Returns the max limit of a minter - * - * @param _minter The minter we are viewing the limits of - * @return _limit The limit the minter has - */ - function mintingMaxLimitOf( - address _minter - ) external view returns (uint256 _limit); - - /** - * @notice Returns the max limit of a bridge - * - * @param _bridge the bridge we are viewing the limits of - * @return _limit The limit the bridge has - */ - - function burningMaxLimitOf( - address _bridge - ) external view returns (uint256 _limit); - - /** - * @notice Returns the current limit of a minter - * - * @param _minter The minter we are viewing the limits of - * @return _limit The limit the minter has - */ - - function mintingCurrentLimitOf( - address _minter - ) external view returns (uint256 _limit); - - /** - * @notice Returns the current limit of a bridge - * - * @param _bridge the bridge we are viewing the limits of - * @return _limit The limit the bridge has - */ - - function burningCurrentLimitOf( - address _bridge - ) external view returns (uint256 _limit); - - /** - * @notice Mints tokens for a user - * @dev Can only be called by a minter - * @param _user The address of the user who needs tokens minted - * @param _amount The amount of tokens being minted - */ - - function mint(address _user, uint256 _amount) external; - - /** - * @notice Burns tokens for a user - * @dev Can only be called by a minter - * @param _user The address of the user who needs tokens burned - * @param _amount The amount of tokens being burned - */ - - function burn(address _user, uint256 _amount) external; - - // TODO: for clarity, move everything below to an interface IPTokenv2 - /** - * @notice Returns the fees manager address - */ - function getLockbox() external view returns (address); - - /** - * @notice Returns the PAM address - * - * @param adapter the relative adapter - */ - function getPAM(address adapter) external view returns (address); - - /** - * @notice Returns if this token is local or not - */ - function isLocal() external view returns (bool); - - /** - * @notice Returns the fees manager address - */ - function getFeesManager() external view returns (address); - - /** - * @notice Set the fees manager address - * @param newAddress new fees manager address - */ - function setFeesManager(address newAddress) external; -} diff --git a/solidity/src/ptoken-v2/PTokenV1NoGSNStorage.sol b/solidity/src/ptoken-v2/PTokenV1NoGSNStorage.sol new file mode 100644 index 00000000..65053221 --- /dev/null +++ b/solidity/src/ptoken-v2/PTokenV1NoGSNStorage.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +/** + * @title PTokenNoGSN.sol storage layout + * @author pNetwork Team + * @notice Should be the first to inherit from on the v2 pToken contract version. + */ +contract PTokenV1NoGSNStorage { + struct Set { + bytes32[] _values; + mapping(bytes32 => uint256) _indexes; + } + struct AddressSet { + Set _inner; + } + + struct RoleData { + AddressSet members; + bytes32 adminRole; + } + + bool internal _initialized; + bool internal _initializing; + uint256[50] private __gap0; + mapping(bytes32 => RoleData) private _roles; + uint256[49] private __gap1; + mapping(address => uint256) internal _balances; + uint256 internal _totalSupply; + string internal _name; + string internal _symbol; + address[] private _defaultOperatorsArray; + mapping(address => bool) private _defaultOperators; + mapping(address => mapping(address => bool)) private _operators; + mapping(address => mapping(address => bool)) + private _revokedDefaultOperators; + mapping(address => mapping(address => uint256)) internal _allowances; + uint256[41] private __gap4; + address private adminOperator; + bytes4 private ORIGIN_CHAIN_ID; + // This wasn't on the origin NoGSN. We expect to + // initialize this value properly in the initializeV2 + // function + address internal _owner; +} diff --git a/solidity/src/ptoken-v2/PTokenV1Storage.sol b/solidity/src/ptoken-v2/PTokenV1Storage.sol new file mode 100644 index 00000000..a651d17f --- /dev/null +++ b/solidity/src/ptoken-v2/PTokenV1Storage.sol @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +/** + * @title PToken.sol storage layout + * @author pNetwork Team + * @notice Should be the first to inherit from on the v2 pToken contract version. + */ +contract PTokenV1Storage { + struct Set { + bytes32[] _values; + mapping(bytes32 => uint256) _indexes; + } + struct AddressSet { + Set _inner; + } + + struct RoleData { + AddressSet members; + bytes32 adminRole; + } + + bool internal _initialized; + bool internal _initializing; + uint256[50] private __gap0; + mapping(bytes32 => RoleData) private _roles; + uint256[49] private __gap1; + address internal _owner; + uint256[49] private __gap2; + address private _relayHub; + uint256[49] private __gap3; + mapping(address => uint256) internal _balances; + uint256 internal _totalSupply; + string internal _name; + string internal _symbol; + address[] private _defaultOperatorsArray; + mapping(address => bool) private _defaultOperators; + mapping(address => mapping(address => bool)) private _operators; + mapping(address => mapping(address => bool)) + private _revokedDefaultOperators; + mapping(address => mapping(address => uint256)) internal _allowances; + uint256[41] private __gap4; + address private gsnTrustedSigner; + address private gsnFeeTarget; + uint256 private gsnExtraGas; + address private adminOperator; + bytes4 private ORIGIN_CHAIN_ID; +} diff --git a/solidity/src/ptoken-v2/PTokenV2.sol b/solidity/src/ptoken-v2/PTokenV2.sol index 934cacd1..8c7892de 100644 --- a/solidity/src/ptoken-v2/PTokenV2.sol +++ b/solidity/src/ptoken-v2/PTokenV2.sol @@ -1,128 +1,123 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.6.2; -pragma experimental ABIEncoderV2; - -import {IXERC20_solc_0_6 as IXERC20} from "./IXERC20-solc-0.6.sol"; -import {IFeesManager_solc_0_6 as IFeesManager} from "./IFeesManager-solc-0.6.sol"; -import "../ptoken-v1/ERC777WithAdminOperatorUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/proxy/Initializable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlUpgradeable.sol"; - -/** - * @dev Note: Unfortunately we can't just refactor the original pToken to inherit most of this - * logic, then have just the GSN version add the GSN specific logic because of the breaking - * changes it would make to the storage layout, breaking the upgradeability. So alas, we have - * this near clone to maintain instead. - */ -contract PTokenV2NoGSN is - Initializable, - AccessControlUpgradeable, - ERC777WithAdminOperatorUpgradeable, - IXERC20 +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import {IXERC20} from "../interfaces/IXERC20.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol"; +import {IERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol"; +import {IOwnable} from "../interfaces/IOwnable.sol"; +import {IFeesManager} from "../interfaces/IFeesManager.sol"; +import {IPTokenV2} from "../interfaces/IPTokenV2.sol"; +import {PTokenV1Storage} from "./PTokenV1Storage.sol"; + +import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; + +contract PTokenV2 is + PTokenV1Storage, + IERC20Upgradeable, + IOwnable, + IXERC20, + EIP712Upgradeable, + IERC20PermitUpgradeable { - // V1 - bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); - bytes4 public ORIGIN_CHAIN_ID; - - // A new initializer is required to call __Ownable_init(). - // Unfortunately, reinitialize is not available as the base Initializable contract - // comes from an old openzeppelin release. - // Thus, redefine the two fields and a new initializer2 modifier. - address private _owner; - bool private _initialized; - bool private _initializing; - - // V2 - uint256 private constant _DURATION = 1 days; - address public lockbox; - mapping(address => Bridge) public bridges; - address public feesManager; - mapping(address => address) public adapterToPAM; + /** + * @dev Used by ERC20PermitUpgradeable implementation + */ + using CountersUpgradeable for CountersUpgradeable.Counter; - event OwnershipTransferred( - address indexed previousOwner, - address indexed newOwner - ); - event PAMChanged(address newAddress); - event FeesManagerChanged(address newAddress); + /** + * @notice The duration it takes for the limits to fully replenish + */ + uint256 private constant _DURATION = 1 days; - function initialize( - string memory tokenName, - string memory tokenSymbol, - address defaultAdmin, - bytes4 originChainId - ) public initializer { - address[] memory defaultOperators; - __AccessControl_init(); - __ERC777_init(tokenName, tokenSymbol, defaultOperators); - __ERC777WithAdminOperatorUpgradeable_init(defaultAdmin); - _setupRole(DEFAULT_ADMIN_ROLE, defaultAdmin); - ORIGIN_CHAIN_ID = originChainId; - } + /** + * @notice The address of the lockbox contract + */ + address public lockbox; - function initializeV2(address owner) public initializer2 { - _owner = owner; - } + /** + * @dev Used by ERC20PermitUpgradeable implementation + */ + mapping(address => CountersUpgradeable.Counter) private _nonces; /** - * @dev Modifier to protect an initializer function from being invoked twice. + * @dev This is for the ERC20Permit implementation */ - modifier initializer2() { - require( - _initializing || !_initialized, - "Initializable: contract is already initialized" + // solhint-disable-next-line var-name-mixedcase + bytes32 private constant _PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" ); - bool isTopLevelCall = !_initializing; - if (isTopLevelCall) { - _initializing = true; - _initialized = true; - } + /** + * @notice Maps bridge address to bridge configurations + */ + mapping(address => Bridge) public bridges; + mapping(address => address) public adapterToPAM; - _; + address public feesManager; - if (isTopLevelCall) { - _initializing = false; - } + error OnlyFeesManager(); + error InsufficientAmount(); + error NotAContract(address addr); + + event FeesManagerChanged(address newAddress); + event PAMChanged(address pamAddress); + + modifier onlyContractAddress(address anAddress) { + if (anAddress.code.length == 0) revert NotAContract(anAddress); + _; } - function setFeesManager(address newAddress) public override { + modifier onlyFeesManager() { if (feesManager != address(0) && msg.sender != feesManager) - revert("OnlyFeesManager"); + revert OnlyFeesManager(); + _; + } + + function initializeV2(address owner_) public reinitializer(2) { + // Check XERC20 call + __EIP712_init(_name, "1"); + _owner = owner_; + } + /// @inheritdoc IPTokenV2 + function setFeesManager( + address newAddress + ) public onlyFeesManager onlyContractAddress(newAddress) { feesManager = newAddress; emit FeesManagerChanged(newAddress); } - /** - * @notice Set the new PAM address - * @dev Be sure the API called by the adapter is respected - * - * @param adapterAddress the adapter address - * @param pamAddress the new PAM address - */ + /// @inheritdoc IPTokenV2 function setPAM( address adapterAddress, address pamAddress - ) external onlyOwner { + ) external onlyOwner onlyContractAddress(pamAddress) { adapterToPAM[adapterAddress] = pamAddress; - PAMChanged(pamAddress); + emit PAMChanged(pamAddress); } - function isLocal() external view override returns (bool) { + /// @inheritdoc IPTokenV2 + function isLocal() external view returns (bool) { return (lockbox != address(0)); } - function getPAM(address adapter) external view override returns (address) { + /// @inheritdoc IPTokenV2 + function getPAM(address adapter) external view returns (address) { return adapterToPAM[adapter]; } - function getFeesManager() external view override returns (address) { + /// @inheritdoc IPTokenV2 + function getFeesManager() external view returns (address) { return feesManager; } - function getLockbox() external view override returns (address) { + /// @inheritdoc IPTokenV2 + function getLockbox() external view returns (address) { return lockbox; } @@ -133,7 +128,7 @@ contract PTokenV2NoGSN is * @param _amount The amount of tokens being minted */ - function mint(address _user, uint256 _amount) external override { + function mint(address _user, uint256 _amount) external { _mintWithCaller(msg.sender, _user, _amount); } @@ -144,7 +139,7 @@ contract PTokenV2NoGSN is * @param _amount The amount of tokens being burned */ - function burn(address _user, uint256 _amount) external override { + function burn(address _user, uint256 _amount) external { if (msg.sender != _user) { _spendAllowance(_user, msg.sender, _amount); } @@ -158,7 +153,7 @@ contract PTokenV2NoGSN is * @param _lockbox The address of the lockbox */ - function setLockbox(address _lockbox) external override onlyOwner { + function setLockbox(address _lockbox) public onlyOwner { // if (msg.sender != FACTORY) revert IXERC20_NotFactory(); lockbox = _lockbox; @@ -176,7 +171,7 @@ contract PTokenV2NoGSN is address _bridge, uint256 _mintingLimit, uint256 _burningLimit - ) external override onlyOwner { + ) external onlyOwner { _changeMinterLimit(_bridge, _mintingLimit); _changeBurnerLimit(_bridge, _burningLimit); emit BridgeLimitsSet(_mintingLimit, _burningLimit, _bridge); @@ -191,7 +186,7 @@ contract PTokenV2NoGSN is function mintingMaxLimitOf( address _bridge - ) public view override returns (uint256 _limit) { + ) public view returns (uint256 _limit) { _limit = bridges[_bridge].minterParams.maxLimit; } @@ -204,7 +199,7 @@ contract PTokenV2NoGSN is function burningMaxLimitOf( address _bridge - ) public view override returns (uint256 _limit) { + ) public view returns (uint256 _limit) { _limit = bridges[_bridge].burnerParams.maxLimit; } @@ -217,7 +212,7 @@ contract PTokenV2NoGSN is function mintingCurrentLimitOf( address _bridge - ) public view override returns (uint256 _limit) { + ) public view returns (uint256 _limit) { _limit = _getCurrentLimit( bridges[_bridge].minterParams.currentLimit, bridges[_bridge].minterParams.maxLimit, @@ -235,7 +230,7 @@ contract PTokenV2NoGSN is function burningCurrentLimitOf( address _bridge - ) public view override returns (uint256 _limit) { + ) public view returns (uint256 _limit) { _limit = _getCurrentLimit( bridges[_bridge].burnerParams.currentLimit, bridges[_bridge].burnerParams.maxLimit, @@ -383,7 +378,7 @@ contract PTokenV2NoGSN is uint256 _amount ) internal { uint256 fees; - // is local? + // We don't make an internal call to isLocal() in order to save gas if (lockbox != address(0) && feesManager != address(0)) { fees = IFeesManager(feesManager).calculateFee( address(this), @@ -391,14 +386,13 @@ contract PTokenV2NoGSN is ); } - if (fees > _amount) revert("InsufficientAmount"); + if (fees > _amount) revert InsufficientAmount(); uint256 netAmount = _amount - fees; if (_caller != lockbox) { uint256 _currentLimit = burningCurrentLimitOf(_caller); - if (_currentLimit < netAmount) - revert("IXERC20_NotHighEnoughLimits"); + if (_currentLimit < netAmount) revert IXERC20_NotHighEnoughLimits(); _useBurnerLimits(_caller, netAmount); } @@ -410,7 +404,7 @@ contract PTokenV2NoGSN is ); } - _burn(_user, netAmount, "", ""); + _burn(_user, netAmount); } /** @@ -428,62 +422,249 @@ contract PTokenV2NoGSN is ) internal { if (_caller != lockbox) { uint256 _currentLimit = mintingCurrentLimitOf(_caller); - if (_currentLimit < _amount) revert("IXERC20_NotHighEnoughLimits"); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); _useMinterLimits(_caller, _amount); } - _mint(_user, _amount, "", ""); + _mint(_user, _amount); } - // V1 - function grantMinterRole(address _account) external { - grantRole(MINTER_ROLE, _account); + /* solhint-disable */ + /////////////////// Copied from ERC20Upgradable.sol - OpenZeppelin Upgradeable Contracts v4.9.6 + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; } - function revokeMinterRole(address _account) external { - revokeRole(MINTER_ROLE, _account); + function balanceOf( + address account + ) public view virtual override returns (uint256) { + return _balances[account]; } - function hasMinterRole(address _account) external view returns (bool) { - return hasRole(MINTER_ROLE, _account); + function transfer( + address to, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; } - /** - * @dev Returns the address of the current owner. - */ - function owner() public view virtual returns (address) { - return _owner; + function allowance( + address owner, + address spender + ) public view virtual override returns (uint256) { + return _allowances[owner][spender]; } - /** - * @dev Throws if called by any account other than the owner. - */ + function approve( + address spender, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require( + fromBalance >= amount, + "ERC20: transfer amount exceeds balance" + ); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require( + currentAllowance >= amount, + "ERC20: insufficient allowance" + ); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /////////////////// Copied from ERC20PermitUpgradeable - OpenZeppelin v4.9.6 + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + bytes32 structHash = keccak256( + abi.encode( + _PERMIT_TYPEHASH, + owner, + spender, + value, + _useNonce(owner), + deadline + ) + ); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSAUpgradeable.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _approve(owner, spender, value); + } + + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + function nonces( + address owner + ) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + function _useNonce( + address owner + ) internal virtual returns (uint256 current) { + CountersUpgradeable.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /////////////////// Copied from OwnableUpgradeable - OpenZeppelin v4.9.6 modifier onlyOwner() { - require(owner() == _msgSender(), "Ownable: caller is not the owner"); + _checkOwner(); _; } - /** - * @dev Leaves the contract without owner. It will not be possible to call - * `onlyOwner` functions anymore. Can only be called by the current owner. - * - * NOTE: Renouncing ownership will leave the contract without an owner, - * thereby removing any functionality that is only available to the owner. - */ - function renounceOwnership() external virtual onlyOwner { - emit OwnershipTransferred(_owner, address(0)); - _owner = address(0); + function owner() public view returns (address) { + return _owner; } - /** - * @dev Transfers ownership of the contract to a new account (`newOwner`). - * Can only be called by the current owner. - */ - function transferOwnership(address newOwner) external virtual onlyOwner { + function _checkOwner() internal view { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + function renounceOwnership() public onlyOwner { + _transferOwnership(address(0)); + } + + function transferOwnership(address newOwner) public onlyOwner { require( newOwner != address(0), "Ownable: new owner is the zero address" ); - emit OwnershipTransferred(_owner, newOwner); + _transferOwnership(newOwner); + } + + function _transferOwnership(address newOwner) internal { + address oldOwner = _owner; _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + + /////////////////// Copied from ContextUpgradeable.sol - OpenZeppelin Upgradeable Contracts v4.9.6 + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; } + /* solhint-enable */ } diff --git a/solidity/src/ptoken-v2/PTokenV2NoGSN.sol b/solidity/src/ptoken-v2/PTokenV2NoGSN.sol new file mode 100644 index 00000000..181a7b11 --- /dev/null +++ b/solidity/src/ptoken-v2/PTokenV2NoGSN.sol @@ -0,0 +1,670 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity >=0.8.4 <0.9.0; + +import {IXERC20} from "../interfaces/IXERC20.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol"; +import {IERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/extensions/IERC20PermitUpgradeable.sol"; +import {IOwnable} from "../interfaces/IOwnable.sol"; +import {IFeesManager} from "../interfaces/IFeesManager.sol"; +import {IPTokenV2} from "../interfaces/IPTokenV2.sol"; +import {PTokenV1NoGSNStorage} from "./PTokenV1NoGSNStorage.sol"; + +import "@openzeppelin/contracts-upgradeable/utils/cryptography/ECDSAUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/cryptography/EIP712Upgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol"; + +contract PTokenV2NoGSN is + PTokenV1NoGSNStorage, + IERC20Upgradeable, + IOwnable, + IXERC20, + EIP712Upgradeable, + IERC20PermitUpgradeable +{ + /** + * @dev Used by ERC20PermitUpgradeable implementation + */ + using CountersUpgradeable for CountersUpgradeable.Counter; + + /** + * @notice The duration it takes for the limits to fully replenish + */ + uint256 private constant _DURATION = 1 days; + + /** + * @notice The address of the lockbox contract + */ + address public lockbox; + + /** + * @dev Used by ERC20PermitUpgradeable implementation + */ + mapping(address => CountersUpgradeable.Counter) private _nonces; + + /** + * @dev This is for the ERC20Permit implementation + */ + // solhint-disable-next-line var-name-mixedcase + bytes32 private constant _PERMIT_TYPEHASH = + keccak256( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" + ); + + /** + * @notice Maps bridge address to bridge configurations + */ + mapping(address => Bridge) public bridges; + mapping(address => address) public adapterToPAM; + + address public feesManager; + + error OnlyFeesManager(); + error InsufficientAmount(); + error NotAContract(address addr); + + event FeesManagerChanged(address newAddress); + event PAMChanged(address pamAddress); + + modifier onlyContractAddress(address anAddress) { + if (anAddress.code.length == 0) revert NotAContract(anAddress); + _; + } + + modifier onlyFeesManager() { + if (feesManager != address(0) && msg.sender != feesManager) + revert OnlyFeesManager(); + _; + } + + function initializeV2(address owner_) public reinitializer(2) { + // Check XERC20 call + __EIP712_init(_name, "1"); + _owner = owner_; + } + + /// @inheritdoc IPTokenV2 + function setFeesManager( + address newAddress + ) public onlyFeesManager onlyContractAddress(newAddress) { + feesManager = newAddress; + + emit FeesManagerChanged(newAddress); + } + + /// @inheritdoc IPTokenV2 + function setPAM( + address adapterAddress, + address pamAddress + ) external onlyOwner onlyContractAddress(pamAddress) { + adapterToPAM[adapterAddress] = pamAddress; + emit PAMChanged(pamAddress); + } + + /// @inheritdoc IPTokenV2 + function isLocal() external view returns (bool) { + return (lockbox != address(0)); + } + + /// @inheritdoc IPTokenV2 + function getPAM(address adapter) external view returns (address) { + return adapterToPAM[adapter]; + } + + /// @inheritdoc IPTokenV2 + function getFeesManager() external view returns (address) { + return feesManager; + } + + /// @inheritdoc IPTokenV2 + function getLockbox() external view returns (address) { + return lockbox; + } + + /** + * @notice Mints tokens for a user + * @dev Can only be called by a bridge + * @param _user The address of the user who needs tokens minted + * @param _amount The amount of tokens being minted + */ + + function mint(address _user, uint256 _amount) external { + _mintWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Burns tokens for a user + * @dev Can only be called by a bridge + * @param _user The address of the user who needs tokens burned + * @param _amount The amount of tokens being burned + */ + + function burn(address _user, uint256 _amount) external { + if (msg.sender != _user) { + _spendAllowance(_user, msg.sender, _amount); + } + + _burnWithCaller(msg.sender, _user, _amount); + } + + /** + * @notice Sets the lockbox address + * + * @param _lockbox The address of the lockbox + */ + + function setLockbox(address _lockbox) public onlyOwner { + // if (msg.sender != FACTORY) revert IXERC20_NotFactory(); + lockbox = _lockbox; + + emit LockboxSet(_lockbox); + } + + /** + * @notice Updates the limits of any bridge + * @dev Can only be called by the owner + * @param _mintingLimit The updated minting limit we are setting to the bridge + * @param _burningLimit The updated burning limit we are setting to the bridge + * @param _bridge The address of the bridge we are setting the limits too + */ + function setLimits( + address _bridge, + uint256 _mintingLimit, + uint256 _burningLimit + ) external onlyOwner { + _changeMinterLimit(_bridge, _mintingLimit); + _changeBurnerLimit(_bridge, _burningLimit); + emit BridgeLimitsSet(_mintingLimit, _burningLimit, _bridge); + } + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + + function mintingMaxLimitOf( + address _bridge + ) public view returns (uint256 _limit) { + _limit = bridges[_bridge].minterParams.maxLimit; + } + + /** + * @notice Returns the max limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + + function burningMaxLimitOf( + address _bridge + ) public view returns (uint256 _limit) { + _limit = bridges[_bridge].burnerParams.maxLimit; + } + + /** + * @notice Returns the current limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + + function mintingCurrentLimitOf( + address _bridge + ) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].minterParams.currentLimit, + bridges[_bridge].minterParams.maxLimit, + bridges[_bridge].minterParams.timestamp, + bridges[_bridge].minterParams.ratePerSecond + ); + } + + /** + * @notice Returns the current limit of a bridge + * + * @param _bridge the bridge we are viewing the limits of + * @return _limit The limit the bridge has + */ + + function burningCurrentLimitOf( + address _bridge + ) public view returns (uint256 _limit) { + _limit = _getCurrentLimit( + bridges[_bridge].burnerParams.currentLimit, + bridges[_bridge].burnerParams.maxLimit, + bridges[_bridge].burnerParams.timestamp, + bridges[_bridge].burnerParams.ratePerSecond + ); + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + + function _useMinterLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.timestamp = block.timestamp; + bridges[_bridge].minterParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Uses the limit of any bridge + * @param _bridge The address of the bridge who is being changed + * @param _change The change in the limit + */ + + function _useBurnerLimits(address _bridge, uint256 _change) internal { + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.timestamp = block.timestamp; + bridges[_bridge].burnerParams.currentLimit = _currentLimit - _change; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + + function _changeMinterLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].minterParams.maxLimit; + uint256 _currentLimit = mintingCurrentLimitOf(_bridge); + bridges[_bridge].minterParams.maxLimit = _limit; + + bridges[_bridge].minterParams.currentLimit = _calculateNewCurrentLimit( + _limit, + _oldLimit, + _currentLimit + ); + + bridges[_bridge].minterParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].minterParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the limit of any bridge + * @dev Can only be called by the owner + * @param _bridge The address of the bridge we are setting the limit too + * @param _limit The updated limit we are setting to the bridge + */ + + function _changeBurnerLimit(address _bridge, uint256 _limit) internal { + uint256 _oldLimit = bridges[_bridge].burnerParams.maxLimit; + uint256 _currentLimit = burningCurrentLimitOf(_bridge); + bridges[_bridge].burnerParams.maxLimit = _limit; + + bridges[_bridge].burnerParams.currentLimit = _calculateNewCurrentLimit( + _limit, + _oldLimit, + _currentLimit + ); + + bridges[_bridge].burnerParams.ratePerSecond = _limit / _DURATION; + bridges[_bridge].burnerParams.timestamp = block.timestamp; + } + + /** + * @notice Updates the current limit + * + * @param _limit The new limit + * @param _oldLimit The old limit + * @param _currentLimit The current limit + * @return _newCurrentLimit The new current limit + */ + + function _calculateNewCurrentLimit( + uint256 _limit, + uint256 _oldLimit, + uint256 _currentLimit + ) internal pure returns (uint256 _newCurrentLimit) { + uint256 _difference; + + if (_oldLimit > _limit) { + _difference = _oldLimit - _limit; + _newCurrentLimit = _currentLimit > _difference + ? _currentLimit - _difference + : 0; + } else { + _difference = _limit - _oldLimit; + _newCurrentLimit = _currentLimit + _difference; + } + } + + /** + * @notice Gets the current limit + * + * @param _currentLimit The current limit + * @param _maxLimit The max limit + * @param _timestamp The timestamp of the last update + * @param _ratePerSecond The rate per second + * @return _limit The current limit + */ + + function _getCurrentLimit( + uint256 _currentLimit, + uint256 _maxLimit, + uint256 _timestamp, + uint256 _ratePerSecond + ) internal view returns (uint256 _limit) { + _limit = _currentLimit; + if (_limit == _maxLimit) { + return _limit; + } else if (_timestamp + _DURATION <= block.timestamp) { + _limit = _maxLimit; + } else if (_timestamp + _DURATION > block.timestamp) { + uint256 _timePassed = block.timestamp - _timestamp; + uint256 _calculatedLimit = _limit + (_timePassed * _ratePerSecond); + _limit = _calculatedLimit > _maxLimit + ? _maxLimit + : _calculatedLimit; + } + } + + /** + * @notice Internal function for burning tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to burn + */ + + function _burnWithCaller( + address _caller, + address _user, + uint256 _amount + ) internal { + uint256 fees; + // We don't make an internal call to isLocal() in order to save gas + if (lockbox != address(0) && feesManager != address(0)) { + fees = IFeesManager(feesManager).calculateFee( + address(this), + _amount + ); + } + + if (fees > _amount) revert InsufficientAmount(); + + uint256 netAmount = _amount - fees; + + if (_caller != lockbox) { + uint256 _currentLimit = burningCurrentLimitOf(_caller); + if (_currentLimit < netAmount) revert IXERC20_NotHighEnoughLimits(); + _useBurnerLimits(_caller, netAmount); + } + + if (fees > 0) { + IFeesManager(feesManager).depositFeeFrom( + msg.sender, + address(this), + fees + ); + } + + _burn(_user, netAmount); + } + + /** + * @notice Internal function for minting tokens + * + * @param _caller The caller address + * @param _user The user address + * @param _amount The amount to mint + */ + + function _mintWithCaller( + address _caller, + address _user, + uint256 _amount + ) internal { + if (_caller != lockbox) { + uint256 _currentLimit = mintingCurrentLimitOf(_caller); + if (_currentLimit < _amount) revert IXERC20_NotHighEnoughLimits(); + _useMinterLimits(_caller, _amount); + } + _mint(_user, _amount); + } + + /* solhint-disable */ + /////////////////// Copied from ERC20Upgradable.sol - OpenZeppelin Upgradeable Contracts v4.9.6 + function totalSupply() public view virtual override returns (uint256) { + return _totalSupply; + } + + function balanceOf( + address account + ) public view virtual override returns (uint256) { + return _balances[account]; + } + + function transfer( + address to, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _transfer(owner, to, amount); + return true; + } + + function allowance( + address owner, + address spender + ) public view virtual override returns (uint256) { + return _allowances[owner][spender]; + } + + function approve( + address spender, + uint256 amount + ) public virtual override returns (bool) { + address owner = _msgSender(); + _approve(owner, spender, amount); + return true; + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public virtual override returns (bool) { + address spender = _msgSender(); + _spendAllowance(from, spender, amount); + _transfer(from, to, amount); + return true; + } + + function _transfer( + address from, + address to, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC20: transfer from the zero address"); + require(to != address(0), "ERC20: transfer to the zero address"); + + _beforeTokenTransfer(from, to, amount); + + uint256 fromBalance = _balances[from]; + require( + fromBalance >= amount, + "ERC20: transfer amount exceeds balance" + ); + unchecked { + _balances[from] = fromBalance - amount; + // Overflow not possible: the sum of all balances is capped by totalSupply, and the sum is preserved by + // decrementing then incrementing. + _balances[to] += amount; + } + + emit Transfer(from, to, amount); + + _afterTokenTransfer(from, to, amount); + } + + function _mint(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: mint to the zero address"); + + _beforeTokenTransfer(address(0), account, amount); + + _totalSupply += amount; + unchecked { + // Overflow not possible: balance + amount is at most totalSupply + amount, which is checked above. + _balances[account] += amount; + } + emit Transfer(address(0), account, amount); + + _afterTokenTransfer(address(0), account, amount); + } + + function _burn(address account, uint256 amount) internal virtual { + require(account != address(0), "ERC20: burn from the zero address"); + + _beforeTokenTransfer(account, address(0), amount); + + uint256 accountBalance = _balances[account]; + require(accountBalance >= amount, "ERC20: burn amount exceeds balance"); + unchecked { + _balances[account] = accountBalance - amount; + // Overflow not possible: amount <= accountBalance <= totalSupply. + _totalSupply -= amount; + } + + emit Transfer(account, address(0), amount); + + _afterTokenTransfer(account, address(0), amount); + } + + function _approve( + address owner, + address spender, + uint256 amount + ) internal virtual { + require(owner != address(0), "ERC20: approve from the zero address"); + require(spender != address(0), "ERC20: approve to the zero address"); + + _allowances[owner][spender] = amount; + emit Approval(owner, spender, amount); + } + + function _spendAllowance( + address owner, + address spender, + uint256 amount + ) internal virtual { + uint256 currentAllowance = allowance(owner, spender); + if (currentAllowance != type(uint256).max) { + require( + currentAllowance >= amount, + "ERC20: insufficient allowance" + ); + unchecked { + _approve(owner, spender, currentAllowance - amount); + } + } + } + + function _beforeTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + function _afterTokenTransfer( + address from, + address to, + uint256 amount + ) internal virtual {} + + /////////////////// Copied from ERC20PermitUpgradeable - OpenZeppelin v4.9.6 + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + bytes32 structHash = keccak256( + abi.encode( + _PERMIT_TYPEHASH, + owner, + spender, + value, + _useNonce(owner), + deadline + ) + ); + + bytes32 hash = _hashTypedDataV4(structHash); + + address signer = ECDSAUpgradeable.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + _approve(owner, spender, value); + } + + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + function nonces( + address owner + ) public view virtual override returns (uint256) { + return _nonces[owner].current(); + } + + function _useNonce( + address owner + ) internal virtual returns (uint256 current) { + CountersUpgradeable.Counter storage nonce = _nonces[owner]; + current = nonce.current(); + nonce.increment(); + } + + /////////////////// Copied from OwnableUpgradeable - OpenZeppelin v4.9.6 + modifier onlyOwner() { + _checkOwner(); + _; + } + + function owner() public view returns (address) { + return _owner; + } + + function _checkOwner() internal view { + require(owner() == _msgSender(), "Ownable: caller is not the owner"); + } + + function renounceOwnership() public onlyOwner { + _transferOwnership(address(0)); + } + + function transferOwnership(address newOwner) public onlyOwner { + require( + newOwner != address(0), + "Ownable: new owner is the zero address" + ); + _transferOwnership(newOwner); + } + + function _transferOwnership(address newOwner) internal { + address oldOwner = _owner; + _owner = newOwner; + emit OwnershipTransferred(oldOwner, newOwner); + } + + /////////////////// Copied from ContextUpgradeable.sol - OpenZeppelin Upgradeable Contracts v4.9.6 + function _msgSender() internal view virtual returns (address) { + return msg.sender; + } + + function _msgData() internal view virtual returns (bytes calldata) { + return msg.data; + } + /* solhint-enable */ +} diff --git a/solidity/src/xerc20/XERC20.sol b/solidity/src/xerc20/XERC20.sol index 50e7e090..b1b01c38 100644 --- a/solidity/src/xerc20/XERC20.sol +++ b/solidity/src/xerc20/XERC20.sol @@ -6,6 +6,7 @@ import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import {ERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/ERC20Permit.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import {IFeesManager} from "../interfaces/IFeesManager.sol"; +import {IPTokenV2} from "../interfaces/IPTokenV2.sol"; contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit { /** @@ -63,7 +64,7 @@ contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit { _; } - /// @inheritdoc IXERC20 + /// @inheritdoc IPTokenV2 function setFeesManager( address newAddress ) public onlyFeesManager onlyContractAddress(newAddress) { @@ -72,13 +73,7 @@ contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit { emit FeesManagerChanged(newAddress); } - /** - * @notice Set the new PAM address - * @dev Be sure the API called by the adapter is respected - * - * @param adapterAddress the adapter address - * @param pamAddress the new PAM address - */ + /// @inheritdoc IPTokenV2 function setPAM( address adapterAddress, address pamAddress @@ -87,22 +82,22 @@ contract XERC20 is ERC20, Ownable, IXERC20, ERC20Permit { emit PAMChanged(pamAddress); } - /// @inheritdoc IXERC20 + /// @inheritdoc IPTokenV2 function isLocal() external view returns (bool) { return (lockbox != address(0)); } - /// @inheritdoc IXERC20 + /// @inheritdoc IPTokenV2 function getPAM(address adapter) external view returns (address) { return adapterToPAM[adapter]; } - /// @inheritdoc IXERC20 + /// @inheritdoc IPTokenV2 function getFeesManager() external view returns (address) { return feesManager; } - /// @inheritdoc IXERC20 + /// @inheritdoc IPTokenV2 function getLockbox() external view returns (address) { return lockbox; } diff --git a/solidity/test/hardhat/Adapter.test.js b/solidity/test/hardhat/Adapter.test.js index 52742479..ced07eef 100644 --- a/solidity/test/hardhat/Adapter.test.js +++ b/solidity/test/hardhat/Adapter.test.js @@ -21,7 +21,7 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) describe(`Adapter ${_useGSN} Test Units`, () => { ;['', 'Native'].map(_isNative => { const setup = async () => { - const [owner, minter, recipient, user, evil] = + const [owner, admin, minter, recipient, user, evil] = await hre.ethers.getSigners() const name = 'Token A' const symbol = 'TKN A' @@ -34,7 +34,7 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) await deployERC1820() - const pToken = await deployProxy(hre, `PToken${_useGSN}`, [ + const pToken = await deployProxy(hre, `PToken${_useGSN}`, admin, [ `p${name}`, `p${symbol}`, owner.address, @@ -47,8 +47,8 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) pToken, `PTokenV2${_useGSN}`, opts, + admin, ) - const isNative = _isNative == 'Native' const firstEpoch = 0 const nodes = [] @@ -93,9 +93,11 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) ) await feesManager.setFee(pTokenV2, minFee, basisPoints) await pTokenV2.setFeesManager(feesManager) - await pTokenV2.setLockbox(lockbox) - await pTokenV2.setLimits(adapter, mintingLimit, burningLimit) - await pTokenV2.setPAM(adapter, PAM) + await pTokenV2.connect(owner).setLockbox(lockbox) + await pTokenV2 + .connect(owner) + .setLimits(adapter, mintingLimit, burningLimit) + await pTokenV2.connect(owner).setPAM(adapter, PAM) if (!_isNative) await erc20.connect(owner).transfer(user, 10000) diff --git a/solidity/test/hardhat/PTokenV2.test.js b/solidity/test/hardhat/PTokenV2.test.js index ce1be4f3..04e451fa 100644 --- a/solidity/test/hardhat/PTokenV2.test.js +++ b/solidity/test/hardhat/PTokenV2.test.js @@ -1,5 +1,4 @@ import helpers, { loadFixture } from '@nomicfoundation/hardhat-network-helpers' -import assert from 'assert' import { expect } from 'chai' import { ZeroAddress } from 'ethers/constants' import hre from 'hardhat' @@ -14,60 +13,32 @@ import { validateUpgrade } from './utils/validate-upgrade.cjs' const ERC1820 = '0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24' const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) -;['XERC20', 'PTokenV2NoGSN', 'PTokenV2'].map(_tokenKind => { - describe(`${_tokenKind}`, () => { - const _useGSN = _tokenKind.includes('NoGSN') ? 'NoGSN' : '' +;['', 'NoGSN'].map(_useGSN => { + describe(`Testing upgrade from 'PToken${_useGSN}'`, () => { + describe('Storage Layout invariance checks', () => { + const name = 'pToken A' + const symbol = 'pTKN A' + const originChainId = '0x10000000' - if (_tokenKind.includes('PToken')) { - describe('Storage Layout invariance checks', () => { - const name = 'pToken A' - const symbol = 'pTKN A' - const originChainId = '0x10000000' - - it('Should not detect any storage violation', async () => { - // Set the registry - await deployERC1820() - - const [_, admin] = await hre.ethers.getSigners() - const pToken = await deployProxy(hre, `PToken${_useGSN}`, [ - name, - symbol, - admin.address, - originChainId, - ]) - - expect( - await validateUpgrade(hre, `PTokenV2${_useGSN}`, pToken.target), - ) - }) - - it('Should not be possible to upgrade from GSN to non-GSN and viceversa', async () => { - // Set the registry - await deployERC1820() - - const [_, admin] = await hre.ethers.getSigners() - const pToken = await deployProxy(hre, `PToken${_useGSN}`, [ - name, - symbol, - admin.address, - originChainId, - ]) + it('Should not detect any storage violation', async () => { + // Set the registry + await deployERC1820() - const notUseGSN = _useGSN === '' ? 'NoGSN' : '' + const [_, admin] = await hre.ethers.getSigners() + const pToken = await deployProxy(hre, `PToken${_useGSN}`, admin, [ + name, + symbol, + admin.address, + originChainId, + ]) - try { - await validateUpgrade(hre, `PTokenV2${notUseGSN}`, pToken.target), - assert.fail('Should never reach here') - } catch (e) { - expect(e.message).to.include('New storage layout is incompatible') - } - }) + expect(await validateUpgrade(hre, `PTokenV2${_useGSN}`, pToken.target)) }) - } + }) - describe.only('Tests units', () => { + describe('Tests units', () => { const setup = async () => { - const [owner, minter, recipient, user, evil, bridge] = + const [owner, admin, minter, recipient, user, evil, bridge] = await hre.ethers.getSigners() const name = 'pToken A' const symbol = 'pTKN A' @@ -75,60 +46,56 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) await deployERC1820() - const pToken = - _tokenKind == 'XERC20' - ? await deploy(hre, 'XERC20', [name, symbol, ZeroAddress]) - : await deployProxy(hre, `PToken${_useGSN}`, [ - name, - symbol, - owner.address, - originChainId, - ]) - - return { owner, minter, recipient, user, evil, bridge, pToken } + const pToken = await deployProxy(hre, `PToken${_useGSN}`, admin, [ + name, + symbol, + owner.address, + originChainId, + ]) + + return { owner, admin, minter, recipient, user, evil, bridge, pToken } } - if (_tokenKind.includes('PToken')) { - it('Should mint some pTokens before upgrade', async () => { - const { owner, minter, recipient, pToken } = await loadFixture(setup) + it('Should mint some pTokens before upgrade', async () => { + const { owner, minter, recipient, pToken } = await loadFixture(setup) - const value = 100 - await expect(pToken.connect(owner).grantMinterRole(minter)).to.emit( - pToken, - 'RoleGranted', - ) - await expect(pToken.connect(minter).mint(recipient, value)) - .to.emit(pToken, 'Transfer') - .withArgs(ZeroAddress, recipient.address, value) + const value = 100 + await expect(pToken.connect(owner).grantMinterRole(minter)).to.emit( + pToken, + 'RoleGranted', + ) + await expect(pToken.connect(minter).mint(recipient, value)) + .to.emit(pToken, 'Transfer') + .withArgs(ZeroAddress, recipient.address, value) - expect(await pToken.balanceOf(recipient)).to.be.equal(value) - }) + expect(await pToken.balanceOf(recipient)).to.be.equal(value) + }) - it('Should upgrade the ptoken correctly', async () => { - const { owner, minter, recipient, pToken } = await loadFixture(setup) - const value = 100 - await pToken.connect(owner).grantMinterRole(minter) - await pToken.connect(minter).mint(recipient, value) + it('Should upgrade the ptoken correctly', async () => { + const { owner, admin, minter, recipient, pToken } = + await loadFixture(setup) + const value = 100 + await pToken.connect(owner).grantMinterRole(minter) + await pToken.connect(minter).mint(recipient, value) - const opts = getUpgradeOpts(owner, _useGSN) + expect(await pToken.balanceOf(recipient)).to.be.equal(value) - const pTokenV2 = await upgradeProxy( - hre, - pToken, - `PTokenV2${_useGSN}`, - opts, - ) + const opts = getUpgradeOpts(owner) + const pTokenV2 = await upgradeProxy( + hre, + pToken, + `PTokenV2${_useGSN}`, + opts, + admin, + ) - expect(await pTokenV2.balanceOf(recipient)).to.be.equal(100) - }) - } + expect(await pTokenV2.balanceOf(recipient)).to.be.equal(value) + }) - describe('Cumulative tests after pToken contract upgrade or when using XERC20', () => { + describe('Cumulative tests after pToken contract upgrade', () => { let owner, admin, user, - minter, - recipient, evil, PAM, bridge, @@ -147,22 +114,55 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) const env = await loadFixture(setup) owner = env.owner admin = env.admin - minter = env.minter - recipient = env.recipient user = env.user evil = env.evil bridge = env.bridge pToken = env.pToken - const opts = getUpgradeOpts(owner, _useGSN) - - pTokenV2 = _tokenKind.includes('PToken') - ? await upgradeProxy(hre, pToken, `PTokenV2${_useGSN}`, opts) - : pToken + const opts = getUpgradeOpts(owner) + pTokenV2 = await upgradeProxy( + hre, + pToken, + `PTokenV2${_useGSN}`, + opts, + admin, + ) expect(await pTokenV2.owner()).to.be.equal(owner.address) }) + it('Should revert when trying to call initializeV2', async () => { + const initError = 'Initializable: contract is already initialized' + await expect( + pTokenV2.connect(admin).initializeV2(evil), + ).to.be.revertedWith(initError) + + await expect( + pTokenV2.connect(owner).initializeV2(evil), + ).to.be.revertedWith(initError) + + await expect( + pTokenV2.connect(evil).initializeV2(evil), + ).to.be.revertedWith(initError) + }) + + it('Only the admin can upgrade the contract', async () => { + const snapshot = await helpers.takeSnapshot() + const opts = {} + await expect( + upgradeProxy(hre, pTokenV2, `PTokenV2${_useGSN}`, opts, owner), + ).to.be.reverted + + await expect( + upgradeProxy(hre, pTokenV2, `PTokenV2${_useGSN}`, opts, evil), + ).to.be.reverted + + await expect( + upgradeProxy(hre, pTokenV2, `PTokenV2${_useGSN}`, opts, admin), + ).to.not.be.reverted + await snapshot.restore() + }) + it('Anyone can set the fee manager the first time', async () => { feesManagerTest = await deploy(hre, 'FeesManagerTest') @@ -180,14 +180,10 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) const tx = pTokenV2.connect(owner).setFeesManager(feesManagerTest) - if (_tokenKind === 'XERC20') { - await expect(tx).to.be.revertedWithCustomError( - pTokenV2, - 'OnlyFeesManager', - ) - } else { - await expect(tx).to.be.revertedWith('OnlyFeesManager') - } + await expect(tx).to.be.revertedWithCustomError( + pTokenV2, + 'OnlyFeesManager', + ) await expect( oldFeesManager.setFeesManagerForXERC20(pTokenV2, feesManagerTest), @@ -203,16 +199,9 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) .connect(evil) .setLimits(bridge, mintingLimit, burningLimit) - if (_tokenKind === 'XERC20') { - await expect(tx).to.be.revertedWithCustomError( - pTokenV2, - 'OwnableUnauthorizedAccount', - ) - } else { - await expect(tx).to.be.revertedWith( - 'Ownable: caller is not the owner', - ) - } + await expect(tx).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) await expect( pTokenV2 @@ -251,16 +240,8 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) it('Should get the correct bridge params after minting and burning', async () => { const amount = 100n await pTokenV2.connect(bridge).mint(user, amount) - await pTokenV2.connect(user).approve(bridge, amount) - - if (_tokenKind === 'XERC20') { - await pTokenV2.connect(bridge).burn(user, amount) - } else { - await pTokenV2 - .connect(bridge) - ['burn(address,uint256)'](user, amount) - } + await pTokenV2.connect(bridge).burn(user, amount) const bridgeParams = await pTokenV2.bridges(bridge) @@ -279,18 +260,9 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) it('Only the owner can set the PAM address', async () => { PAM = await deploy(hre, 'PAM') - const tx = pTokenV2.connect(evil).setPAM(bridge, PAM) - - if (_tokenKind === 'XERC20') { - await expect(tx).to.be.revertedWithCustomError( - pTokenV2, - 'OwnableUnauthorizedAccount', - ) - } else { - await expect(tx).to.be.revertedWith( - 'Ownable: caller is not the owner', - ) - } + await expect( + pTokenV2.connect(evil).setPAM(bridge, PAM), + ).to.be.revertedWith('Ownable: caller is not the owner') await expect(pTokenV2.connect(owner).setPAM(bridge, PAM)) .to.emit(pTokenV2, 'PAMChanged') @@ -312,16 +284,9 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) ]) const tx = pTokenV2.connect(evil).setLockbox(lockbox) - if (_tokenKind === 'XERC20') { - await expect(tx).to.be.revertedWithCustomError( - pTokenV2, - 'OwnableUnauthorizedAccount', - ) - } else { - await expect(tx).to.be.revertedWith( - 'Ownable: caller is not the owner', - ) - } + await expect(tx).to.be.revertedWith( + 'Ownable: caller is not the owner', + ) await expect(pTokenV2.connect(owner).setLockbox(lockbox)) .to.emit(pTokenV2, 'LockboxSet') @@ -348,18 +313,12 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) const remainingMintAmount = currentMintingLimit + seconds * mintingRatePerSecond + 1n - if (_tokenKind === 'XERC20') { - await expect( - pTokenV2.connect(bridge).mint(user, remainingMintAmount), - ).to.be.revertedWithCustomError( - pTokenV2, - 'IXERC20_NotHighEnoughLimits', - ) - } else { - await expect( - pTokenV2.connect(bridge).mint(user, remainingMintAmount), - ).to.be.revertedWith('IXERC20_NotHighEnoughLimits') - } + await expect( + pTokenV2.connect(bridge).mint(user, remainingMintAmount), + ).to.be.revertedWithCustomError( + pTokenV2, + 'IXERC20_NotHighEnoughLimits', + ) // Transfer the whole amount in order to burn await pTokenV2.connect(bridge).mint(user, mintingLimit) @@ -371,22 +330,14 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) await pTokenV2.connect(user).approve(bridge, remainingBurnLimit) - if (_tokenKind === 'XERC20') { - await expect( - pTokenV2 - .connect(bridge) - ['burn(address,uint256)'](user, remainingBurnLimit), - ).to.be.revertedWithCustomError( - pTokenV2, - 'IXERC20_NotHighEnoughLimits', - ) - } else { - await expect( - pTokenV2 - .connect(bridge) - ['burn(address,uint256)'](user, remainingBurnLimit), - ).to.be.revertedWith('IXERC20_NotHighEnoughLimits') - } + await expect( + pTokenV2 + .connect(bridge) + ['burn(address,uint256)'](user, remainingBurnLimit), + ).to.be.revertedWithCustomError( + pTokenV2, + 'IXERC20_NotHighEnoughLimits', + ) }) it('Should lower the limit successfully', async () => { @@ -505,20 +456,6 @@ const deployERC1820 = () => helpers.setCode(ERC1820, ERC1820BYTES) await snapshot.restore() }) - - it('Should revert with `No implemented`', async () => { - await helpers.time.increase(oneDay) - - if (_tokenKind === 'PTokenV2') { - const amount = 100n - await pTokenV2.connect(bridge).mint(user, amount) - await pTokenV2.connect(user).approve(bridge, amount) - - await expect( - pTokenV2.connect(bridge)['burn(uint256,bytes)'](amount, '0x'), - ).to.revertedWith('Not implemented') - } - }) }) }) }) diff --git a/solidity/test/hardhat/utils/deploy-proxy.cjs b/solidity/test/hardhat/utils/deploy-proxy.cjs index b0c41087..7b732ce0 100644 --- a/solidity/test/hardhat/utils/deploy-proxy.cjs +++ b/solidity/test/hardhat/utils/deploy-proxy.cjs @@ -1,7 +1,7 @@ const { pollingInterval } = require('./openzeppelin-opts.cjs') -module.exports.deployProxy = (_hre, _factoryName, _args = []) => - _hre.ethers.getContractFactory(_factoryName).then(_pToken => +module.exports.deployProxy = (_hre, _factoryName, _admin, _args = []) => + _hre.ethers.getContractFactory(_factoryName, _admin).then(_pToken => _hre.upgrades.deployProxy(_pToken, _args, { initializer: 'initialize(string,string,address,bytes4)', pollingInterval, diff --git a/solidity/test/hardhat/utils/get-upgrade-opts.cjs b/solidity/test/hardhat/utils/get-upgrade-opts.cjs index 521795d4..d0367f62 100644 --- a/solidity/test/hardhat/utils/get-upgrade-opts.cjs +++ b/solidity/test/hardhat/utils/get-upgrade-opts.cjs @@ -1,4 +1,3 @@ -module.exports.getUpgradeOpts = (_owner, _useGSN) => - _useGSN === '' - ? {} - : { call: { fn: 'initializeV2(address)', args: [_owner.address] } } +module.exports.getUpgradeOpts = _owner => ({ + call: { fn: 'initializeV2(address)', args: [_owner.address] }, +}) diff --git a/solidity/test/hardhat/utils/upgrade-proxy.cjs b/solidity/test/hardhat/utils/upgrade-proxy.cjs index febc1946..f2c5425c 100644 --- a/solidity/test/hardhat/utils/upgrade-proxy.cjs +++ b/solidity/test/hardhat/utils/upgrade-proxy.cjs @@ -5,8 +5,8 @@ module.exports.upgradeProxy = ( _proxy, _contractFactoryName, _opts = {}, - _owner = undefined, + _admin, ) => _hre.ethers - .getContractFactory(_contractFactoryName, _owner) + .getContractFactory(_contractFactoryName, _admin) .then(_ethPNTv2 => _hre.upgrades.upgradeProxy(_proxy, _ethPNTv2, _opts)) diff --git a/yarn.lock b/yarn.lock index 21d64ca7..d9d5993c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1931,6 +1931,7 @@ __metadata: ethers: "npm:^6.13.1" hardhat: "npm:^2.22.6" hardhat-gas-reporter: "npm:^2.2.0" + hardhat-storage-layout: "npm:^0.1.7" hardhat-tracer: "npm:^3.0.1" ramda: "npm:^0.30.1" solhint: "npm:^5.0.2" @@ -3783,6 +3784,15 @@ __metadata: languageName: node linkType: hard +"console-table-printer@npm:^2.9.0": + version: 2.12.1 + resolution: "console-table-printer@npm:2.12.1" + dependencies: + simple-wcswidth: "npm:^1.0.1" + checksum: 10c0/8f28e9c0ae5df77f5d60da3da002ecd95ebe1812b0b9e0a6d2795c81b5121b39774f32506bccf68830a838ca4d8fbb2ab8824e729dba2c5e30cdeb9df4dd5f2b + languageName: node + linkType: hard + "convert-source-map@npm:^2.0.0": version: 2.0.0 resolution: "convert-source-map@npm:2.0.0" @@ -5420,6 +5430,17 @@ __metadata: languageName: node linkType: hard +"hardhat-storage-layout@npm:^0.1.7": + version: 0.1.7 + resolution: "hardhat-storage-layout@npm:0.1.7" + dependencies: + console-table-printer: "npm:^2.9.0" + peerDependencies: + hardhat: ^2.0.3 + checksum: 10c0/257b52a079183953d079ae221d05551391ff57adbad1ba033a3ccfa1b9df495ddd29285e67a7d03da484aa69f65850feb64a9bd7e37f53c549efd3833ed8b38c + languageName: node + linkType: hard + "hardhat-tracer@npm:^3.0.1": version: 3.0.1 resolution: "hardhat-tracer@npm:3.0.1" @@ -8560,6 +8581,13 @@ __metadata: languageName: node linkType: hard +"simple-wcswidth@npm:^1.0.1": + version: 1.0.1 + resolution: "simple-wcswidth@npm:1.0.1" + checksum: 10c0/2befead4c97134424aa3fba593a81daa9934fd61b9e4c65374b57ac5eecc2f2be1984b017bbdbc919923e19b77f2fcbdb94434789b9643fa8c3fde3a2a6a4b6f + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5"