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', () => {