Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add token listing restrictions module #197

Merged
merged 6 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file.

### Added

- 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.

- **Default Allowance Mechanism**:
- Introduced a new feature allowing the contract owner to set certain addresses as trusted external smart contracts, enabling them to use `transferFrom` without requiring an explicit allowance from users. By default, users are opted in, allowing these contracts to have an "infinite allowance". Users can opt-out if they prefer to control allowances manually.
- Added custom errors and events to provide better feedback and traceability:
Expand Down Expand Up @@ -294,4 +299,4 @@ Version 4.0.0 of TREX has been successfully audited by Hacken [more details here
- changed required key for roles on AgentManager contract (MANAGEMENT key -> EXECUTION key)
- changed required key for roles on OwnerManager contract (MANAGEMENT key -> EXECUTION key)
- import interfaces from openzeppelin instead of local copy
- contract name : **Storage** -> **TokenStorage**
- contract name : **Storage** -> **TokenStorage**
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
// SPDX-License-Identifier: GPL-3.0
//
// :+#####%%%%%%%%%%%%%%+
// .-*@@@%+.:+%@@@@@%%#***%@@%=
// :=*%@@@#=. :#@@% *@@@%=
// .-+*%@%*-.:+%@@@@@@+. -*+: .=#. :%@@@%-
// :=*@@@@%%@@@@@@@@@%@@@- .=#@@@%@%= =@@@@#.
// -=+#%@@%#*=:. :%@@@@%. -*@@#*@@@@@@@#=:- *@@@@+
// =@@%=:. :=: *@@@@@%#- =%*%@@@@#+-. =+ :%@@@%-
// -@@%. .+@@@ =+=-. @@#- +@@@%- =@@@@%:
// :@@@. .+@@#%: : .=*=-::.-%@@@+*@@= +@@@@#.
// %@@: +@%%* =%@@@@@@@@@@@#. .*@%- +@@@@*.
// #@@= .+@@@@%:=*@@@@@- :%@%: .*@@@@+
// *@@* +@@@#-@@%-:%@@* +@@#. :%@@@@-
// -@@% .:-=++*##%%%@@@@@@@@@@@@*. :@+.@@@%: .#@@+ =@@@@#:
// .@@@*-+*#%%%@@@@@@@@@@@@@@@@%%#**@@%@@@. *@=*@@# :#@%= .#@@@@#-
// -%@@@@@@@@@@@@@@@*+==-:-@@@= *@# .#@*-=*@@@@%= -%@@@* =@@@@@%-
// -+%@@@#. %@%%= -@@:+@: -@@* *@@*-:: -%@@%=. .*@@@@@#
// *@@@* +@* *@@##@@- #@*@@+ -@@= . :+@@@#: .-+@@@%+-
// +@@@%*@@:..=@@@@* .@@@* .#@#. .=+- .=%@@@*. :+#@@@@*=:
// =@@@@%@@@@@@@@@@@@@@@@@@@@@@%- :+#*. :*@@@%=. .=#@@@@%+:
// .%@@= ..... .=#@@+. .#@@@*: -*%@@@@%+.
// +@@#+===---:::... .=%@@*- +@@@+. -*@@@@@%+.
// -@@@@@@@@@@@@@@@@@@@@@@%@@@@= -@@@+ -#@@@@@#=.
// ..:::---===+++***###%%%@@@#- .#@@+ -*@@@@@#=.
// @@@@@@+. +@@*. .+@@@@@%=.
// -@@@@@= =@@%: -#@@@@%+.
// +@@@@@. =@@@= .+@@@@@*:
// #@@@@#:%@@#. :*@@@@#-
// @@@@@%@@@= :#@@@@+.
// :@@@@@@@#.:#@@@%-
// +@@@@@@-.*@@@*:
// #@@@@#.=@@@+.
// @@@@+-%@%=
// :@@@#%@%=
// +@@@@%-
// :#%%=
//
/**
* NOTICE
*
* The T-REX software is licensed under a proprietary license or the GPL v.3.
* If you choose to receive it under the GPL v.3 license, the following applies:
* T-REX is a suite of smart contracts implementing the ERC-3643 standard and
* developed by Tokeny to manage and transfer financial assets on EVM blockchains
*
* Copyright (C) 2023, Tokeny sàrl.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

pragma solidity 0.8.27;

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
}


/// 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);

/// 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 token => 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 token => mapping(address investor => bool)) private _tokenInvestorListingStatus;

/// 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();
}
}
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export namespace contracts {
export const SupplyLimitModule: ContractJSON;
export const TransferFeesModule: ContractJSON;
export const TransferRestrictModule: ContractJSON;
export const TokenListingRestrictionsModule: ContractJSON;
}

export namespace interfaces {
Expand Down
Loading
Loading