--- eip: 3643 title: T-REX - Token for Regulated EXchanges description: An institutional grade security token standard that provides interfaces for the management and compliant transfer of security tokens. author: Joachim Lebrun (@Joachim-Lebrun), Tony Malghem (@TonyMalghem), Kevin Thizy (@Nakasar), Luc Falempin (@lfalempin), Adam Boudjemaa (@Aboudjem) type: Standards Track category: ERC status: Stagnant requires: 20, 1822 discussions-to: https://ethereum-magicians.org/t/eip-3643-proposition-of-the-t-rex-token-standard-for-securities/6844 created: 2021-07-09 --- ## Simple Summary The T-REX token is This standard ## Abstract Standards should be backwards compatible with [ERC-20](./eip-20.md) and should be able to interact with [ERC-735](https://github.com/ethereum/EIPs/issues/735) to validate the claims linked to an [`ONCHAINID`](https://github.com/onchain-id/solidity), based on [ERC-734](https://github.com/ethereum/EIPs/issues/734) and ERC-735. The standard defines several interfaces that are described hereunder: - Token - Identity Registry - Identity Registry Storage - Compliance - Trusted Issuers Registry - Claim Topics Registry ## Motivation Give standard interfaces for security tokens issued on Ethereum, through which any third party could interact with the security token. The functions described by these interfaces vary and allow the appropriate users to call a range of different actions, such as forced transfers, freeze tokens (partially or totally on a wallet or even freeze the entire token), minting, burning, recover lost tokens (if an investor loses access to his wallet), etc. The following requirements have been compiled following discussions with parties across financial institutions that are looking to issue securities on a DLT infrastructure such as ethereum. - **MUST** be [ERC-20](./eip-20.md) compatible. - **MUST** be used in combination with an Identification system onchain ([ONCHAINID](https://github.com/onchain-id/solidity)) - **MUST** be able to apply any rule of compliance that is required by the regulator or by the token issuer (about the factors of eligibility of an identity or about the rules of the token itself) - **MUST** have a standard interface to pre-check if a transfer is going to pass or fail before sending it to the blockchain - **MUST** have a recovery system in case an investor loses access to his private key - **MUST** be able to freeze tokens on the wallet of investors if needed, partially or totally - **MUST** have the possibility to pause the token - **MUST** be able to mint and burn tokens - **MUST** define an Agent role and an Owner (token issuer) role - **MUST** be able to force transfers from an Agent wallet - **MUST** be able to issue transactions in batch (to save gas and to have all the transactions performed in the same block) - **MUST** be upgradeable (code of the smart contract should be upgradeable without changing the token smart contract address) ## Rationale ### Transfer Restrictions Transfers of securities can fail for a variety of reasons. This is in direct contrast to utility tokens, of which generally only require the sender to have a sufficient balance. These conditions can be related to the status of an investor’s wallet, the identity of the sender and receiver of the securities (i.e. whether they have been through a KYC process, whether they are accredited or an affiliate of the issuer) or for reasons unrelated to the specific transfer but instead set at the token level (i.e. the token contract enforces a maximum number of investors or a cap on the percentage held by any single investor). For [ERC-20](./eip-20.md) tokens, the `balanceOf` and `allowance` functions provide a way to check that a transfer is likely to succeed before executing the transfer, which can be executed both on-chain and off-chain. For tokens representing securities, the T-REX standard introduces a function `canTransfer` which provides a more general purpose way to achieve this. I.e. when the reasons for failure are related to the compliance rules of the token and a function `isVerified` which allows to check the eligibility status of the identity of the investor. ### Upgradeability The token contract should be upgradeable without changing its address on the blockchain, therefore, we decided to make it `proxiable` through [ERC-1822](./eip-1822.md) (Universal Upgradeable Proxy Standard) ### Identity Management Security and compliance of transfers is issued through the management of onchain identities. - ONCHAINID - Claim - Identity Storage/registry Transfers of securities can fail for a variety of reasons in contrast to utility tokens which generally only require the sender to have a sufficient balance. ## Specification This standard is backwards compatible with [ERC-20](./eip-20.md), therefore, all ERC-20 functions can be called on an ERC-3643 token, the interfaces being compatible. But the functions are not implemented in the same way as a classic ERC-20 as ERC-3643 is a permissioned token, which implies a check to be performed on each single token transfer to validate the compliance of the transfer and the eligibility of the stakeholder’s identities. ### Main functions #### Transfer To be able to perform a transfer on T-REX you need to fulfill several conditions : - The sender needs to hold enough free balance (total balance - frozen tokens, if any) - The receiver needs to be whitelisted on the Identity Registry and verified (hold the necessary claims on his [ONCHAINID](https://github.com/onchain-id/solidity)) - The sender's wallet cannot be frozen - The receiver's wallet cannot be frozen - The transfer has to respect all the rules of compliance defined in the `Compliance` smart contract (`canTransfer` needs to return `TRUE`) Here is an example of `transfer` function implementation : ```solidity function transfer(address _to, uint256 _amount) public override whenNotPaused returns (bool) { require(!frozen[_to] && !frozen[msg.sender], 'wallet is frozen'); require(_amount <= balanceOf(msg.sender).sub(frozenTokens[msg.sender]), 'Insufficient Balance'); if (tokenIdentityRegistry.isVerified(_to) && tokenCompliance.canTransfer(msg.sender, _to, _amount)) { tokenCompliance.transferred(msg.sender, _to, _amount); _transfer(msg.sender, _to, _amount); return true; } revert('Transfer not possible'); } ``` The `transferFrom` function works the same way while the `mint` function and the `forcedTransfer` function only require the receiver to be whitelisted and verified on the Identity Registry (they bypass the compliance rules). The `burn` function bypasses all checks on eligibility. #### isVerified The `isVerified` function is called from within the transfer functions `transfer`, `transferFrom`, `mint` and `forcedTransfer` to instruct the `Identity Registry` to check if the receiver is a valid investor, i.e. if his wallet address is in the `Identity Registry` of the token, and if the `ONCHAINID`contract linked to his wallet contains the claims (see ERC-735) required in the `Claim Topics Registry` and if these claims are signed by an authorized Claim Issuer as required in the `Trusted Issuers Registry`. If all the requirements are fulfilled, the `isVerified` function returns `TRUE`, otherwise it returns `FALSE`. An implementation of this function can be found on the [T-REX repository](https://github.com/TokenySolutions/T-REX). #### canTransfer The `canTransfer` function is also called from within transfer functions. This function checks if the transfer is compliant with global compliance rules applied to the token, in opposition with `isVerified` that only checks the eligibility of an investor to hold and receive tokens, the `canTransfer` function is looking at global compliance rules, e.g. check if the transfer is compliant in the case there is a fixed maximum number of token holders to respect (can be a limited number of holders per country as well), check if the transfer respects rules setting a maximum amount of tokens per investor, ... If all the requirements are fulfilled, the `canTransfer` function will return `TRUE` otherwise it will return `FALSE` and the transfer will not be allowed to happen. An implementation of this function can be found on the [T-REX repository](https://github.com/TokenySolutions/T-REX). #### Other functions Description of other functions of the ERC-3643 can be found in the `interfaces` folder. An implementation of the ERC-3643 suite of smart contracts can be found on the [T-REX repository](https://github.com/TokenySolutions/T-REX). ### Token interface ERC-3643 permissioned tokens are based on a standard ERC-20 structure but with some functions being added in order to ensure compliance in the transactions of the security tokens. The functions `transfer` and `transferFrom` are implemented in a conditional way, allowing them to proceed with a transfer only IF the transaction is valid. The permissioned tokens are allowed to be transferred only to validated counterparties, in order to avoid tokens being held in wallets/ONCHAINIDs of ineligible/unauthorized investors. The ERC-3643 standard also supports the recovery of security tokens in case an investor loses his/her wallet private key. A history of recovered tokens is maintained on the blockchain for transparency reasons. ERC-3643 tokens are implementing a lot of additional functions to give the owner or his agent the possibility to manage supply, transfer rules, lockups and everything that could be required in the management of a security. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces). ```solidity interface IERC3643 is IERC20 { // events event UpdatedTokenInformation(string _newName, string _newSymbol, uint8 _newDecimals, string _newVersion, address _newOnchainID); event IdentityRegistryAdded(address indexed _identityRegistry); event ComplianceAdded(address indexed _compliance); event RecoverySuccess(address _lostWallet, address _newWallet, address _investorOnchainID); event AddressFrozen(address indexed _userAddress, bool indexed _isFrozen, address indexed _owner); event TokensFrozen(address indexed _userAddress, uint256 _amount); event TokensUnfrozen(address indexed _userAddress, uint256 _amount); event Paused(address _userAddress); event Unpaused(address _userAddress); // functions // getters function decimals() external view returns (uint8); function name() external view returns (string memory); function onchainID() external view returns (address); function symbol() external view returns (string memory); function version() external view returns (string memory); function identityRegistry() external view returns (IIdentityRegistry); function compliance() external view returns (ICompliance); function paused() external view returns (bool); function isFrozen(address _userAddress) external view returns (bool); function getFrozenTokens(address _userAddress) external view returns (uint256); // setters function setName(string calldata _name) external; function setSymbol(string calldata _symbol) external; function setOnchainID(address _onchainID) external; function pause() external; function unpause() external; function setAddressFrozen(address _userAddress, bool _freeze) external; function freezePartialTokens(address _userAddress, uint256 _amount) external; function unfreezePartialTokens(address _userAddress, uint256 _amount) external; function setIdentityRegistry(address _identityRegistry) external; function setCompliance(address _compliance) external; // transfer actions function forcedTransfer(address _from, address _to, uint256 _amount) external returns (bool); function mint(address _to, uint256 _amount) external; function burn(address _userAddress, uint256 _amount) external; function recoveryAddress(address _lostWallet, address _newWallet, address _investorOnchainID) external returns (bool); // batch functions function batchTransfer(address[] calldata _toList, uint256[] calldata _amounts) external; function batchForcedTransfer(address[] calldata _fromList, address[] calldata _toList, uint256[] calldata _amounts) external; function batchMint(address[] calldata _toList, uint256[] calldata _amounts) external; function batchBurn(address[] calldata _userAddresses, uint256[] calldata _amounts) external; function batchSetAddressFrozen(address[] calldata _userAddresses, bool[] calldata _freeze) external; function batchFreezePartialTokens(address[] calldata _userAddresses, uint256[] calldata _amounts) external; function batchUnfreezePartialTokens(address[] calldata _userAddresses, uint256[] calldata _amounts) external; // roles setting function transferOwnershipOnTokenContract(address _newOwner) external; function addAgentOnTokenContract(address _agent) external; function removeAgentOnTokenContract(address _agent) external; } ``` ### Identity Registry Interface This Identity Registry is linked to storage that contains a dynamic whitelist of identities. The Identity Registry makes the link between a wallet address, an [ONCHAINID](https://tokeny.com/onchainid/) and a country code corresponding to the country of residence of the investor, this country code is set in accordance with the [ISO-3166 standard](https://www.iso.org/iso-3166-country-codes.html). It also contains a function called `isVerified()`, which returns a status based on the validity of claims (as per the security token requirements) in the user’s ONCHAINID. The Identity Registry is managed by the agent wallet(s) i.e. only the agent(s) can add or remove identities in the registry (note: the agent role on the Identity Registry is set by the owner, therefore the owner could set himself as the agent if he wants to keep everything under his own control). There is a specific identity registry for each security token. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces). Note that [`IClaimIssuer`](https://github.com/onchain-id/solidity/blob/master/contracts/interface/IClaimIssuer.sol) and [`IIdentity`](https://github.com/onchain-id/solidity/blob/master/contracts/interface/IIdentity.sol) are needed in this interface and are coming from [ONCHAINID](https://github.com/onchain-id/solidity) ```solidity interface IIdentityRegistry { // events event ClaimTopicsRegistrySet(address indexed claimTopicsRegistry); event IdentityStorageSet(address indexed identityStorage); event TrustedIssuersRegistrySet(address indexed trustedIssuersRegistry); event IdentityRegistered(address indexed investorAddress, IIdentity indexed identity); event IdentityRemoved(address indexed investorAddress, IIdentity indexed identity); event IdentityUpdated(IIdentity indexed oldIdentity, IIdentity indexed newIdentity); event CountryUpdated(address indexed investorAddress, uint16 indexed country); // functions // identity registry getters function identityStorage() external view returns (IIdentityRegistryStorage); function issuersRegistry() external view returns (ITrustedIssuersRegistry); function topicsRegistry() external view returns (IClaimTopicsRegistry); //identity registry setters function setIdentityRegistryStorage(address _identityRegistryStorage) external; function setClaimTopicsRegistry(address _claimTopicsRegistry) external; function setTrustedIssuersRegistry(address _trustedIssuersRegistry) external; // registry actions function registerIdentity(address _userAddress, IIdentity _identity, uint16 _country) external; function deleteIdentity(address _userAddress) external; function updateCountry(address _userAddress, uint16 _country) external; function updateIdentity(address _userAddress, IIdentity _identity) external; function batchRegisterIdentity(address[] calldata _userAddresses, IIdentity[] calldata _identities, uint16[] calldata _countries) external; // registry consultation function contains(address _userAddress) external view returns (bool); function isVerified(address _userAddress) external view returns (bool); function identity(address _userAddress) external view returns (IIdentity); function investorCountry(address _userAddress) external view returns (uint16); // roles setters function transferOwnershipOnIdentityRegistryContract(address _newOwner) external; function addAgentOnIdentityRegistryContract(address _agent) external; function removeAgentOnIdentityRegistryContract(address _agent) external; } ``` ### Identity Registry Storage Interface The Identity Registry Storage stores the identity addresses of all the authorized investors in the security token(s) linked to the storage contract i.e. all identities of investors who have been authorized to hold the token(s) after having gone through the appropriate KYC and eligibility checks. The Identity Registry Storage can be bound to one or several Identity Registry contract(s). The goal of the Identity Registry storage is to separate the Identity Registry functions and specifications from its storage, this way it is possible to keep one single Identity Registry contract per token, with its own Trusted Issuers Registry and Claim Topics Registry but with a shared whitelist of investors used by the `isVerifed()` function implemented in the Identity Registries to check the eligibility of the receiver in a transfer transaction. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces). ```solidity interface IIdentityRegistryStorage { //events event IdentityStored(address indexed investorAddress, IIdentity indexed identity); event IdentityUnstored(address indexed investorAddress, IIdentity indexed identity); event IdentityModified(IIdentity indexed oldIdentity, IIdentity indexed newIdentity); event CountryModified(address indexed investorAddress, uint16 indexed country); event IdentityRegistryBound(address indexed identityRegistry); event IdentityRegistryUnbound(address indexed identityRegistry); //functions // storage related functions function storedIdentity(address _userAddress) external view returns (IIdentity); function storedInvestorCountry(address _userAddress) external view returns (uint16); function addIdentityToStorage(address _userAddress, IIdentity _identity, uint16 _country) external; function removeIdentityFromStorage(address _userAddress) external; function modifyStoredInvestorCountry(address _userAddress, uint16 _country) external; function modifyStoredIdentity(address _userAddress, IIdentity _identity) external; // role setter function transferOwnershipOnIdentityRegistryStorage(address _newOwner) external; function bindIdentityRegistry(address _identityRegistry) external; function unbindIdentityRegistry(address _identityRegistry) external; // getter for bound IdentityRegistry role function linkedIdentityRegistries() external view returns (address[] memory); } ``` ### Compliance Interface The Compliance is used to set the rules of the offering itself and ensures these rules are respected during the whole lifecycle of the token, e.g. the compliance contract will define the maximum amount of investors per country, the maximum amount of tokens per investor, the accepted countries for the circulation of the token (using the country code corresponding to each investor in the Identity Registry). The compliance smart contract is a “tailor-made” contract that is implemented in accordance with the legal requirements and following the desires of the token issuer. This contract is triggered at every transaction by the Token and returns `TRUE` if the transaction is compliant with the rules of the offering and `FALSE` otherwise. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces). ```solidity interface ICompliance { // events event TokenAgentAdded(address _agentAddress); event TokenAgentRemoved(address _agentAddress); event TokenBound(address _token); event TokenUnbound(address _token); // functions // initialization of the compliance contract function addTokenAgent(address _agentAddress) external; function removeTokenAgent(address _agentAddress) external; function bindToken(address _token) external; function unbindToken(address _token) external; // check the parameters of the compliance contract function isTokenAgent(address _agentAddress) external view returns (bool); function isTokenBound(address _token) external view returns (bool); // compliance check and state update function canTransfer(address _from, address _to, uint256 _amount) external view returns (bool); function transferred(address _from, address _to, uint256 _amount) external; function created(address _to, uint256 _amount) external; function destroyed(address _from, uint256 _amount) external; // setting owner role function transferOwnershipOnComplianceContract(address newOwner) external; } ``` ### Trusted Issuer's Registry Interface The Trusted Issuer's Registry stores the contract addresses ([ONCHAINID](https://tokeny.com/onchainid/)) of all the trusted claim issuers for a specific security token. The [ONCHAINID](https://tokeny.com/onchainid/) of token owners (the investors) must have claims signed by the claim issuers stored in this smart contract in order to be able to hold the token. The ownership of this contract is given to the token issuer allowing them to manage this registry as per their requirements. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces) ```solidity interface ITrustedIssuersRegistry { // events event TrustedIssuerAdded(IClaimIssuer indexed trustedIssuer, uint[] claimTopics); event TrustedIssuerRemoved(IClaimIssuer indexed trustedIssuer); event ClaimTopicsUpdated(IClaimIssuer indexed trustedIssuer, uint[] claimTopics); // functions // setters function addTrustedIssuer(IClaimIssuer _trustedIssuer, uint[] calldata _claimTopics) external; function removeTrustedIssuer(IClaimIssuer _trustedIssuer) external; function updateIssuerClaimTopics(IClaimIssuer _trustedIssuer, uint[] calldata _claimTopics) external; // getters function getTrustedIssuers() external view returns (IClaimIssuer[] memory); function isTrustedIssuer(address _issuer) external view returns(bool); function getTrustedIssuerClaimTopics(IClaimIssuer _trustedIssuer) external view returns(uint[] memory); function hasClaimTopic(address _issuer, uint _claimTopic) external view returns(bool); // role setter function transferOwnershipOnIssuersRegistryContract(address _newOwner) external; } ``` ### Claim Topics Registry Interface The Claim Topics Registry stores all the trusted claim topics for the security token. The [ONCHAINID](https://tokeny.com/onchainid/) of token owners must contain claims of the claim topics stored in this smart contract. The ownership of this contract is given to the token issuer allowing them to manage this registry as per their requirements. A detailed description of the functions can be found in the [interfaces folder](https://github.com/TokenySolutions/EIP3643/tree/main/interfaces) ```solidity interface IClaimTopicsRegistry { // events event ClaimTopicAdded(uint256 indexed claimTopic); event ClaimTopicRemoved(uint256 indexed claimTopic); // functions // setters function addClaimTopic(uint256 _claimTopic) external; function removeClaimTopic(uint256 _claimTopic) external; // getter function getClaimTopics() external view returns (uint256[] memory); // role setter function transferOwnershipOnClaimTopicsRegistryContract(address _newOwner) external; } ``` ## Test Cases The standard is implemented and tested with full coverage on Tokeny's [T-REX repository](https://github.com/TokenySolutions/T-REX) ## Security Considerations The suite of Smart Contracts has been audited by an external and independent company. The results can be found in [this document](https://tokeny.com/wp-content/uploads/2020/05/Tokeny-Solutions_T-REX-v3_Smart-Contract-Audit-Report_Kapersky.pdf). ## Copyright Copyright and related rights waived via [CC0](../LICENSE.md).