diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1919e379..593ab46a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,10 +13,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Setup NodeJS 16 + - name: Setup NodeJS 20.5.0 uses: actions/setup-node@v3 with: - node-version: 16.17.0 + node-version: 20.5.0 - name: Show NodeJS version run: npm --version diff --git a/.gitmodules b/.gitmodules index b2149193..c5c976f2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "openzeppelin-contracts-upgradeable"] path = openzeppelin-contracts-upgradeable url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "ccip"] + path = ccip + url = https://github.com/smartcontractkit/ccip diff --git a/ccip b/ccip new file mode 160000 index 00000000..9d4d2a43 --- /dev/null +++ b/ccip @@ -0,0 +1 @@ +Subproject commit 9d4d2a4303283818222e0211825d0dd1f7495e33 diff --git a/contracts/interfaces/ICCIPToken.sol b/contracts/interfaces/ICCIPToken.sol new file mode 100644 index 00000000..4ab3564e --- /dev/null +++ b/contracts/interfaces/ICCIPToken.sol @@ -0,0 +1,20 @@ +//SPDX-License-Identifier: MPL-2.0 + +pragma solidity ^0.8.0; + +interface ICCIPMintERC20 { + /// @notice Mints new tokens for a given address. + /// @param account The address to mint the new tokens to. + /// @param value The number of tokens to be minted. + /// @dev this function increases the total supply. + function mint(address account, uint256 value) external; +} + +interface ICCIPBurnFromERC20 { + /// @notice Burns tokens from a given address.. + /// @param account The address to burn tokens from. + /// @param value The number of tokens to be burned. + /// @dev this function decreases the total supply. + function burnFrom(address account, uint256 value) external; + +} \ No newline at end of file diff --git a/contracts/modules/CMTAT_BASE.sol b/contracts/modules/CMTAT_BASE.sol index d9362d28..64dd0eb9 100644 --- a/contracts/modules/CMTAT_BASE.sol +++ b/contracts/modules/CMTAT_BASE.sol @@ -15,8 +15,9 @@ import "./wrapper/core/PauseModule.sol"; /* SnapshotModule: Add this import in case you add the SnapshotModule -import "./wrapper/optional/SnapshotModule.sol"; */ +import "./wrapper/extensions/ERC20SnapshotModule.sol"; + import "./wrapper/controllers/ValidationModule.sol"; import "./wrapper/extensions/MetaTxModule.sol"; import "./wrapper/extensions/DebtModule/DebtBaseModule.sol"; @@ -36,7 +37,7 @@ abstract contract CMTAT_BASE is ValidationModule, MetaTxModule, ERC20BaseModule, - // ERC20SnapshotModule, + ERC20SnapshotModule, DebtBaseModule, CreditEventsModule { @@ -110,10 +111,10 @@ abstract contract CMTAT_BASE is /* SnapshotModule: Add these two calls in case you add the SnapshotModule - + */ __SnapshotModuleBase_init_unchained(); __ERC20Snapshot_init_unchained(); - */ + __Validation_init_unchained(ruleEngine_); /* Wrapper */ @@ -131,8 +132,9 @@ abstract contract CMTAT_BASE is /* SnapshotModule: Add this call in case you add the SnapshotModule - __ERC20SnasphotModule_init_unchained(); */ + __ERC20SnasphotModule_init_unchained(); + /* Other modules */ __DebtBaseModule_init_unchained(); @@ -173,6 +175,22 @@ abstract contract CMTAT_BASE is return ERC20BaseModule.transferFrom(sender, recipient, amount); } + /** + @notice burn and mint atomically + @param from current token holder to burn tokens + @param to receiver to send the new minted tokens + @param amountToBurn number of tokens to burn + @param amountToMint number of tokens to mint + @dev + - The access control is managed by the functions burn (ERC20BurnModule) and mint (ERC20MintModule) + - Input validation is also managed by the functions burn and mint + - You can mint more tokens than burnt + */ + function burnAndMint(address from, address to, uint256 amountToBurn, uint256 amountToMint, string calldata reason) public { + burn(from, amountToBurn, reason); + mint(to, amountToMint); + } + /** * @dev * @@ -190,7 +208,7 @@ abstract contract CMTAT_BASE is Add this in case you add the SnapshotModule We call the SnapshotModule only if the transfer is valid */ - // ERC20SnapshotModuleInternal._snapshotUpdate(from, to); + ERC20SnapshotModuleInternal._snapshotUpdate(from, to); ERC20Upgradeable._update(from, to, amount); } diff --git a/contracts/modules/security/AuthorizationModule.sol b/contracts/modules/security/AuthorizationModule.sol index 3203b875..06d86a88 100644 --- a/contracts/modules/security/AuthorizationModule.sol +++ b/contracts/modules/security/AuthorizationModule.sol @@ -15,6 +15,7 @@ abstract contract AuthorizationModule is AccessControlUpgradeable { event AuthorizationEngine(IAuthorizationEngine indexed newAuthorizationEngine); // BurnModule bytes32 public constant BURNER_ROLE = keccak256("BURNER_ROLE"); + bytes32 public constant BURNER_FROM_ROLE = keccak256("BURNER_FROM_ROLE"); // CreditEvents bytes32 public constant DEBT_CREDIT_EVENT_ROLE = keccak256("DEBT_CREDIT_EVENT_ROLE"); @@ -51,8 +52,8 @@ abstract contract AuthorizationModule is AccessControlUpgradeable { /* - @notice set an authorizationEngine if not already set - @dev once an AuthorizationEngine is set, it is not possible to unset it + * @notice set an authorizationEngine if not already set + * @dev once an AuthorizationEngine is set, it is not possible to unset it */ function setAuthorizationEngine( IAuthorizationEngine authorizationEngine_ diff --git a/contracts/modules/wrapper/core/BaseModule.sol b/contracts/modules/wrapper/core/BaseModule.sol index 23b26a76..b1f44401 100644 --- a/contracts/modules/wrapper/core/BaseModule.sol +++ b/contracts/modules/wrapper/core/BaseModule.sol @@ -26,6 +26,7 @@ abstract contract BaseModule is AuthorizationModule { string public tokenId; string public terms; string public information; + // additional attribute to store information as an uint256 uint256 public flag; diff --git a/contracts/modules/wrapper/core/ERC20BaseModule.sol b/contracts/modules/wrapper/core/ERC20BaseModule.sol index 43743437..0a2af91d 100644 --- a/contracts/modules/wrapper/core/ERC20BaseModule.sol +++ b/contracts/modules/wrapper/core/ERC20BaseModule.sol @@ -11,6 +11,7 @@ abstract contract ERC20BaseModule is ERC20Upgradeable { /* Events */ /** * @notice Emitted when the specified `spender` spends the specified `value` tokens owned by the specified `owner` reducing the corresponding allowance. + * @dev The allowance can be also "spend" with the function BurnFrom, but in this case, the emitted event is BurnFrom. */ event Spend(address indexed owner, address indexed spender, uint256 value); diff --git a/contracts/modules/wrapper/core/ERC20BurnModule.sol b/contracts/modules/wrapper/core/ERC20BurnModule.sol index ce4e36a6..43467e71 100644 --- a/contracts/modules/wrapper/core/ERC20BurnModule.sol +++ b/contracts/modules/wrapper/core/ERC20BurnModule.sol @@ -5,13 +5,16 @@ pragma solidity ^0.8.20; import "../../../../openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; import "../../../../openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import "../../security/AuthorizationModule.sol"; - -abstract contract ERC20BurnModule is ERC20Upgradeable, AuthorizationModule { +import "../../../interfaces/ICCIPToken.sol"; +abstract contract ERC20BurnModule is ERC20Upgradeable, ICCIPBurnFromERC20, AuthorizationModule { /** * @notice Emitted when the specified `value` amount of tokens owned by `owner`are destroyed with the given `reason` */ event Burn(address indexed owner, uint256 value, string reason); - + /** + * @notice Emitted when the specified `spender` burns the specified `value` tokens owned by the specified `owner` reducing the corresponding allowance. + */ + event BurnFrom(address indexed owner, address indexed spender, uint256 value); function __ERC20BurnModule_init_unchained() internal onlyInitializing { // no variable to initialize } @@ -25,7 +28,7 @@ abstract contract ERC20BurnModule is ERC20Upgradeable, AuthorizationModule { * Requirements: * - the caller must have the `BURNER_ROLE`. */ - function forceBurn( + function burn( address account, uint256 value, string calldata reason @@ -34,9 +37,10 @@ abstract contract ERC20BurnModule is ERC20Upgradeable, AuthorizationModule { emit Burn(account, value, reason); } + /** * - * @notice batch version of {forceBurn}. + * @notice batch version of {burn}. * @dev * See {ERC20-_burn} and {OpenZeppelin ERC1155_burnBatch}. * @@ -48,7 +52,7 @@ abstract contract ERC20BurnModule is ERC20Upgradeable, AuthorizationModule { * - `accounts` and `values` must have the same length * - the caller must have the `BURNER_ROLE`. */ - function forceBurnBatch( + function burnBatch( address[] calldata accounts, uint256[] calldata values, string calldata reason @@ -71,5 +75,42 @@ abstract contract ERC20BurnModule is ERC20Upgradeable, AuthorizationModule { } } + /** + * @notice Destroys `amount` tokens from `account`, deducting from the caller's + * allowance. + * @dev + * Can be used to authorize a bridge (e.g. CCIP) to burn token owned by the bridge + * No string parameter reason to be compatible with Bridge, e.g. CCIP + * + * See {ERC20-_burn} and {ERC20-allowance}. + * + * Requirements: + * + * - the caller must have allowance for ``accounts``'s tokens of at least + * `value`. + */ + function burnFrom(address account, uint256 value) + public + onlyRole(BURNER_FROM_ROLE) + { + // Allowance check + address sender = _msgSender(); + uint256 currentAllowance = allowance(account, sender); + if(currentAllowance < value){ + // ERC-6093 + revert ERC20InsufficientAllowance(sender, currentAllowance, value); + } + // Update allowance + unchecked { + _approve(account, sender, currentAllowance - value); + } + // burn + _burn(account, value); + // We also emit a burn event since its a burn operation + emit Burn(account, value, "burnFrom"); + // Specific event for the operation + emit BurnFrom(account, sender, value); + } + uint256[50] private __gap; } diff --git a/contracts/modules/wrapper/core/ERC20MintModule.sol b/contracts/modules/wrapper/core/ERC20MintModule.sol index 3744a4f3..cb380224 100644 --- a/contracts/modules/wrapper/core/ERC20MintModule.sol +++ b/contracts/modules/wrapper/core/ERC20MintModule.sol @@ -5,8 +5,8 @@ pragma solidity ^0.8.20; import "../../../../openzeppelin-contracts-upgradeable/contracts/token/ERC20/ERC20Upgradeable.sol"; import "../../../../openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; import "../../security/AuthorizationModule.sol"; - -abstract contract ERC20MintModule is ERC20Upgradeable, AuthorizationModule { +import "../../../interfaces/ICCIPToken.sol"; +abstract contract ERC20MintModule is ERC20Upgradeable, ICCIPMintERC20, AuthorizationModule { /** * @notice Emitted when the specified `value` amount of new tokens are created and * allocated to the specified `account`. @@ -19,6 +19,8 @@ abstract contract ERC20MintModule is ERC20Upgradeable, AuthorizationModule { /** * @notice Creates a `value` amount of tokens and assigns them to `account`, by transferring it from address(0) + * @param account token receiver + * @param value amount of tokens * @dev * See {OpenZeppelin ERC20-_mint}. * Emits a {Mint} event. diff --git a/contracts/modules/wrapper/core/EnforcementModule.sol b/contracts/modules/wrapper/core/EnforcementModule.sol index 739cadf4..df1b3d68 100644 --- a/contracts/modules/wrapper/core/EnforcementModule.sol +++ b/contracts/modules/wrapper/core/EnforcementModule.sol @@ -15,10 +15,10 @@ abstract contract EnforcementModule is AuthorizationModule { string internal constant TEXT_TRANSFER_REJECTED_FROM_FROZEN = - "The address FROM is frozen"; + "Address FROM is frozen"; string internal constant TEXT_TRANSFER_REJECTED_TO_FROZEN = - "The address TO is frozen"; + "Address TO is frozen"; function __EnforcementModule_init_unchained() internal onlyInitializing { // no variable to initialize diff --git a/contracts/modules/wrapper/extensions/DebtModule/DebtBaseModule.sol b/contracts/modules/wrapper/extensions/DebtModule/DebtBaseModule.sol index 1fbfd8d8..9193531a 100644 --- a/contracts/modules/wrapper/extensions/DebtModule/DebtBaseModule.sol +++ b/contracts/modules/wrapper/extensions/DebtModule/DebtBaseModule.sol @@ -59,9 +59,9 @@ abstract contract DebtBaseModule is // no variable to initialize } - /* - @notice Set all attributes of debt - The values of all attributes will be changed even if the new values are the same as the current ones + /** + * @notice Set all attributes of debt + * The values of all attributes will be changed even if the new values are the same as the current ones */ function setDebt(DebtBase calldata debt_) public onlyRole(DEBT_ROLE) { debt = debt_; @@ -96,8 +96,8 @@ abstract contract DebtBaseModule is emit CouponFrequency(debt_.couponFrequency, debt_.couponFrequency); } - /* - @notice The call will be reverted if the new value of interestRate is the same as the current one + /** + * @notice The call will be reverted if the new value of interestRate is the same as the current one */ function setInterestRate(uint256 interestRate_) public onlyRole(DEBT_ROLE) { if (interestRate_ == debt.interestRate) { @@ -107,8 +107,8 @@ abstract contract DebtBaseModule is emit InterestRate(interestRate_); } - /* - @notice The call will be reverted if the new value of parValue is the same as the current one + /** + * @notice The call will be reverted if the new value of parValue is the same as the current one */ function setParValue(uint256 parValue_) public onlyRole(DEBT_ROLE) { if (parValue_ == debt.parValue) { @@ -118,8 +118,8 @@ abstract contract DebtBaseModule is emit ParValue(parValue_); } - /* - @notice The Guarantor will be changed even if the new value is the same as the current one + /** + * @notice The Guarantor will be changed even if the new value is the same as the current one */ function setGuarantor( string calldata guarantor_ @@ -128,8 +128,8 @@ abstract contract DebtBaseModule is emit Guarantor(guarantor_, guarantor_); } - /* - @notice The bonHolder will be changed even if the new value is the same as the current one + /** + * @notice The bonHolder will be changed even if the new value is the same as the current one */ function setBondHolder( string calldata bondHolder_ @@ -138,8 +138,8 @@ abstract contract DebtBaseModule is emit BondHolder(bondHolder_, bondHolder_); } - /* - @notice The maturityDate will be changed even if the new value is the same as the current one + /** + * @notice The maturityDate will be changed even if the new value is the same as the current one */ function setMaturityDate( string calldata maturityDate_ @@ -148,8 +148,8 @@ abstract contract DebtBaseModule is emit MaturityDate(maturityDate_, maturityDate_); } - /* - @notice The interestScheduleFormat will be changed even if the new value is the same as the current one + /** + * @notice The interestScheduleFormat will be changed even if the new value is the same as the current one */ function setInterestScheduleFormat( string calldata interestScheduleFormat_ @@ -161,8 +161,8 @@ abstract contract DebtBaseModule is ); } - /* - @notice The interestPaymentDate will be changed even if the new value is the same as the current one + /** + * @notice The interestPaymentDate will be changed even if the new value is the same as the current one */ function setInterestPaymentDate( string calldata interestPaymentDate_ @@ -171,8 +171,8 @@ abstract contract DebtBaseModule is emit InterestPaymentDate(interestPaymentDate_, interestPaymentDate_); } - /* - @notice The dayCountConvention will be changed even if the new value is the same as the current one + /** + * @notice The dayCountConvention will be changed even if the new value is the same as the current one */ function setDayCountConvention( string calldata dayCountConvention_ @@ -181,8 +181,8 @@ abstract contract DebtBaseModule is emit DayCountConvention(dayCountConvention_, dayCountConvention_); } - /* - @notice The businessDayConvention will be changed even if the new value is the same as the current one + /** + * @notice The businessDayConvention will be changed even if the new value is the same as the current one */ function setBusinessDayConvention( string calldata businessDayConvention_ @@ -194,8 +194,8 @@ abstract contract DebtBaseModule is ); } - /* - @notice The publicHolidayCalendar will be changed even if the new value is the same as the current one + /** + * @notice The publicHolidayCalendar will be changed even if the new value is the same as the current one */ function setPublicHolidaysCalendar( string calldata publicHolidaysCalendar_ @@ -207,8 +207,8 @@ abstract contract DebtBaseModule is ); } - /* - @notice The issuanceDate will be changed even if the new value is the same as the current one + /** + * @notice The issuanceDate will be changed even if the new value is the same as the current one */ function setIssuanceDate( string calldata issuanceDate_ @@ -217,8 +217,8 @@ abstract contract DebtBaseModule is emit IssuanceDate(issuanceDate_, issuanceDate_); } - /* - @notice The couponFrequency will be changed even if the new value is the same as the current one + /** + * @notice The couponFrequency will be changed even if the new value is the same as the current one */ function setCouponFrequency( string calldata couponFrequency_ diff --git a/contracts/test/CMTATSnapshot/CMTATSnapshotProxyTest.sol b/contracts/test/CMTATSnapshot/CMTATSnapshotProxyTest.sol deleted file mode 100644 index 75525862..00000000 --- a/contracts/test/CMTATSnapshot/CMTATSnapshotProxyTest.sol +++ /dev/null @@ -1,21 +0,0 @@ -//SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -import "./CMTAT_BASE_SnapshotTest.sol"; - -contract CMTATSnapshotProxyTest is CMTAT_BASE_SnapshotTest { - /** - @notice Contract version for the deployment with a proxy - @param forwarderIrrevocable address of the forwarder, required for the gasless support - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor( - address forwarderIrrevocable - ) MetaTxModule(forwarderIrrevocable) { - // Disable the possibility to initialize the implementation - _disableInitializers(); - } - - uint256[50] private __gap; -} diff --git a/contracts/test/CMTATSnapshot/CMTATSnapshotStandaloneTest.sol b/contracts/test/CMTATSnapshot/CMTATSnapshotStandaloneTest.sol deleted file mode 100644 index 599e3a5a..00000000 --- a/contracts/test/CMTATSnapshot/CMTATSnapshotStandaloneTest.sol +++ /dev/null @@ -1,51 +0,0 @@ -//SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -import "./CMTAT_BASE_SnapshotTest.sol"; - -contract CMTATSnapshotStandaloneTest is CMTAT_BASE_SnapshotTest { - /** - @notice Contract version for standalone deployment - @param forwarderIrrevocable address of the forwarder, required for the gasless support - @param admin address of the admin of contract (Access Control) - @param nameIrrevocable name of the token - @param symbolIrrevocable name of the symbol - @param tokenId name of the tokenId - @param terms terms associated with the token - @param ruleEngine address of the ruleEngine to apply rules to transfers - @param information additional information to describe the token - @param flag add information under the form of bit(0, 1) - */ - /// @custom:oz-upgrades-unsafe-allow constructor - constructor( - address forwarderIrrevocable, - address admin, - IAuthorizationEngine authorizationEngineIrrevocable, - string memory nameIrrevocable, - string memory symbolIrrevocable, - uint8 decimalsIrrevocable, - string memory tokenId_, - string memory terms_, - IRuleEngine ruleEngine_, - string memory information_, - uint256 flag_ - ) MetaTxModule(forwarderIrrevocable) { - // Initialize the contract to avoid front-running - // Warning : do not initialize the proxy - initialize( - admin, - authorizationEngineIrrevocable, - nameIrrevocable, - symbolIrrevocable, - decimalsIrrevocable, - tokenId_, - terms_, - ruleEngine_, - information_, - flag_ - ); - } - - // No storage gap because the contract is deployed in standalone mode -} diff --git a/contracts/test/CMTATSnapshot/CMTAT_BASE_SnapshotTest.sol b/contracts/test/CMTATSnapshot/CMTAT_BASE_SnapshotTest.sol deleted file mode 100644 index 0e17785f..00000000 --- a/contracts/test/CMTATSnapshot/CMTAT_BASE_SnapshotTest.sol +++ /dev/null @@ -1,216 +0,0 @@ -//SPDX-License-Identifier: MPL-2.0 - -pragma solidity ^0.8.20; - -// required OZ imports here -import "../../../openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol"; -import "../../../openzeppelin-contracts-upgradeable/contracts/utils/ContextUpgradeable.sol"; - -import "../../modules/wrapper/core/BaseModule.sol"; -import "../../modules/wrapper/core/ERC20BurnModule.sol"; -import "../../modules/wrapper/core/ERC20MintModule.sol"; -import "../../modules/wrapper/core/EnforcementModule.sol"; -import "../../modules/wrapper/core/ERC20BaseModule.sol"; -import "../../modules/wrapper/core/PauseModule.sol"; -/* -SnapshotModule: -Add this import in case you add the SnapshotModule -*/ -import "../../modules/wrapper/controllers/ValidationModule.sol"; -import "../../modules/wrapper/extensions/ERC20SnapshotModule.sol"; -import "../../modules/wrapper/extensions/MetaTxModule.sol"; -import "../../modules/wrapper/extensions/DebtModule/DebtBaseModule.sol"; -import "../../modules/wrapper/extensions/DebtModule/CreditEventsModule.sol"; -import "../../modules/security/AuthorizationModule.sol"; - -import "../../libraries/Errors.sol"; - -abstract contract CMTAT_BASE_SnapshotTest is - Initializable, - ContextUpgradeable, - BaseModule, - PauseModule, - ERC20MintModule, - ERC20BurnModule, - EnforcementModule, - ValidationModule, - MetaTxModule, - ERC20BaseModule, - ERC20SnapshotModule, - DebtBaseModule, - CreditEventsModule -{ - /** - @notice - initialize the proxy contract - The calls to this function will revert if the contract was deployed without a proxy - */ - function initialize( - address admin, - IAuthorizationEngine authorizationEngineIrrevocable, - string memory nameIrrevocable, - string memory symbolIrrevocable, - uint8 decimalsIrrevocable, - string memory tokenId_, - string memory terms_, - IRuleEngine ruleEngine_, - string memory information_, - uint256 flag_ - ) public initializer { - __CMTAT_init( - admin, - authorizationEngineIrrevocable, - nameIrrevocable, - symbolIrrevocable, - decimalsIrrevocable, - tokenId_, - terms_, - ruleEngine_, - information_, - flag_ - ); - } - - /** - @dev calls the different initialize functions from the different modules - */ - function __CMTAT_init( - address admin, - IAuthorizationEngine authorizationEngineIrrevocable, - string memory nameIrrevocable, - string memory symbolIrrevocable, - uint8 decimalsIrrevocable, - string memory tokenId_, - string memory terms_, - IRuleEngine ruleEngine_, - string memory information_, - uint256 flag_ - ) internal onlyInitializing { - /* OpenZeppelin library */ - // OZ init_unchained functions are called firstly due to inheritance - __Context_init_unchained(); - __ERC20_init_unchained(nameIrrevocable, symbolIrrevocable); - // AccessControlUpgradeable inherits from ERC165Upgradeable - __ERC165_init_unchained(); - // AuthorizationModule inherits from AccessControlUpgradeable - __AccessControl_init_unchained(); - __Pausable_init_unchained(); - - /* Internal Modules */ - __Enforcement_init_unchained(); - /* - SnapshotModule: - Add these two calls in case you add the SnapshotModule - */ - __SnapshotModuleBase_init_unchained(); - __ERC20Snapshot_init_unchained(); - - __Validation_init_unchained(ruleEngine_); - - /* Wrapper */ - // AuthorizationModule_init_unchained is called firstly due to inheritance - __AuthorizationModule_init_unchained(admin, authorizationEngineIrrevocable); - __ERC20BurnModule_init_unchained(); - __ERC20MintModule_init_unchained(); - // EnforcementModule_init_unchained is called before ValidationModule_init_unchained due to inheritance - __EnforcementModule_init_unchained(); - __ERC20BaseModule_init_unchained(decimalsIrrevocable); - // PauseModule_init_unchained is called before ValidationModule_init_unchained due to inheritance - __PauseModule_init_unchained(); - __ValidationModule_init_unchained(); - - /* - SnapshotModule: - Add this call in case you add the SnapshotModule - */ - __ERC20SnasphotModule_init_unchained(); - - /* Other modules */ - __DebtBaseModule_init_unchained(); - __CreditEvents_init_unchained(); - __Base_init_unchained(tokenId_, terms_, information_, flag_); - - /* own function */ - __CMTAT_init_unchained(); - } - - function __CMTAT_init_unchained() internal onlyInitializing { - // no variable to initialize - } - - /** - @notice Returns the number of decimals used to get its user representation. - */ - function decimals() - public - view - virtual - override(ERC20Upgradeable, ERC20BaseModule) - returns (uint8) - { - return ERC20BaseModule.decimals(); - } - - function transferFrom( - address sender, - address recipient, - uint256 amount - ) - public - virtual - override(ERC20Upgradeable, ERC20BaseModule) - returns (bool) - { - return ERC20BaseModule.transferFrom(sender, recipient, amount); - } - - /* - @dev - SnapshotModule: - - override SnapshotModuleInternal if you add the SnapshotModule - e.g. override(SnapshotModuleInternal, ERC20Upgradeable) - - remove the keyword view - */ - function _update( - address from, - address to, - uint256 amount - ) internal override(ERC20Upgradeable) { - if (!ValidationModule._operateOnTransfer(from, to, amount)){ - revert Errors.CMTAT_InvalidTransfer(from, to, amount); - } - /* - SnapshotModule: - Add this in case you add the SnapshotModule - We call the SnapshotModule only if the transfer is valid - */ - ERC20SnapshotModuleInternal._snapshotUpdate(from, to); - ERC20Upgradeable._update(from, to, amount); - } - - /** - @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgSender() - internal - view - override(MetaTxModule, ContextUpgradeable) - returns (address sender) - { - return MetaTxModule._msgSender(); - } - - /** - @dev This surcharge is not necessary if you do not use the MetaTxModule - */ - function _msgData() - internal - view - override(MetaTxModule, ContextUpgradeable) - returns (bytes calldata) - { - return MetaTxModule._msgData(); - } - - uint256[50] private __gap; -} diff --git a/test/common/ERC20BurnModuleCommon.js b/test/common/ERC20BurnModuleCommon.js index 779ef694..b88c7822 100644 --- a/test/common/ERC20BurnModuleCommon.js +++ b/test/common/ERC20BurnModuleCommon.js @@ -1,12 +1,12 @@ const { BN, expectEvent } = require('@openzeppelin/test-helpers') -const { BURNER_ROLE, ZERO_ADDRESS } = require('../utils') +const { BURNER_ROLE, BURNER_FROM_ROLE, ZERO_ADDRESS } = require('../utils') const { expectRevertCustomError } = require('../../openzeppelin-contracts-upgradeable/test/helpers/customError.js') const { should } = require('chai').should() function ERC20BurnModuleCommon (admin, address1, address2) { - context('Burn', function () { + context('burn', function () { const INITIAL_SUPPLY = new BN(50) const REASON = 'BURN_TEST' const VALUE1 = new BN(20) @@ -22,7 +22,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { it('testCanBeBurntByAdmin', async function () { // Act // Burn 20 - this.logs = await this.cmtat.forceBurn(address1, VALUE1, REASON, { + this.logs = await this.cmtat.burn(address1, VALUE1, REASON, { from: admin }) // Assert @@ -46,7 +46,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { // Burn 30 // Act - this.logs = await this.cmtat.forceBurn(address1, DIFFERENCE, REASON, { + this.logs = await this.cmtat.burn(address1, DIFFERENCE, REASON, { from: admin }) @@ -72,7 +72,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { // Arrange await this.cmtat.grantRole(BURNER_ROLE, address2, { from: admin }) // Act - this.logs = await this.cmtat.forceBurn(address1, VALUE1, REASON, { + this.logs = await this.cmtat.burn(address1, VALUE1, REASON, { from: address2 }); // Assert @@ -102,7 +102,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { const ADDRESS1_BALANCE = await this.cmtat.balanceOf(address1) // Act await expectRevertCustomError( - this.cmtat.forceBurn(address1, AMOUNT_TO_BURN, '', { from: admin }), + this.cmtat.burn(address1, AMOUNT_TO_BURN, '', { from: admin }), 'ERC20InsufficientBalance', [address1, ADDRESS1_BALANCE, AMOUNT_TO_BURN] ) @@ -110,13 +110,63 @@ function ERC20BurnModuleCommon (admin, address1, address2) { it('testCannotBeBurntWithoutBurnerRole', async function () { await expectRevertCustomError( - this.cmtat.forceBurn(address1, 20, '', { from: address2 }), + this.cmtat.burn(address1, 20, '', { from: address2 }), 'AccessControlUnauthorizedAccount', [address2, BURNER_ROLE] ) }) }) - context('BurnBatch', function () { + + context('burnFrom', function () { + const INITIAL_SUPPLY = new BN(50) + const VALUE1 = new BN(20) + + beforeEach(async function () { + await this.cmtat.mint(address1, INITIAL_SUPPLY, { from: admin }); + (await this.cmtat.totalSupply()).should.be.bignumber.equal( + INITIAL_SUPPLY + ) + }) + + it('canBeBurnFrom', async function () { + // Arrange + const AMOUNT_TO_BURN = BN(20) + await this.cmtat.grantRole(BURNER_FROM_ROLE, address2, { from: admin }) + await this.cmtat.approve(address2, 50, { from: address1 }) + // Act + this.logs = await this.cmtat.burnFrom(address1, AMOUNT_TO_BURN, { from: address2 }) + // Assert + expectEvent(this.logs, 'Transfer', { + from: address1, + to: ZERO_ADDRESS, + value: AMOUNT_TO_BURN + }) + expectEvent(this.logs, 'BurnFrom', { + owner: address1, + spender: address2, + value: AMOUNT_TO_BURN + }); + (await this.cmtat.balanceOf(address1)).should.be.bignumber.equal('30'); + (await this.cmtat.totalSupply()).should.be.bignumber.equal('30') + }) + + it('TestCannotBeBurnWithoutAllowance', async function () { + const AMOUNT_TO_BURN = 20 + await expectRevertCustomError( + this.cmtat.burnFrom(address1, AMOUNT_TO_BURN, { from: admin }), + 'ERC20InsufficientAllowance', + [admin, 0, AMOUNT_TO_BURN]) + }) + + it('testCannotBeBurntWithoutBurnerFromRole', async function () { + await expectRevertCustomError( + this.cmtat.burnFrom(address1, 20, { from: address2 }), + 'AccessControlUnauthorizedAccount', + [address2, BURNER_FROM_ROLE]) + }) + }) + + context('burnBatch', function () { const REASON = 'BURN_TEST' const TOKEN_HOLDER = [admin, address1, address2] const TOKEN_SUPPLY_BY_HOLDERS = [BN(10), BN(100), BN(1000)] @@ -152,7 +202,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { it('testCanBeBurntBatchByAdmin', async function () { // Act // Burn - this.logs = await this.cmtat.forceBurnBatch( + this.logs = await this.cmtat.burnBatch( TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN, REASON, @@ -200,7 +250,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { // Act // Burn - this.logs = await this.cmtat.forceBurnBatch( + this.logs = await this.cmtat.burnBatch( TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN, REASON, @@ -247,7 +297,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { const ADDRESS2_BALANCE = await this.cmtat.balanceOf(address2) // Act await expectRevertCustomError( - this.cmtat.forceBurnBatch( + this.cmtat.burnBatch( TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN_FAIL, '', @@ -260,7 +310,7 @@ function ERC20BurnModuleCommon (admin, address1, address2) { it('testCannotBeBurntWithoutBurnerRole', async function () { await expectRevertCustomError( - this.cmtat.forceBurnBatch(TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN, '', { + this.cmtat.burnBatch(TOKEN_HOLDER, TOKEN_BY_HOLDERS_TO_BURN, '', { from: address2 }), 'AccessControlUnauthorizedAccount', @@ -268,11 +318,11 @@ function ERC20BurnModuleCommon (admin, address1, address2) { ) }) - it('testCannotBurnBatchIfLengthMismatchMissingAddresses', async function () { + it('testCannotburnBatchIfLengthMismatchMissingAddresses', async function () { // Number of addresses is insufficient const TOKEN_HOLDER_INVALID = [admin, address1] await expectRevertCustomError( - this.cmtat.forceBurnBatch( + this.cmtat.burnBatch( TOKEN_HOLDER_INVALID, TOKEN_BY_HOLDERS_TO_BURN, REASON, @@ -283,11 +333,11 @@ function ERC20BurnModuleCommon (admin, address1, address2) { ) }) - it('testCannotBurnBatchIfLengthMismatchTooManyAddresses', async function () { + it('testCannotburnBatchIfLengthMismatchTooManyAddresses', async function () { // There are too many addresses const TOKEN_HOLDER_INVALID = [admin, address1, address1, address1] await expectRevertCustomError( - this.cmtat.forceBurnBatch( + this.cmtat.burnBatch( TOKEN_HOLDER_INVALID, TOKEN_BY_HOLDERS_TO_BURN, REASON, @@ -298,10 +348,10 @@ function ERC20BurnModuleCommon (admin, address1, address2) { ) }) - it('testCannotBurnBatchIfAccountsIsEmpty', async function () { + it('testCannotburnBatchIfAccountsIsEmpty', async function () { const TOKEN_ADDRESS_TOS_INVALID = [] await expectRevertCustomError( - this.cmtat.forceBurnBatch( + this.cmtat.burnBatch( TOKEN_ADDRESS_TOS_INVALID, TOKEN_BY_HOLDERS_TO_BURN, REASON, diff --git a/test/common/ERC20SnapshotModuleCommon/global/ERC20SnapshotModuleOnePlannedSnapshotTest.js b/test/common/ERC20SnapshotModuleCommon/global/ERC20SnapshotModuleOnePlannedSnapshotTest.js index 8ecfd2a0..989f26d9 100644 --- a/test/common/ERC20SnapshotModuleCommon/global/ERC20SnapshotModuleOnePlannedSnapshotTest.js +++ b/test/common/ERC20SnapshotModuleCommon/global/ERC20SnapshotModuleOnePlannedSnapshotTest.js @@ -86,7 +86,7 @@ function ERC20SnapshotModuleOnePlannedSnapshotTest ( ) // Act - await this.cmtat.forceBurn(address1, BURN_AMOUNT, reason, { + await this.cmtat.burn(address1, BURN_AMOUNT, reason, { from: admin, gas: 5000000, gasPrice: 500000000 diff --git a/test/common/EnforcementModuleCommon.js b/test/common/EnforcementModuleCommon.js index 78f636ba..2893e0bf 100644 --- a/test/common/EnforcementModuleCommon.js +++ b/test/common/EnforcementModuleCommon.js @@ -145,7 +145,7 @@ function EnforcementModuleCommon (owner, address1, address2, address3) { await this.cmtat.detectTransferRestriction(address1, address2, 10) ).should.be.bignumber.equal('2'); (await this.cmtat.messageForTransferRestriction(2)).should.equal( - 'The address FROM is frozen' + 'Address FROM is frozen' ) const AMOUNT_TO_TRANSFER = 10 await expectRevertCustomError( @@ -168,7 +168,7 @@ function EnforcementModuleCommon (owner, address1, address2, address3) { await this.cmtat.detectTransferRestriction(address1, address2, 10) ).should.be.bignumber.equal('3'); (await this.cmtat.messageForTransferRestriction(3)).should.equal( - 'The address TO is frozen' + 'Address TO is frozen' ) const AMOUNT_TO_TRANSFER = 10 await expectRevertCustomError( diff --git a/test/deploymentUtils.js b/test/deploymentUtils.js index e3f57b44..77c84a41 100644 --- a/test/deploymentUtils.js +++ b/test/deploymentUtils.js @@ -1,10 +1,11 @@ const { ZERO_ADDRESS } = require('./utils') const CMTAT_STANDALONE = artifacts.require('CMTAT_STANDALONE') const CMTAT_STANDALONE_SNAPSHOT = artifacts.require( - 'CMTATSnapshotStandaloneTest' + 'CMTAT_STANDALONE' ) const CMTAT_PROXY = artifacts.require('CMTAT_PROXY') -const CMTAT_PROXY_SNAPSHOT = artifacts.require('CMTATSnapshotProxyTest') +// const CMTAT_PROXY_SNAPSHOT_TRUFFLE = artifacts.require('CMTATSnapshotProxyTest') +const CMTAT_PROXY_SNAPSHOT_TRUFFLE = artifacts.require('CMTAT_PROXY') const { deployProxy } = require('@openzeppelin/truffle-upgrades') const { ethers, upgrades } = require('hardhat') const DEPLOYMENT_FLAG = 5 @@ -102,8 +103,7 @@ async function deployCMTATProxy (_, admin, deployerAddress) { from: deployerAddress } ) - const TRUFFLE_CMTAT_PROXY = artifacts.require('CMTAT_PROXY') - const TRUFFLE_CMTAT_PROXY_ADDRESS = await TRUFFLE_CMTAT_PROXY.at( + const TRUFFLE_CMTAT_PROXY_ADDRESS = await CMTAT_PROXY_SNAPSHOT_TRUFFLE.at( await ETHERS_CMTAT_PROXY.getAddress() ) return TRUFFLE_CMTAT_PROXY_ADDRESS @@ -112,7 +112,7 @@ async function deployCMTATProxy (_, admin, deployerAddress) { async function deployCMTATProxyWithSnapshot (_, admin, deployerAddress) { // Ref: https://forum.openzeppelin.com/t/upgrades-hardhat-truffle5/30883/3 const ETHERS_CMTAT_PROXY_FACTORY = await ethers.getContractFactory( - 'CMTATSnapshotProxyTest' + 'CMTAT_PROXY' ) const ETHERS_CMTAT_PROXY = await upgrades.deployProxy( ETHERS_CMTAT_PROXY_FACTORY, @@ -134,7 +134,7 @@ async function deployCMTATProxyWithSnapshot (_, admin, deployerAddress) { from: deployerAddress } ) - const TRUFFLE_CMTAT_PROXY = artifacts.require('CMTATSnapshotProxyTest') + const TRUFFLE_CMTAT_PROXY = artifacts.require('CMTAT_PROXY') const TRUFFLE_CMTAT_PROXY_ADDRESS = await TRUFFLE_CMTAT_PROXY.at( await ETHERS_CMTAT_PROXY.getAddress() ) diff --git a/test/utils.js b/test/utils.js index a09b6992..ed8baa25 100644 --- a/test/utils.js +++ b/test/utils.js @@ -3,6 +3,8 @@ module.exports = { '0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6', // keccak256("MINTER_ROLE"); BURNER_ROLE: '0x3c11d16cbaffd01df69ce1c404f6340ee057498f5f00246190ea54220576a848', // keccak256("BURNER_ROLE"); + BURNER_FROM_ROLE: + '0x5bfe08abba057c54e6a28bce27ce8c53eb21d7a94376a70d475b5dee60b6c4e2', // keccak256("BURNER_FROM_ROLE"); ENFORCER_ROLE: '0x973ef39d76cc2c6090feab1c030bec6ab5db557f64df047a4c4f9b5953cf1df3', // keccak256("ENFORCER_ROLE"); PAUSER_ROLE: