The T-REX token is an institutional grade security token standard. This standard provides a library of interfaces for the management and compliant transfer of security tokens, using an automated onchain validator system leveraging onchain identities for eligibility checks.
The standard defines several interfaces that are described hereunder:
The advent of blockchain technology has brought about a new era of efficiency, accessibility, and liquidity in the world of asset transfer. This is particularly evident in the realm of cryptocurrencies, where users can transfer token ownership peer-to-peer without intermediaries. However, when it comes to tokenized securities or security tokens, the situation is more complex due to the need for compliance with securities laws. These tokens cannot be permissionless like utility tokens; they must be permissioned to track ownership and ensure that only eligible investors can hold tokens.
The existing Ethereum protocol, while powerful and versatile, does not fully address the unique challenges posed by security tokens. There is a need for a standard that supports compliant issuance and management of permissioned tokens, suitable for representing a wide range of asset classes, including small businesses and real estate.
The proposed ERC-3643 standard is motivated by this need. It aims to provide a comprehensive framework for managing the lifecycle of security tokens, from issuance to transfers between eligible investors, while enforcing compliance rules at every stage. The standard also supports additional features such as token pausing and freezing, which can be used to manage the token in response to regulatory requirements or changes in the status of the token or its holders.
Moreover, the standard is designed to work in conjunction with an on-chain Identity system, allowing for the validation of the identities and credentials of investors through signed attestations issued by trusted claim issuers. This ensures compliance with legal and regulatory requirements for the trading of security tokens.
In summary, the motivation behind the proposed standard is to bring the benefits of blockchain technology to the world of securities, while ensuring compliance with existing securities laws. It aims to provide a robust, flexible, and efficient framework for the issuance and management of security tokens, thereby accelerating the evolution of capital markets.
The proposed standard has the following requirements:
While this standard is backwards compatible with ERC-20 and all ERC-20 functions can be called on an ERC-3643 token, the implementation of these functions differs due to the permissioned nature of ERC-3643. Each token transfer under this standard involves a compliance check to validate the transfer and the eligibility of the stakeholder’s identities.
The standard defines an Agent role, which is crucial for managing access to various functions of the smart contracts. The interface for the Agent role is as follows:
interface IAgentRole {
// events
event AgentAdded(address indexed _agent);
event AgentRemoved(address indexed _agent);
// functions
// setters
function addAgent(address _agent) external;
function removeAgent(address _agent) external;
// getters
function isAgent(address _agent) external view returns (bool);
}
```
The `IAgentRole` interface allows for the addition and removal of agents, as well as checking if an address is an agent. In this standard, it is the owner role, as defined by [ERC-173](/eips/eip-173.html), that has the responsibility of appointing and removing agents. Any contract that fulfills the role of a Token contract or an Identity Registry within the context of this standard must be compatible with the `IAgentRole` interface.
### Main functions
#### Transfer
To be able to perform a transfer on T-REX you need to fulfill several conditions :
- The sender **MUST** hold enough free balance (total balance - frozen tokens, if any)
- The receiver **MUST** be whitelisted on the Identity Registry and verified (hold the necessary claims on his onchain Identity)
- The sender's wallet **MUST NOT** be frozen
- The receiver's wallet **MUST NOT** be frozen
- The token **MUST NOT** be paused
- The transfer **MUST** 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], "ERC-3643: Frozen wallet");
require(_amount <= balanceOf(msg.sender) - (_frozenTokens[msg.sender]), "ERC-3643: Insufficient Balance");
require( _tokenIdentityRegistry.isVerified(to), "ERC-3643: Invalid identity" );
require( _tokenCompliance.canTransfer(from, to, amount), "ERC-3643: Compliance failure" );
_transfer(msg.sender, _to, _amount);
_tokenCompliance.transferred(msg.sender, _to, _amount);
return true;
}
```
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 `Identity`contract linked to his wallet
contains the claims (see [Claim Holder](../static/assets/eip-3643/ONCHAINID/IERC735.sol)) 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 of Tokeny.
#### 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 of Tokeny.
#### 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 of Tokeny.
### Token interface
ERC-3643 permissioned tokens build upon the standard ERC-20 structure, but with additional functions 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/Identity contracts of ineligible/unauthorized investors. The ERC-3643 standard also supports the recovery of security tokens in case an investor loses access to their wallet private key. A history of recovered tokens is maintained on the blockchain for transparency reasons.
ERC-3643 tokens implement a range of additional functions to enable the owner or their appointed agents to manage supply, transfer rules, lockups, and any other requirements in the management of a security. The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents. Any contract that fulfills the role of a Token contract within the context of this standard must be compatible with the `IAgentRole` interface.
A detailed description of the functions can be found in the [interfaces folder](../static/assets/eip-3643/interfaces/IERC3643.sol).
```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 onchainID() external view returns (address);
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;
}
The Identity Registry is linked to storage that contains a dynamic whitelist of identities. It establishes the link between a wallet address, an Identity smart contract, and a country code corresponding to the investor's country of residence. This country code is set in accordance with the ISO-3166 standard. The Identity Registry also includes a function called isVerified()
, which returns a status based on the validity of claims (as per the security token requirements) in the user’s Identity contract.
The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents. Any contract that fulfills the role of an Identity Registry within the context of this standard must be compatible with the IAgentRole
interface. The Identity Registry is managed by the agent wallet(s), meaning only the agent(s) can add or remove identities in the registry. Note that the agent role on the Identity Registry is set by the owner, therefore the owner could set themselves as the agent if they want to maintain full control. There is a specific identity registry for each security token.
A detailed description of the functions can be found in the interfaces folder.
Note that IClaimIssuer
and IIdentity
are needed in this interface as they are required for the Identity eligibility checks.
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);
}
The Identity Registry Storage stores the identity addresses of all the authorized investors in the security token(s) linked to the storage contract. These are 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.
The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of appointing agents(in this case through the bindIdentityRegistry
function). Any contract that fulfills the role of an Identity Registry Storage within the context of this standard must be compatible with the IAgentRole
interface. The Identity Registry Storage is managed by the agent addresses (i.e. the bound Identity Registries), meaning only the agent(s) can add or remove identities in the registry. Note that the agent role on the Identity Registry Storage is set by the owner, therefore the owner could set themselves as the agent if they want to modify the storage manually. Otherwise it is the bound Identity Registries that are using the agent role to write in the Identity Registry Storage.
A detailed description of the functions can be found in the interfaces folder.
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 bindIdentityRegistry(address _identityRegistry) external;
function unbindIdentityRegistry(address _identityRegistry) external;
// getter for bound IdentityRegistry role
function linkedIdentityRegistries() external view returns (address[] memory);
}
The Compliance contract is used to set the rules of the offering itself and ensures these rules are respected during the whole lifecycle of the token. For example, the Compliance contract will define the maximum amount of investors per country, the maximum amount of tokens per investor, and 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 can be either “tailor-made”, following the legal requirements of the token issuer, or can be deployed under a generic modular form, which can then add and remove external compliance Modules
to fit the legal requirements of the token in the same way as a custom "tailor-made" contract would.
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.
The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of setting the Compliance parameters and binding the Compliance to a Token contract.
A detailed description of the functions can be found in the interfaces folder.
interface ICompliance {
// events
event TokenBound(address _token);
event TokenUnbound(address _token);
// functions
// initialization of the compliance contract
function bindToken(address _token) external;
function unbindToken(address _token) external;
// check the parameters of the compliance contract
function isTokenBound(address _token) external view returns (bool);
function getTokenBound() external view returns (address);
// 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;
}
The Trusted Issuer's Registry stores the contract addresses (IClaimIssuer) of all the trusted claim issuers for a specific security token. The Identity contract (IIdentity) 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 standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of managing this registry as per their requirements. This includes the ability to add, remove, and update the list of Trusted Issuers.
A detailed description of the functions can be found in the interfaces folder.
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 getTrustedIssuersForClaimTopic(uint256 claimTopic) external view returns (IClaimIssuer[] memory);
function hasClaimTopic(address _issuer, uint _claimTopic) external view returns(bool);
}
The Claim Topics Registry stores all the trusted claim topics for the security token. The Identity contract (IIdentity) of token owners must contain claims of the claim topics stored in this smart contract.
The standard relies on ERC-173 to define contract ownership, with the owner having the responsibility of managing this registry as per their requirements. This includes the ability to add and remove required Claim Topics.
A detailed description of the functions can be found in the interfaces folder.
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);
}
Transfers of securities can fail for a variety of reasons. This is in direct contrast to utility tokens, 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 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 checking the eligibility status of the identity of the investor. Transfers can also fail if the address of the sender and/or receiver is frozen, or if the free balance of the sender (total balance - frozen tokens) is lower than the amount to transfer. Ultimately, the transfer could be blocked if the token is paused
.
Security and compliance of transfers are enforced through the management of on-chain identities. These include:
The T-REX standard provides a comprehensive framework for managing the lifecycle of security tokens. This includes the issuance of tokens, transfers between eligible investors, and the enforcement of compliance rules at every stage of the token's lifecycle. The standard also supports additional features such as token pausing and freezing, which can be used to manage the token in response to regulatory requirements or changes in the status of the token or its holders.
The T-REX standard supports the implementation of additional compliance rules through modular compliance. These modules can be used to enforce a wide range of rules and restrictions, such as caps on the number of investors or the percentage of tokens held by a single investor, restrictions on transfers between certain types of investors, and more. This flexibility allows issuers to tailor the compliance rules of their tokens to their specific needs and regulatory environment.
The inclusion of Agent-scoped functions within the standard interfaces is deliberate. The intent is to accommodate secure and adaptable token management practices that surpass the capabilities of EOA management. We envision scenarios where the agent role is fulfilled by automated systems or smart contracts, capable of programmatically executing operational functions like minting, burning, and freezing in response to specified criteria or regulatory triggers. For example, a smart contract might automatically burn tokens to align with redemption requests in an open-ended fund, or freeze tokens associated with wallets engaged in fraudulent activities.
Consequently, these functions are standardized to provide a uniform interface for various automated systems interacting with different ERC-3643 tokens, allowing for standardized tooling and interfaces that work across the entire ecosystem. This approach ensures that ERC-3643 remains flexible, future-proof, and capable of supporting a wide array of operational models.
T-REX tokens should be backwards compatible with ERC-20 and ERC-173 and should be able to interact with a Claim Holder contract to validate the claims linked to an Identity contract.
This specification has been audited by Kapersky and Hacken, and no notable security considerations were found. While the audits were primarily focused on the specific implementation by Tokeny, they also challenged and validated the core principles of the T-REX standard. The auditing teams approval of these principles provides assurance that the standard itself is robust and does not present any significant security concerns.
Copyright and related rights waived via CC0.