diff --git a/CHANGELOG.md b/CHANGELOG.md index a3891be9..7e3e1650 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,22 @@ # Change Log All notable changes to this project will be documented in this file. +## [4.1.6] + +### Added + +- Added **removeTimeTransferLimit** function to TimeTransferLimitsModule, allows removing time transfer limits for the given limitTime. +- Added **batchSetTimeTransferLimit** and **batchRemoveTimeTransferLimit** functions to TimeTransferLimitsModule, allows setting and removing multiple time transfer limits at once. +- Introduced **Token Listing Restrictions Module**: Investors can determine which tokens they can receive by whitelisting or blacklisting them + Token issuers can choose the listing type they prefer for their tokens: + WHITELISTING: investors must whitelist/allow the token address in order to receive it. + BLACKLISTING: investors can receive the token by default. If they do not want to receive it, they need to blacklist/disallow it. +- Added commercial licensing to all modules of compliance as they are part of Tokeny's IP. + +### Update + +- Updated the **moduleCheck** function of TransferRestrictModule to directly allow transfers when the _from or _to address is the null address, enabling mint and burn operations without additional restrictions. + ## [4.1.5] ### Added diff --git a/contracts/compliance/modular/modules/ConditionalTransferModule.sol b/contracts/compliance/modular/modules/ConditionalTransferModule.sol index 46aee9a9..803dbe07 100644 --- a/contracts/compliance/modular/modules/ConditionalTransferModule.sol +++ b/contracts/compliance/modular/modules/ConditionalTransferModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/CountryAllowModule.sol b/contracts/compliance/modular/modules/CountryAllowModule.sol index 16e15819..29446e26 100644 --- a/contracts/compliance/modular/modules/CountryAllowModule.sol +++ b/contracts/compliance/modular/modules/CountryAllowModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/CountryRestrictModule.sol b/contracts/compliance/modular/modules/CountryRestrictModule.sol index cd07191a..edb789c4 100644 --- a/contracts/compliance/modular/modules/CountryRestrictModule.sol +++ b/contracts/compliance/modular/modules/CountryRestrictModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/ExchangeMonthlyLimitsModule.sol b/contracts/compliance/modular/modules/ExchangeMonthlyLimitsModule.sol index b1e530f0..ebd4f273 100644 --- a/contracts/compliance/modular/modules/ExchangeMonthlyLimitsModule.sol +++ b/contracts/compliance/modular/modules/ExchangeMonthlyLimitsModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/MaxBalanceModule.sol b/contracts/compliance/modular/modules/MaxBalanceModule.sol index 2d39116d..af4fb61f 100644 --- a/contracts/compliance/modular/modules/MaxBalanceModule.sol +++ b/contracts/compliance/modular/modules/MaxBalanceModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/SupplyLimitModule.sol b/contracts/compliance/modular/modules/SupplyLimitModule.sol index b67f83ee..1b4e83cc 100644 --- a/contracts/compliance/modular/modules/SupplyLimitModule.sol +++ b/contracts/compliance/modular/modules/SupplyLimitModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity ^0.8.17; diff --git a/contracts/compliance/modular/modules/TimeExchangeLimitsModule.sol b/contracts/compliance/modular/modules/TimeExchangeLimitsModule.sol index 9ae84e38..a591961b 100644 --- a/contracts/compliance/modular/modules/TimeExchangeLimitsModule.sol +++ b/contracts/compliance/modular/modules/TimeExchangeLimitsModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/TimeTransfersLimitsModule.sol b/contracts/compliance/modular/modules/TimeTransfersLimitsModule.sol index 265d6d5f..119db001 100644 --- a/contracts/compliance/modular/modules/TimeTransfersLimitsModule.sol +++ b/contracts/compliance/modular/modules/TimeTransfersLimitsModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; @@ -96,14 +102,24 @@ contract TimeTransfersLimitsModule is AbstractModuleUpgradeable { /** * this event is emitted whenever a transfer limit is updated for the given compliance address and limit time * the event is emitted by 'setTimeTransferLimit'. - * compliance`is the compliance contract address + * compliance is the compliance contract address * _limitValue is the new limit value for the given limit time * _limitTime is the period of time of the limit */ event TimeTransferLimitUpdated(address indexed compliance, uint32 limitTime, uint256 limitValue); + /** + * this event is emitted whenever a transfer limit is removed for the given compliance address and limit time + * the event is emitted by 'removeTimeTransferLimit'. + * compliance is the compliance contract address + * _limitTime is the period of time of the limit + */ + event TimeTransferLimitRemoved(address indexed compliance, uint32 limitTime); + error LimitsArraySizeExceeded(address compliance, uint arraySize); + error LimitTimeNotFound(address compliance, uint limitTime); + /** * @dev initializes the contract and sets the initial state. * @notice This function should only be called once during the contract deployment. @@ -113,24 +129,25 @@ contract TimeTransfersLimitsModule is AbstractModuleUpgradeable { } /** - * @dev Sets the limit of tokens allowed to be transferred in the given time frame. - * @param _limit The limit time and value + * @dev Sets multiple limit of tokens allowed to be transferred in the given time frame. + * @param _limits The array of limit time and values * Only the owner of the Compliance smart contract can call this function */ - function setTimeTransferLimit(Limit calldata _limit) external onlyComplianceCall { - bool limitIsAttributed = limitValues[msg.sender][_limit.limitTime].attributedLimit; - uint8 limitCount = uint8(transferLimits[msg.sender].length); - if (!limitIsAttributed && limitCount >= 4) { - revert LimitsArraySizeExceeded(msg.sender, limitCount); - } - if (!limitIsAttributed && limitCount < 4) { - transferLimits[msg.sender].push(_limit); - limitValues[msg.sender][_limit.limitTime] = IndexLimit(true, limitCount); - } else { - transferLimits[msg.sender][limitValues[msg.sender][_limit.limitTime].limitIndex] = _limit; + function batchSetTimeTransferLimit(Limit[] calldata _limits) external { + for (uint256 i = 0; i < _limits.length; i++) { + setTimeTransferLimit(_limits[i]); } + } - emit TimeTransferLimitUpdated(msg.sender, _limit.limitTime, _limit.limitValue); + /** + * @dev Removes multiple limits for the given limit time values. + * @param _limitTimes The array of limit times + * Only the owner of the Compliance smart contract can call this function + **/ + function batchRemoveTimeTransferLimit(uint32[] calldata _limitTimes) external { + for (uint256 i = 0; i < _limitTimes.length; i++) { + removeTimeTransferLimit(_limitTimes[i]); + } } /** @@ -208,6 +225,56 @@ contract TimeTransfersLimitsModule is AbstractModuleUpgradeable { return true; } + /** + * @dev Sets the limit of tokens allowed to be transferred in the given time frame. + * @param _limit The limit time and value + * Only the owner of the Compliance smart contract can call this function + */ + function setTimeTransferLimit(Limit calldata _limit) public onlyComplianceCall { + bool limitIsAttributed = limitValues[msg.sender][_limit.limitTime].attributedLimit; + uint8 limitCount = uint8(transferLimits[msg.sender].length); + if (!limitIsAttributed && limitCount >= 4) { + revert LimitsArraySizeExceeded(msg.sender, limitCount); + } + if (!limitIsAttributed && limitCount < 4) { + transferLimits[msg.sender].push(_limit); + limitValues[msg.sender][_limit.limitTime] = IndexLimit(true, limitCount); + } else { + transferLimits[msg.sender][limitValues[msg.sender][_limit.limitTime].limitIndex] = _limit; + } + + emit TimeTransferLimitUpdated(msg.sender, _limit.limitTime, _limit.limitValue); + } + + /** + * @dev Removes the limit for the given limit time value. + * @param _limitTime The limit time + * Only the owner of the Compliance smart contract can call this function + */ + function removeTimeTransferLimit(uint32 _limitTime) public onlyComplianceCall { + bool limitFound = false; + uint256 index; + for (uint256 i = 0; i < transferLimits[msg.sender].length; i++) { + if (transferLimits[msg.sender][i].limitTime == _limitTime) { + limitFound = true; + index = i; + break; + } + } + + if (!limitFound) { + revert LimitTimeNotFound(msg.sender, _limitTime); + } + + if (transferLimits[msg.sender].length > 1 && index != transferLimits[msg.sender].length - 1) { + transferLimits[msg.sender][index] = transferLimits[msg.sender][transferLimits[msg.sender].length - 1]; + } + + transferLimits[msg.sender].pop(); + delete limitValues[msg.sender][_limitTime]; + emit TimeTransferLimitRemoved(msg.sender, _limitTime); + } + /** * @dev See {IModule-name}. */ diff --git a/contracts/compliance/modular/modules/TokenListingRestrictionsModule.sol b/contracts/compliance/modular/modules/TokenListingRestrictionsModule.sol new file mode 100644 index 00000000..85794c96 --- /dev/null +++ b/contracts/compliance/modular/modules/TokenListingRestrictionsModule.sol @@ -0,0 +1,371 @@ +// SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. +// +// :+#####%%%%%%%%%%%%%%+ +// .-*@@@%+.:+%@@@@@%%#***%@@%= +// :=*%@@@#=. :#@@% *@@@%= +// .-+*%@%*-.:+%@@@@@@+. -*+: .=#. :%@@@%- +// :=*@@@@%%@@@@@@@@@%@@@- .=#@@@%@%= =@@@@#. +// -=+#%@@%#*=:. :%@@@@%. -*@@#*@@@@@@@#=:- *@@@@+ +// =@@%=:. :=: *@@@@@%#- =%*%@@@@#+-. =+ :%@@@%- +// -@@%. .+@@@ =+=-. @@#- +@@@%- =@@@@%: +// :@@@. .+@@#%: : .=*=-::.-%@@@+*@@= +@@@@#. +// %@@: +@%%* =%@@@@@@@@@@@#. .*@%- +@@@@*. +// #@@= .+@@@@%:=*@@@@@- :%@%: .*@@@@+ +// *@@* +@@@#-@@%-:%@@* +@@#. :%@@@@- +// -@@% .:-=++*##%%%@@@@@@@@@@@@*. :@+.@@@%: .#@@+ =@@@@#: +// .@@@*-+*#%%%@@@@@@@@@@@@@@@@%%#**@@%@@@. *@=*@@# :#@%= .#@@@@#- +// -%@@@@@@@@@@@@@@@*+==-:-@@@= *@# .#@*-=*@@@@%= -%@@@* =@@@@@%- +// -+%@@@#. %@%%= -@@:+@: -@@* *@@*-:: -%@@%=. .*@@@@@# +// *@@@* +@* *@@##@@- #@*@@+ -@@= . :+@@@#: .-+@@@%+- +// +@@@%*@@:..=@@@@* .@@@* .#@#. .=+- .=%@@@*. :+#@@@@*=: +// =@@@@%@@@@@@@@@@@@@@@@@@@@@@%- :+#*. :*@@@%=. .=#@@@@%+: +// .%@@= ..... .=#@@+. .#@@@*: -*%@@@@%+. +// +@@#+===---:::... .=%@@*- +@@@+. -*@@@@@%+. +// -@@@@@@@@@@@@@@@@@@@@@@%@@@@= -@@@+ -#@@@@@#=. +// ..:::---===+++***###%%%@@@#- .#@@+ -*@@@@@#=. +// @@@@@@+. +@@*. .+@@@@@%=. +// -@@@@@= =@@%: -#@@@@%+. +// +@@@@@. =@@@= .+@@@@@*: +// #@@@@#:%@@#. :*@@@@#- +// @@@@@%@@@= :#@@@@+. +// :@@@@@@@#.:#@@@%- +// +@@@@@@-.*@@@*: +// #@@@@#.=@@@+. +// @@@@+-%@%= +// :@@@#%@%= +// +@@@@%- +// :#%%= +// +/** + * NOTICE + * + * The T-REX software is licensed under a proprietary license or the GPL v.3. + * If you choose to receive it under the GPL v.3 license, the following applies: + * T-REX is a suite of smart contracts implementing the ERC-3643 standard and + * developed by Tokeny to manage and transfer financial assets on EVM blockchains + * + * Copyright (C) 2024, Tokeny sàrl. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. + */ + +pragma solidity 0.8.17; + +import "../IModularCompliance.sol"; +import "../../../token/IToken.sol"; +import "./AbstractModuleUpgradeable.sol"; + +/// Types + +enum ListingType { + NOT_CONFIGURED, // default value (token is not configured yet) + WHITELISTING, + BLACKLISTING +} + +enum InvestorAddressType { + WALLET, + ONCHAINID +} + +/// Errors + +/// @dev Thrown when the token is already configured +/// @param _tokenAddress the address of the token +error TokenAlreadyConfigured(address _tokenAddress); + +/// @dev Thrown when the token is not configured +/// @param _tokenAddress the address of the token +error TokenIsNotConfigured(address _tokenAddress); + +/// @dev Thrown when the token is already listed for the investor +/// @param _tokenAddress the address of the token +/// @param _investorAddress the investor address (a wallet or an ONCHAINID address) +error TokenAlreadyListed(address _tokenAddress, address _investorAddress); + +/// @dev Thrown when the token is not listed for the investor +/// @param _tokenAddress the address of the token +/// @param _investorAddress the investor address (a wallet or an ONCHAINID address) +error TokenIsNotListed(address _tokenAddress, address _investorAddress); + +/// @dev Thrown when the identity is not found +/// @param _tokenAddress the address of the token +/// @param _userAddress the user address (a wallet or an ONCHAINID address) +error IdentityNotFound(address _tokenAddress, address _userAddress); + +/// @dev Thrown when the listing type is invalid for configuration +/// @param _listingType the listing type +error InvalidListingTypeForConfiguration(ListingType _listingType); + +contract TokenListingRestrictionsModule is AbstractModuleUpgradeable { + /// Mapping between token and listing type + mapping(address => ListingType) private _tokenListingType; + + /// Mapping between tokenAddress and investor (wallet or OID address) + /// and listing status (whitelisted or blacklisted depending on the listing type of the token) + mapping(address => mapping(address => bool)) private _tokenInvestorListingStatus; + + /// events + + /// @dev This event is emitted whenever a token is configured with a non-zero listing type + /// @param _tokenAddress the address of the configured token + /// @param _listingType the configured listing type for the token (1: WHITELISTING, 2: BLACKLISTING) + event TokenListingConfigured(address _tokenAddress, ListingType _listingType); + + /// @dev This event is emitted whenever a token is listed (whitelisted or blacklisted) for an investor + /// @param _tokenAddress the address of the listed token + /// @param _investorAddress the investor address (a wallet or an ONCHAINID address) + event TokenListed(address _tokenAddress, address _investorAddress); + + /// @dev This event is emitted whenever a token is unlisted for an investor + /// @param _tokenAddress the address of the unlisted token + /// @param _investorAddress the investor address (a wallet or an ONCHAINID address) + event TokenUnlisted(address _tokenAddress, address _investorAddress); + + /// functions + /** + * @dev initializes the contract and sets the initial state. + * @notice This function should only be called once during the contract deployment. + */ + function initialize() external initializer { + __AbstractModule_init(); + } + + /** + * @dev Configures the listing type of a token + * Can only be called once per token + * @param _listingType can be WHITELISTING(1) or BLACKLISTING(2) + * WHITELISTING(1): investors must whitelist/allow the token address in order to receive it + * BLACKLISTING(2): investors can receive this token by default. If they do not want to receive it, + * they need to blacklist/disallow it. + * Only the owner of the Compliance smart contract can call this function + */ + function configureToken(ListingType _listingType) external onlyComplianceCall { + address tokenAddress = _getBoundTokenAddress(msg.sender); + if (_listingType == ListingType.NOT_CONFIGURED) { + revert InvalidListingTypeForConfiguration(_listingType); + } + + if (_tokenListingType[tokenAddress] != ListingType.NOT_CONFIGURED) { + revert TokenAlreadyConfigured(tokenAddress); + } + + _tokenListingType[tokenAddress] = _listingType; + emit TokenListingConfigured(tokenAddress, _listingType); + } + + /** + * @dev Lists multiple tokens for the investor (caller) + * If the token listing type is WHITELISTING, it will be whitelisted/allowed for the investor. + * If the token listing type is BLACKLISTING, it will be blaclisted/disallowed for the investor. + * @param _tokenAddresses is the array of addresses of tokens to be listed + * @param _addressType can be WALLET(0) or ONCHAINID(1) + * If it is WALLET, the token will be listed only for the caller wallet address + * If it is ONCHAINID, the token will be listed for the ONCHAINID (it will be applied to all wallet addresses of the OID) + * It will revert if the listing type of the token is not configured + */ + function batchListTokens(address[] calldata _tokenAddresses, InvestorAddressType _addressType) external { + for (uint256 i = 0; i < _tokenAddresses.length; i++) { + listToken(_tokenAddresses[i], _addressType); + } + } + + /** + * @dev Unlists multiple tokens for the investor (caller) + * @param _tokenAddresses is the array of addresses of tokens to be unlisted + * @param _addressType can be WALLET(0) or ONCHAINID(1) + * If it is WALLET, the token will be unlisted only for the caller wallet address + * If it is ONCHAINID, the token will be unlisted for the ONCHAINID + */ + function batchUnlistTokens(address[] calldata _tokenAddresses, InvestorAddressType _addressType) external { + for (uint256 i = 0; i < _tokenAddresses.length; i++) { + unlistToken(_tokenAddresses[i], _addressType); + } + } + + /** + * @dev See {IModule-moduleTransferAction}. + * no transfer action required in this module + */ + // solhint-disable-next-line no-empty-blocks + function moduleTransferAction(address _from, address _to, uint256 _value) external override onlyComplianceCall {} + + /** + * @dev See {IModule-moduleMintAction}. + * no mint action required in this module + */ + // solhint-disable-next-line no-empty-blocks + function moduleMintAction(address _to, uint256 _value) external override onlyComplianceCall {} + + /** + * @dev See {IModule-moduleBurnAction}. + * no burn action required in this module + */ + // solhint-disable-next-line no-empty-blocks + function moduleBurnAction(address _from, uint256 _value) external override onlyComplianceCall {} + + /** + * @dev See {IModule-moduleCheck}. + * checks whether the _to address allows this token to receive + * returns TRUE if the token is allowed for the address of _to (or for the OID of _to) + * returns FALSE if the token is not allowed for the address of _to + */ + function moduleCheck( + address /*_from*/, + address _to, + uint256 /*_value*/, + address _compliance + ) external view override returns (bool) { + if (_to == address(0)) { + return true; + } + + address tokenAddress = _getBoundTokenAddress(_compliance); + ListingType listingType = _tokenListingType[tokenAddress]; + if (listingType == ListingType.NOT_CONFIGURED) { + return true; + } + + bool listed = _tokenInvestorListingStatus[tokenAddress][_to] + || _tokenInvestorListingStatus[tokenAddress][_getIdentityByTokenAddress(tokenAddress, _to)]; + + if (listingType == ListingType.BLACKLISTING) { + return !listed; + } + + return listed; + } + + /** + * @dev Returns the configured listing type of the given token address + * @param _tokenAddress the token smart contract to be checked + * returns the listing type of the token: + * NOT_CONFIGURED(0): The token is not configured for this module yet + * WHITELISTING(1): investors must whitelist/allow the token address in order to receive it + * BLACKLISTING(2): investors can receive this token by default. If they do not want to receive it, + * they need to blacklist/disallow it. + */ + function getTokenListingType(address _tokenAddress) external view returns (ListingType) { + return _tokenListingType[_tokenAddress]; + } + + /** + * @dev Returns the listing status of the given investor address for the token + * @param _tokenAddress the token smart contract to be checked + * @param _investorAddress the WALLET or ONCHAINID address of an investor to be checked + * returns the listing status of the given investor. + * If it is true: + * - if the listing type of the token is WHITELISTING(1): investor can receive this token. + * - if the listing type of the token is BLACKLISTING(2): investor can not receive this token. + * If it is false: + * - if the listing type of the token is WHITELISTING(1): investor can not receive this token. + * - if the listing type of the token is BLACKLISTING(2): investor can receive this token. + */ + function getInvestorListingStatus(address _tokenAddress, address _investorAddress) external view returns (bool) { + return _tokenInvestorListingStatus[_tokenAddress][_investorAddress]; + } + + /** + * @dev See {IModule-canComplianceBind}. + */ + function canComplianceBind(address /*_compliance*/) external pure override returns (bool) { + return true; + } + + /** + * @dev See {IModule-isPlugAndPlay}. + */ + function isPlugAndPlay() external pure override returns (bool) { + return true; + } + + /** + * @dev Lists a token for the investor (caller) + * If the token listing type is WHITELISTING, it will be whitelisted/allowed for the investor. + * If the token listing type is BLACKLISTING, it will be blaclisted/disallowed for the investor. + * @param _tokenAddress is the address of the token to be listed + * @param _addressType can be WALLET(0) or ONCHAINID(1) + * If it is WALLET, the token will be listed only for the caller wallet address + * If it is ONCHAINID, the token will be listed for the ONCHAINID (it will be applied to all wallet addresses of the OID) + * It will revert if the listing type of the token is not configured + */ + function listToken(address _tokenAddress, InvestorAddressType _addressType) public { + if (_tokenListingType[_tokenAddress] == ListingType.NOT_CONFIGURED) { + revert TokenIsNotConfigured(_tokenAddress); + } + + address investorAddress = _getInvestorAddressByAddressType(_tokenAddress, msg.sender, _addressType); + if (_tokenInvestorListingStatus[_tokenAddress][investorAddress]) { + revert TokenAlreadyListed(_tokenAddress, investorAddress); + } + + _tokenInvestorListingStatus[_tokenAddress][investorAddress] = true; + emit TokenListed(_tokenAddress, investorAddress); + } + + /** + * @dev Unlists a token for the investor (caller) + * @param _tokenAddress is the address of the token to be unlisted + * @param _addressType can be WALLET(0) or ONCHAINID(1) + * If it is WALLET, the token will be unlisted only for the caller wallet address + * If it is ONCHAINID, the token will be unlisted for the ONCHAINID + */ + function unlistToken(address _tokenAddress, InvestorAddressType _addressType) public { + address investorAddress = _getInvestorAddressByAddressType(_tokenAddress, msg.sender, _addressType); + if (!_tokenInvestorListingStatus[_tokenAddress][investorAddress]) { + revert TokenIsNotListed(_tokenAddress, investorAddress); + } + + _tokenInvestorListingStatus[_tokenAddress][investorAddress] = false; + emit TokenUnlisted(_tokenAddress, investorAddress); + } + + /** + * @dev See {IModule-name}. + */ + function name() public pure returns (string memory _name) { + return "TokenListingRestrictionsModule"; + } + + function _getInvestorAddressByAddressType( + address _tokenAddress, + address _userAddress, + InvestorAddressType _addressType + ) internal view returns (address) { + if (_addressType == InvestorAddressType.WALLET) { + return _userAddress; + } + + address identity = _getIdentityByTokenAddress(_tokenAddress, _userAddress); + if (identity == address(0)) { + revert IdentityNotFound(_tokenAddress, _userAddress); + } + + return identity; + } + + function _getIdentityByTokenAddress(address _tokenAddress, address _userAddress) internal view returns (address) { + return address(IToken(_tokenAddress).identityRegistry().identity(_userAddress)); + } + + function _getBoundTokenAddress(address _compliance) internal view returns (address) { + return IModularCompliance(_compliance).getTokenBound(); + } +} \ No newline at end of file diff --git a/contracts/compliance/modular/modules/TransferFeesModule.sol b/contracts/compliance/modular/modules/TransferFeesModule.sol index 657035fe..b1584ade 100644 --- a/contracts/compliance/modular/modules/TransferFeesModule.sol +++ b/contracts/compliance/modular/modules/TransferFeesModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity 0.8.17; diff --git a/contracts/compliance/modular/modules/TransferRestrictModule.sol b/contracts/compliance/modular/modules/TransferRestrictModule.sol index 971eb92b..038c1137 100644 --- a/contracts/compliance/modular/modules/TransferRestrictModule.sol +++ b/contracts/compliance/modular/modules/TransferRestrictModule.sol @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 +// This contract is also licensed under the Creative Commons Attribution-NonCommercial 4.0 International License. // // :+#####%%%%%%%%%%%%%%+ // .-*@@@%+.:+%@@@@@%%#***%@@%= @@ -44,7 +45,7 @@ * T-REX is a suite of smart contracts implementing the ERC-3643 standard and * developed by Tokeny to manage and transfer financial assets on EVM blockchains * - * Copyright (C) 2023, Tokeny sàrl. + * Copyright (C) 2024, Tokeny sàrl. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -58,6 +59,11 @@ * * You should have received a copy of the GNU General Public License * along with this program. If not, see . + * + * This specific smart contract is also licensed under the Creative Commons + * Attribution-NonCommercial 4.0 International License (CC-BY-NC-4.0), + * which prohibits commercial use. For commercial inquiries, please contact + * Tokeny sàrl for licensing options. */ pragma solidity ^0.8.17; @@ -172,6 +178,10 @@ contract TransferRestrictModule is AbstractModuleUpgradeable { uint256 /*_value*/, address _compliance ) external view override returns (bool) { + if (_from == address(0) || _to == address(0)) { + return true; + } + if(_allowedUserAddresses[_compliance][_from]) { return true; } diff --git a/index.d.ts b/index.d.ts index 5aa94f57..e72435b8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -69,6 +69,8 @@ export namespace contracts { export const SupplyLimitModule: ContractJSON; export const TransferFeesModule: ContractJSON; export const TransferRestrictModule: ContractJSON; + + export const TokenListingRestrictionsModule: ContractJSON; } export namespace interfaces { diff --git a/index.js b/index.js index 5d2563ec..0d843d22 100644 --- a/index.js +++ b/index.js @@ -72,6 +72,7 @@ const TimeTransfersLimitsModule = require('./artifacts/contracts/compliance/modu const SupplyLimitModule = require('./artifacts/contracts/compliance/modular/modules/SupplyLimitModule.sol/SupplyLimitModule.json'); const TransferFeesModule = require('./artifacts/contracts/compliance/modular/modules/TransferFeesModule.sol/TransferFeesModule.json'); const TransferRestrictModule = require('./artifacts/contracts/compliance/modular/modules/TransferRestrictModule.sol/TransferRestrictModule.json'); +const TokenListingRestrictionsModule = require('./artifacts/contracts/compliance/modular/modules/TokenListingRestrictionsModule.sol/TokenListingRestrictionsModule.json'); module.exports = { contracts: { @@ -135,6 +136,7 @@ module.exports = { SupplyLimitModule, TransferFeesModule, TransferRestrictModule, + TokenListingRestrictionsModule, }, interfaces: { IToken, diff --git a/package.json b/package.json index 029ecf50..a693727f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@tokenysolutions/t-rex", - "version": "4.1.5", + "version": "4.1.6", "description": "A fully compliant environment for the issuance and use of tokenized securities.", "main": "index.js", "directories": { diff --git a/test/compliances/module-time-transfer-limits.test.ts b/test/compliances/module-time-transfer-limits.test.ts index c3120b61..dfd47404 100644 --- a/test/compliances/module-time-transfer-limits.test.ts +++ b/test/compliances/module-time-transfer-limits.test.ts @@ -231,6 +231,191 @@ describe('Compliance Module: TimeTransferLimits', () => { }); }); + describe('.batchSetTimeTransferLimit', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await expect( + context.contracts.complianceModule.batchSetTimeTransferLimit([ + { limitTime: 1, limitValue: 100 }, + { limitTime: 2, limitValue: 200 }, + ]), + ).to.revertedWith('only bound compliance can call'); + }); + }); + + describe('when calling via compliance', () => { + it('should create the limits', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + const tx = await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface([ + 'function batchSetTimeTransferLimit(tuple(uint32 limitTime, uint256 limitValue)[] _limits)', + ]).encodeFunctionData('batchSetTimeTransferLimit', [ + [ + { limitTime: 1, limitValue: 100 }, + { limitTime: 2, limitValue: 200 }, + ], + ]), + context.contracts.complianceModule.address, + ); + + await expect(tx) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitUpdated') + .withArgs(context.contracts.compliance.address, 1, 100) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitUpdated') + .withArgs(context.contracts.compliance.address, 2, 200); + }); + }); + }); + + describe('.removeTimeTransferLimit', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await expect(context.contracts.complianceModule.removeTimeTransferLimit(10)).to.revertedWith('only bound compliance can call'); + }); + }); + + describe('when calling via compliance', () => { + describe('when limit time is missing', () => { + it('should revert', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await expect( + context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface(['function removeTimeTransferLimit(uint32 _limitTime)']).encodeFunctionData('removeTimeTransferLimit', [10]), + context.contracts.complianceModule.address, + ), + ).to.be.revertedWithCustomError(context.contracts.complianceModule, `LimitTimeNotFound`); + }); + }); + + describe('when limit time exist', () => { + describe('when limit time is the last element', () => { + it('should remove the limit', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface([ + 'function batchSetTimeTransferLimit(tuple(uint32 limitTime, uint256 limitValue)[] _limit)', + ]).encodeFunctionData('batchSetTimeTransferLimit', [ + [ + { limitTime: 1, limitValue: 100 }, + { limitTime: 2, limitValue: 200 }, + { limitTime: 3, limitValue: 300 }, + ], + ]), + context.contracts.complianceModule.address, + ); + + const tx = await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface(['function removeTimeTransferLimit(uint32 _limitTime)']).encodeFunctionData('removeTimeTransferLimit', [3]), + context.contracts.complianceModule.address, + ); + + await expect(tx) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitRemoved') + .withArgs(context.contracts.compliance.address, 3); + + const limits = await context.contracts.complianceModule.getTimeTransferLimits(context.suite.compliance.address); + expect(limits.length).to.be.eq(2); + expect(limits[0].limitTime).to.be.eq(1); + expect(limits[0].limitValue).to.be.eq(100); + expect(limits[1].limitTime).to.be.eq(2); + expect(limits[1].limitValue).to.be.eq(200); + }); + }); + + describe('when limit time is not the last element', () => { + it('should remove the limit', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface([ + 'function batchSetTimeTransferLimit(tuple(uint32 limitTime, uint256 limitValue)[] _limit)', + ]).encodeFunctionData('batchSetTimeTransferLimit', [ + [ + { limitTime: 1, limitValue: 100 }, + { limitTime: 2, limitValue: 200 }, + { limitTime: 3, limitValue: 300 }, + ], + ]), + context.contracts.complianceModule.address, + ); + + const tx = await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface(['function removeTimeTransferLimit(uint32 _limitTime)']).encodeFunctionData('removeTimeTransferLimit', [2]), + context.contracts.complianceModule.address, + ); + + await expect(tx) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitRemoved') + .withArgs(context.contracts.compliance.address, 2); + + const limits = await context.contracts.complianceModule.getTimeTransferLimits(context.suite.compliance.address); + expect(limits.length).to.be.eq(2); + expect(limits[0].limitTime).to.be.eq(1); + expect(limits[0].limitValue).to.be.eq(100); + expect(limits[1].limitTime).to.be.eq(3); + expect(limits[1].limitValue).to.be.eq(300); + }); + }); + }); + }); + }); + + describe('.batchRemoveTimeTransferLimit', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await expect(context.contracts.complianceModule.batchRemoveTimeTransferLimit([10, 20])).to.revertedWith('only bound compliance can call'); + }); + }); + + describe('when calling via compliance', () => { + it('should remove the limits', async () => { + const context = await loadFixture(deployTimeTransferLimitsFixture); + + await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface(['function batchSetTimeTransferLimit(tuple(uint32 limitTime, uint256 limitValue)[] _limit)']).encodeFunctionData( + 'batchSetTimeTransferLimit', + [ + [ + { limitTime: 1, limitValue: 100 }, + { limitTime: 2, limitValue: 200 }, + { limitTime: 3, limitValue: 300 }, + ], + ], + ), + context.contracts.complianceModule.address, + ); + + const tx = await context.contracts.compliance.callModuleFunction( + new ethers.utils.Interface(['function batchRemoveTimeTransferLimit(uint32[] _limitTimes)']).encodeFunctionData( + 'batchRemoveTimeTransferLimit', + [[1, 3]], + ), + context.contracts.complianceModule.address, + ); + + await expect(tx) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitRemoved') + .withArgs(context.contracts.compliance.address, 1) + .to.emit(context.contracts.complianceModule, 'TimeTransferLimitRemoved') + .withArgs(context.contracts.compliance.address, 3); + + const limits = await context.contracts.complianceModule.getTimeTransferLimits(context.suite.compliance.address); + expect(limits.length).to.be.eq(1); + expect(limits[0].limitTime).to.be.eq(2); + expect(limits[0].limitValue).to.be.eq(200); + }); + }); + }); + describe('.getTimeTransferLimits', () => { describe('when there is no time transfer limit', () => { it('should return empty array', async () => { diff --git a/test/compliances/module-token-listing-restrictions.test.ts b/test/compliances/module-token-listing-restrictions.test.ts new file mode 100644 index 00000000..9165f24d --- /dev/null +++ b/test/compliances/module-token-listing-restrictions.test.ts @@ -0,0 +1,710 @@ +import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; +import { ethers, upgrades } from 'hardhat'; +import { expect } from 'chai'; +import { deploySuiteWithModularCompliancesFixture } from '../fixtures/deploy-full-suite.fixture'; +import { deployComplianceFixture } from '../fixtures/deploy-compliance.fixture'; + +async function deployTokenListingRestrictionsFullSuite() { + const context = await loadFixture(deploySuiteWithModularCompliancesFixture); + const module = await ethers.deployContract('TokenListingRestrictionsModule'); + const proxy = await ethers.deployContract('ModuleProxy', [module.address, module.interface.encodeFunctionData('initialize')]); + const complianceModule = await ethers.getContractAt('TokenListingRestrictionsModule', proxy.address); + + await context.suite.compliance.bindToken(context.suite.token.address); + await context.suite.compliance.addModule(complianceModule.address); + + return { + ...context, + suite: { + ...context.suite, + complianceModule, + }, + }; +} + +describe('Compliance Module: TokenListingRestrictions', () => { + it('should deploy the TokenListingRestrictions contract and bind it to the compliance', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + expect(context.suite.complianceModule.address).not.to.be.undefined; + expect(await context.suite.compliance.isModuleBound(context.suite.complianceModule.address)).to.be.true; + }); + + describe('.name', () => { + it('should return the name of the module', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + expect(await context.suite.complianceModule.name()).to.be.equal('TokenListingRestrictionsModule'); + }); + }); + + describe('.isPlugAndPlay', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + expect(await context.suite.complianceModule.isPlugAndPlay()).to.be.true; + }); + }); + + describe('.canComplianceBind', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const complianceModule = await ethers.deployContract('TokenListingRestrictionsModule'); + expect(await complianceModule.canComplianceBind(context.suite.compliance.address)).to.be.true; + }); + }); + + describe('.owner', () => { + it('should return owner', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect(context.suite.complianceModule.owner()).to.eventually.be.eq(context.accounts.deployer.address); + }); + }); + + describe('.initialize', () => { + it('should be called only once', async () => { + // given + const { + accounts: { deployer }, + } = await loadFixture(deployComplianceFixture); + const module = (await ethers.deployContract('TokenListingRestrictionsModule')).connect(deployer); + await module.initialize(); + + // when & then + await expect(module.initialize()).to.be.revertedWith('Initializable: contract is already initialized'); + expect(await module.owner()).to.be.eq(deployer.address); + }); + }); + + describe('.transferOwnership', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).transferOwnership(context.accounts.bobWallet.address), + ).to.revertedWith('Ownable: caller is not the owner'); + }); + }); + + describe('when calling with owner account', () => { + it('should transfer ownership', async () => { + // given + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + // when + const tx = await context.suite.complianceModule.connect(context.accounts.deployer).transferOwnership(context.accounts.bobWallet.address); + + // then + await expect(tx) + .to.emit(context.suite.complianceModule, 'OwnershipTransferred') + .withArgs(context.accounts.deployer.address, context.accounts.bobWallet.address); + + const owner = await context.suite.complianceModule.owner(); + expect(owner).to.eq(context.accounts.bobWallet.address); + }); + }); + }); + + describe('.upgradeTo', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect(context.suite.complianceModule.connect(context.accounts.aliceWallet).upgradeTo(ethers.constants.AddressZero)).to.revertedWith( + 'Ownable: caller is not the owner', + ); + }); + }); + + describe('when calling with owner account', () => { + it('should upgrade proxy', async () => { + // given + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const newImplementation = await ethers.deployContract('TokenListingRestrictionsModule'); + + // when + await context.suite.complianceModule.connect(context.accounts.deployer).upgradeTo(newImplementation.address); + + // then + const implementationAddress = await upgrades.erc1967.getImplementationAddress(context.suite.complianceModule.address); + expect(implementationAddress).to.eq(newImplementation.address); + }); + }); + }); + + describe('.configureToken', () => { + describe('when calling directly', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect(context.suite.complianceModule.configureToken(1)).to.revertedWith('only bound compliance can call'); + }); + }); + + describe('when calling via compliance', () => { + describe('when given listing type is zero (NOT_CONFIGURED)', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect( + context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [0]), + context.suite.complianceModule.address, + ), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `InvalidListingTypeForConfiguration`); + }); + }); + + describe('when token is already configured', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await expect( + context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [2]), + context.suite.complianceModule.address, + ), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenAlreadyConfigured`); + }); + }); + + describe('when token is not configured before', () => { + it('should configure the token', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + const tx = await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await expect(tx).to.emit(context.suite.complianceModule, 'TokenListingConfigured').withArgs(context.suite.token.address, 1); + }); + }); + }); + }); + + describe('.listToken', () => { + describe('when token is not configured', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenIsNotConfigured`); + }); + }); + + describe('when token is configured', () => { + describe('when token is listed before', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenAlreadyListed`); + }); + }); + + describe('when token is not already listed', () => { + describe('when the investor address type is WALLET', () => { + it('should list the token', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenListed') + .withArgs(context.suite.token.address, context.accounts.aliceWallet.address); + }); + }); + + describe('when the investor address type is ONCHAINID', () => { + describe('when identity does not exist', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await expect( + context.suite.complianceModule.connect(context.accounts.anotherWallet).listToken(context.suite.token.address, 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `IdentityNotFound`); + }); + }); + + describe('when identity exists', () => { + it('should list the token', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenListed') + .withArgs(context.suite.token.address, context.identities.aliceIdentity.address); + }); + }); + }); + }); + }); + }); + + describe('.batchListTokens', () => { + describe('when token is not configured', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).batchListTokens([context.suite.token.address], 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenIsNotConfigured`); + }); + }); + + describe('when token is configured', () => { + describe('when token is listed before', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).batchListTokens([context.suite.token.address], 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenAlreadyListed`); + }); + }); + + describe('when token is not already listed', () => { + describe('when the investor address type is WALLET', () => { + it('should list tokens', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).batchListTokens([context.suite.token.address], 0); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenListed') + .withArgs(context.suite.token.address, context.accounts.aliceWallet.address); + }); + }); + + describe('when the investor address type is ONCHAINID', () => { + it('should list tokens', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).batchListTokens([context.suite.token.address], 1); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenListed') + .withArgs(context.suite.token.address, context.identities.aliceIdentity.address); + }); + }); + }); + }); + }); + + describe('.unlistToken', () => { + describe('when token is not listed', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).unlistToken(context.suite.token.address, 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenIsNotListed`); + }); + }); + + describe('when token is listed', () => { + describe('when the investor address type is WALLET', () => { + it('should unlist the token', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).unlistToken(context.suite.token.address, 0); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenUnlisted') + .withArgs(context.suite.token.address, context.accounts.aliceWallet.address); + }); + }); + + describe('when the investor address type is ONCHAINID', () => { + it('should unlist the token', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).unlistToken(context.suite.token.address, 1); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenUnlisted') + .withArgs(context.suite.token.address, context.identities.aliceIdentity.address); + }); + }); + }); + }); + + describe('.batchUnlistTokens', () => { + describe('when token is not listed', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await expect( + context.suite.complianceModule.connect(context.accounts.aliceWallet).batchUnlistTokens([context.suite.token.address], 1), + ).to.be.revertedWithCustomError(context.suite.complianceModule, `TokenIsNotListed`); + }); + }); + + describe('when token is not listed', () => { + describe('when the investor address type is WALLET', () => { + it('should unlist tokens', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).batchUnlistTokens([context.suite.token.address], 0); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenUnlisted') + .withArgs(context.suite.token.address, context.accounts.aliceWallet.address); + }); + }); + + describe('when the investor address type is ONCHAINID', () => { + it('should unlist tokens', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + const tx = await context.suite.complianceModule.connect(context.accounts.aliceWallet).batchUnlistTokens([context.suite.token.address], 1); + + await expect(tx) + .to.emit(context.suite.complianceModule, 'TokenUnlisted') + .withArgs(context.suite.token.address, context.identities.aliceIdentity.address); + }); + }); + }); + }); + + describe('.getTokenListingType', () => { + describe('when token is not configured', () => { + it('should return NOT_CONFIGURED(0)', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const result = await context.suite.complianceModule.getTokenListingType(context.suite.token.address); + expect(result).to.be.eq(0); + }); + }); + + describe('when token is configured', () => { + it('should return token listing type', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.getTokenListingType(context.suite.token.address); + expect(result).to.be.eq(1); + }); + }); + }); + + describe('.getInvestorListingStatus', () => { + describe('when token is not listed', () => { + it('should return false', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const result = await context.suite.complianceModule.getInvestorListingStatus( + context.suite.token.address, + context.accounts.aliceWallet.address, + ); + expect(result).to.be.false; + }); + }); + + describe('when token is listed', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + const result = await context.suite.complianceModule.getInvestorListingStatus( + context.suite.token.address, + context.accounts.aliceWallet.address, + ); + expect(result).to.be.true; + }); + }); + }); + + describe('.moduleCheck', () => { + describe('when receiver is the null address', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const from = context.accounts.aliceWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.moduleCheck(from, ethers.constants.AddressZero, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + + describe('when token is not configured', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const from = context.accounts.aliceWallet.address; + const to = context.accounts.bobWallet.address; + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + + describe('when the listing type is WHITELISTING', () => { + describe('when token is not listed', () => { + it('should return false', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.false; + }); + }); + + describe('when token is listed for the wallet', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + + describe('when token is listed for the OID', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [1]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + }); + + describe('when the listing type is BLACKLISTING', () => { + describe('when token is not listed', () => { + it('should return true', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [2]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + + describe('when token is listed for the wallet', () => { + it('should return false', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [2]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 0); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.false; + }); + }); + + describe('when token is listed for the OID', () => { + it('should return false', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + const to = context.accounts.aliceWallet.address; + const from = context.accounts.bobWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function configureToken(uint8 _listingType)']).encodeFunctionData('configureToken', [2]), + context.suite.complianceModule.address, + ); + + await context.suite.complianceModule.connect(context.accounts.aliceWallet).listToken(context.suite.token.address, 1); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.false; + }); + }); + }); + }); + + describe('.moduleMintAction', () => { + describe('when calling from a random wallet', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect(context.suite.complianceModule.moduleMintAction(context.accounts.anotherWallet.address, 10)).to.be.revertedWith( + 'only bound compliance can call', + ); + }); + }); + + describe('when calling as the compliance', () => { + it('should do nothing', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect( + context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function moduleMintAction(address, uint256)']).encodeFunctionData('moduleMintAction', [ + context.accounts.anotherWallet.address, + 10, + ]), + context.suite.complianceModule.address, + ), + ).to.eventually.be.fulfilled; + }); + }); + }); + + describe('.moduleBurnAction', () => { + describe('when calling from a random wallet', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect(context.suite.complianceModule.moduleBurnAction(context.accounts.anotherWallet.address, 10)).to.be.revertedWith( + 'only bound compliance can call', + ); + }); + }); + + describe('when calling as the compliance', () => { + it('should do nothing', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect( + context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function moduleBurnAction(address, uint256)']).encodeFunctionData('moduleBurnAction', [ + context.accounts.anotherWallet.address, + 10, + ]), + context.suite.complianceModule.address, + ), + ).to.eventually.be.fulfilled; + }); + }); + }); + + describe('.moduleTransfer', () => { + describe('when calling from a random wallet', () => { + it('should revert', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect( + context.suite.complianceModule.moduleTransferAction(context.accounts.aliceWallet.address, context.accounts.anotherWallet.address, 10), + ).to.be.revertedWith('only bound compliance can call'); + }); + }); + + describe('when calling as the compliance', () => { + it('should do nothing', async () => { + const context = await loadFixture(deployTokenListingRestrictionsFullSuite); + + await expect( + context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function moduleTransferAction(address _from, address _to, uint256 _value)']).encodeFunctionData( + 'moduleTransferAction', + [context.accounts.aliceWallet.address, context.accounts.anotherWallet.address, 80], + ), + context.suite.complianceModule.address, + ), + ).to.eventually.be.fulfilled; + }); + }); + }); +}); diff --git a/test/compliances/module-transfer-restrict.test.ts b/test/compliances/module-transfer-restrict.test.ts index 3a560dff..65f4946d 100644 --- a/test/compliances/module-transfer-restrict.test.ts +++ b/test/compliances/module-transfer-restrict.test.ts @@ -325,6 +325,38 @@ describe('Compliance Module: TransferRestrict', () => { expect(result).to.be.true; }); }); + + describe('when sender is null address', () => { + it('should return true', async () => { + const context = await loadFixture(deployTransferRestrictFullSuite); + const to = context.accounts.anotherWallet.address; + const from = '0x0000000000000000000000000000000000000000'; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function allowUser(address _userAddress)']).encodeFunctionData('allowUser', [from]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); + + describe('when receiver is null address', () => { + it('should return true', async () => { + const context = await loadFixture(deployTransferRestrictFullSuite); + const to = '0x0000000000000000000000000000000000000000'; + const from = context.accounts.anotherWallet.address; + + await context.suite.compliance.callModuleFunction( + new ethers.utils.Interface(['function allowUser(address _userAddress)']).encodeFunctionData('allowUser', [from]), + context.suite.complianceModule.address, + ); + + const result = await context.suite.complianceModule.moduleCheck(from, to, 10, context.suite.compliance.address); + expect(result).to.be.true; + }); + }); }); describe('.moduleMintAction', () => {