Exploring ERC-1400: The Standard for Security Tokens

Mohamad Hammoud
15 min readMay 28, 2024

--

The Need for ERC-1400

Before ERC-1400, there was no uniform approach for security tokens, making it hard for platforms to support them without custom solutions.

Security tokens need to meet strict legal requirements. Existing standards like ERC-20 lacked mechanisms for compliance, such as trading restrictions and identity checks.

Financial securities vary widely. A single standard that could accommodate various types of securities within one framework was needed to manage these differences effectively.

The absence of a standardized protocol led to inefficiencies in issuing and managing security tokens. ERC-1400 addresses these issues by providing a clear, unified framework for these tokens.

ERC-1400 Definition

The ERC-1400 is a sophisticated standard for security tokens on the Ethereum blockchain.

It extends the basic functionalities of ERC20 tokens by introducing concepts such as

  1. Partitions
  2. Document management
  3. Controller operations
  4. Compliance with regulations.

This standard is especially designed to meet the regulatory requirements often associated with securities trading and ownership.

Partitions

Partitions are essentially sub-categories or classes of tokens within a single token contract. Each partition can represent a specific tranche of tokens that may have unique characteristics or rights attached to them.

For example, a company might issue tokens where some tokens represent equity, others represent voting rights, and yet others might offer dividends.

Partitions allow these different classes of tokens to be managed under the same smart contract but accounted for separately.

Example Usage of Partitions:

  • A company issues 1,000 tokens in total.
  • 300 tokens are in the “EquityClass” partition (giving holders equity).
  • 200 tokens are in the “VotingClass” partition (giving holders voting rights).
  • 500 tokens are in the “DividendClass” partition (giving holders rights to dividends).

Document Management

It manages token-related documents, which could be compliance documentation, terms and conditions of the token sale, or other legal documents that need to be associated with the token.

Controller Operations

Controller Operations manage operators who are authorized to operate on the entire supply of tokens on behalf of others. This is useful for automated financial management systems or exchanges which need to operate tokens for trading or management purposes without taking custody.

Compliance and Regulatory Hooks

These hooks are used for compliance checks before transfers are executed. It allows the integration of regulatory compliance checks into the token logic.

Token Identification

_name:

  • Purpose: Stores the official name of the token, which can be displayed in user interfaces and wallets.
  • Example: “Soli Chain Security Token”.

_symbol:

  • Purpose: Holds the token’s symbol, a shorter way to represent the token name.
  • Example: “SCST”.

Token Characteristics

_granularity:

  • Purpose: Determines the smallest unit into which a token can be divided, reflecting the token’s divisibility.
  • Example: If set to 1, the token cannot be divided into smaller parts than one whole token.

_totalSupply:

  • Purpose: Represents the total number of tokens currently in circulation.
  • Example: 100,000 tokens might represent 100,000 shares of a company.

_migrated:

  • Purpose: Indicates whether the token has been migrated to a new contract, usually set during upgrades to disable the old contract.
  • Example: true if the token functionalities have been moved to a new contract address.

Control Features

_isControllable:

  • Purpose: A flag to check if the token can still be controlled by designated operators or if it has become fully decentralized.
  • Example: true allows for administrative control, such as forced transfers or burns.

_isIssuable:

  • Purpose: Determines whether more tokens can be issued in the future or if the supply is now fixed.
  • Example: false after the final issuance to cap the total supply permanently.

Balance and Allowances

_balances:

  • Purpose: Maps each holder’s address to the quantity of tokens they own, facilitating balance checks and transfers.
  • Example: Tracks that a user owns 500 tokens.

_allowed:

  • Purpose: Maps each token holder to another address with the amount of tokens they are allowed to use on the holder’s behalf, supporting the approve and transferFrom functionalities.
  • Example: User A allows User B to transfer up to 100 of their tokens.

Document Management

_documents:

  • Purpose: Associates documents with the token, identified by a name hash, to store compliance documents or other relevant information.
  • Example: Linking a “Prospectus” document showing investment details.

Partitions Management

_totalSupplyByPartition:

  • Purpose: Maps each partition to its specific supply of tokens, allowing for partition-specific tracking.
  • Example: 30,000 tokens in the “Voting Rights” partition.

_balanceOfByPartition:

  • Purpose: Maps each token holder’s address and partition to the number of tokens they own within that specific partition.
  • Example: User C owns 200 tokens in the “Dividend” partition.

Operator Permissions

_authorizedOperator:

  • Purpose: Maps operators who are allowed to manage all tokens on behalf of other users.
  • Example: A wallet service has control rights over its users’ tokens for easier management.

_authorizedOperatorByPartition:

  • Purpose: Similar to _authorizedOperator, but specific to partitions, allowing more granular control.
  • Example: A financial advisor has rights only to manage tokens in the “Investment” partition.

Compliance and Control

_controllers and _controllersByPartition:

  • Purpose: Lists addresses that can perform high-level control actions such as minting new tokens or burning existing ones, both globally and for specific partitions.
  • Example: A regulatory authority might be able to control token movements in critical scenarios.

Technical Breakdown of ERC-1400

Interface and Extension Constants

In ERC-1400, several constants are defined to identify and register the various interfaces and extensions. These constants play a crucial role in ensuring that the contract can interact with other contracts and applications seamlessly. Let’s delve into each of these constants:

Token Interface Names

string constant internal ERC1400_INTERFACE_NAME = "ERC1400Token";
string constant internal ERC20_INTERFACE_NAME = "ERC20Token";
  • ERC1400_INTERFACE_NAME: This represents the interface name for ERC-1400 tokens. By registering this name in the ERC1820 registry, the contract ensures that it can be recognized as an ERC-1400 compliant token. This allows other contracts and applications to interact with it using the ERC-1400 standard.
  • ERC20_INTERFACE_NAME: This represents the interface name for ERC-20 tokens. Since ERC-1400 is designed to be backward compatible with ERC-20, registering this interface ensures that the token can be used wherever ERC-20 tokens are accepted.

Token Extensions

string constant internal ERC1400_TOKENS_CHECKER = "ERC1400TokensChecker";
string constant internal ERC1400_TOKENS_VALIDATOR = "ERC1400TokensValidator";
  • ERC1400_TOKENS_CHECKER: This constant represents the interface name for the ERC1400TokensChecker extension. This extension is responsible for performing compliance checks before any token transfer. It validates that the transfer adheres to the rules and regulations set by the token issuer.
  • ERC1400_TOKENS_VALIDATOR: This constant represents the interface name for the ERC1400TokensValidator extension. This extension is called during token transfers to perform additional validations, ensuring that all transfers comply with regulatory requirements and the issuer’s rules.

User Extensions

string constant internal ERC1400_TOKENS_SENDER = "ERC1400TokensSender";
string constant internal ERC1400_TOKENS_RECIPIENT = "ERC1400TokensRecipient";
  • ERC1400_TOKENS_SENDER: This constant represents the interface name for the ERC1400TokensSender extension. This extension is invoked before a token transfer is executed, allowing the sender to implement custom logic or checks specific to their needs.
  • ERC1400_TOKENS_RECIPIENT: This constant represents the interface name for the ERC1400TokensRecipient extension. This extension is invoked after a token transfer is executed, allowing the recipient to implement custom logic or checks specific to their requirements.

Purpose of Interface Names and Extensions

These constants serve as identifiers for the various components and extensions within the ERC-1400 standard. By registering these interfaces in the ERC1820 registry, the contract can be dynamically discovered and interacted with by other contracts and applications on the Ethereum network. This modular approach provides flexibility and extensibility, enabling token issuers to enforce specific rules and compliance checks as needed.

Example Usage

During the deployment and initialization of an ERC-1400 contract, these interface names and extensions are used to set up and register the contract with the ERC1820 registry. Here’s an example:

constructor(
string memory tokenName,
string memory tokenSymbol,
uint256 tokenGranularity,
address[] memory initialControllers,
bytes32[] memory defaultPartitions
) {
_name = tokenName;
_symbol = tokenSymbol;
_totalSupply = 0;

require(tokenGranularity >= 1);

_granularity = tokenGranularity;

_setControllers(initialControllers);

_defaultPartitions = defaultPartitions;
_isControllable = true;
_isIssuable = true;

// Register contract in ERC1820 registry
ERC1820Client.setInterfaceImplementation(ERC1400_INTERFACE_NAME, address(this));
ERC1820Client.setInterfaceImplementation(ERC20_INTERFACE_NAME, address(this));

// Indicate token verifies ERC1400 and ERC20 interfaces
ERC1820Implementer._setInterface(ERC1400_INTERFACE_NAME);
ERC1820Implementer._setInterface(ERC20_INTERFACE_NAME);
}

In this constructor, the token is initialized with its name, symbol, granularity, initial controllers, and default partitions.

The contract is then registered with the ERC1820 registry using the predefined interface names, ensuring it is recognized as both an ERC-1400 and ERC-20 token.

Partitions Mappings

List of Partitions

bytes32[] internal _totalPartitions;
  • Description: This array keeps a list of all the partitions that exist for the token. Each partition can represent a different class or tranche of tokens.
  • Purpose: It allows the contract to keep track of all the different partitions that have been created, making it possible to manage and iterate over them when necessary.
  • Example Usage: If a token issuer wants to create multiple tranches of security tokens, such as equity, voting rights, and dividends, each of these tranches would be represented as a separate partition and added to this array.

Mapping from Partition to Their Index

mapping (bytes32 => uint256) internal _indexOfTotalPartitions;
  • Description: This mapping associates each partition with its index in the _totalPartitions array.
  • Purpose: It provides a quick lookup to find the position of a partition within the _totalPartitions array. This is useful for efficient management and operations on partitions.
  • Example Usage: When a new partition is added, its index in _totalPartitions is stored in this mapping. This allows for efficient removal or querying of partitions by their identifier.

Mapping from Partition to Global Balance of Corresponding Partition

mapping (bytes32 => uint256) internal _totalSupplyByPartition;
  • Description: This mapping keeps track of the total supply of tokens for each partition.
  • Purpose: It allows the contract to manage the total amount of tokens that exist within each partition, facilitating operations like transfers and balance checks.
  • Example Usage: If a partition represents a specific class of equity, this mapping would track the total number of tokens issued under that class.

Mapping from TokenHolder to Their Partitions

mapping (address => bytes32[]) internal _partitionsOf;
  • Description: This mapping links each token holder to an array of partitions they own tokens in.
  • Purpose: It enables the contract to keep track of which partitions a specific token holder has tokens in, allowing for detailed balance queries and operations specific to a token holder’s partitions.
  • Example Usage: If a token holder owns tokens in both the “EquityClass” and “DividendClass” partitions, this mapping would store those partitions for that token holder.

Mapping from (TokenHolder, Partition) to Their Index

mapping (address => mapping (bytes32 => uint256)) internal _indexOfPartitionsOf;
  • Description: This mapping associates a specific token holder and partition with the index of that partition in the token holder’s _partitionsOf array.
  • Purpose: It provides an efficient way to find the position of a partition in a specific token holder’s list of partitions.
  • Example Usage: When querying or updating the partitions a token holder owns, this mapping allows for efficient lookups and modifications.

Mapping from (TokenHolder, Partition) to Balance of Corresponding Partition

mapping (address => mapping (bytes32 => uint256)) internal _balanceOfByPartition;
  • Description: This mapping keeps track of the balance of tokens for each token holder within a specific partition.
  • Purpose: It allows the contract to manage and query the amount of tokens a token holder owns in each partition.
  • Example Usage: If a token holder has 100 tokens in the “VotingClass” partition and 200 tokens in the “DividendClass” partition, this mapping would store those balances.

List of Token Default Partitions (for ERC20 Compatibility)

bytes32[] internal _defaultPartitions;
  • Description: This array holds the list of default partitions used for ERC-20 compatibility. When partition is not specified, the default partition(s) are used.
  • Purpose: It provides a mechanism to ensure backward compatibility with ERC-20 by specifying which partitions should be used for standard ERC-20 operations.
  • Example Usage: When a standard ERC-20 transfer is initiated without specifying a partition, the contract can default to using the partitions listed in this array.

Global Operators Mappings

The global operators mappings in the ERC-1400 contract define the relationships and permissions of various operators who have specific rights over the tokens. These mappings and arrays manage operator authorizations and controller roles, ensuring proper access control and management capabilities within the token contract.

Mapping from (Operator, TokenHolder) to Authorized Status

mapping(address => mapping(address => bool)) internal _authorizedOperator;
  • Description: This mapping tracks whether a specific operator is authorized to manage the tokens of a particular token holder.
  • Purpose: It allows individual token holders to delegate management rights over their tokens to other addresses. This can include transferring tokens on their behalf or performing other actions that require the holder’s authorization.
  • Example Usage: If Alice wants Bob to manage her tokens, she can authorize Bob as an operator. This mapping would then have an entry like _authorizedOperator[BobsAddress][AlicesAddress] = true.

Array of Controllers

address[] internal _controllers;
  • Description: This array holds the addresses of the global controllers. These controllers have broad management rights over the entire token supply and are not specific to individual token holders.
  • Purpose: Controllers can perform high-level operations such as minting new tokens, burning tokens, and enforcing compliance rules. These roles are usually assigned to trusted entities like regulatory bodies, financial institutions, or the token issuer itself.
  • Example Usage: If the token issuer and a regulatory authority are both controllers, their addresses would be included in this array. They can then use their special rights to manage the token supply and ensure compliance with relevant regulations.

Mapping from Operator to Controller Status

mapping(address => bool) internal _isController;
  • Description: This mapping keeps track of which addresses are recognized as global controllers.
  • Purpose: It provides a quick way to check if a specific address has global control rights over the token contract. This ensures that only designated controllers can perform certain high-level operations.
  • Example Usage: When a new controller is added, their address is set to true in this mapping. For example, _isController[IssuerAddress] = true indicates that the issuer is a global controller.

Purpose and Benefits

These mappings and arrays ensure a robust and flexible access control mechanism within the ERC-1400 contract:

  • Delegated Management: Token holders can delegate token management to trusted operators, making it easier to manage their holdings.
  • Centralized Control: Trusted entities like issuers and regulators can perform high-level operations, ensuring compliance with laws and regulations.
  • Security and Flexibility: By separating operator and controller roles, the contract ensures that different entities can have different levels of access and control, enhancing both security and operational flexibility.

Partition Operators Mappings

Partition operators mappings in the ERC-1400 contract manage permissions and authorizations related to specific partitions of tokens. These mappings facilitate granular control over token operations within different partitions, enabling more detailed and specific management of tokenized securities.

Mapping from (Partition, TokenHolder, Spender) to Allowed Value

mapping(bytes32 => mapping (address => mapping (address => uint256))) internal _allowedByPartition;
  • Description: This mapping tracks the amount of tokens that a spender is allowed to transfer on behalf of a token holder within a specific partition.
  • Purpose: It supports partition-specific allowances, enabling token holders to grant permissions to spenders for specific partitions, similar to how allowances work in ERC-20 but with partition granularity.
  • Example Usage: If Alice wants Bob to be able to spend 100 tokens on her behalf within the “DividendClass” partition, this mapping would have an entry like _allowedByPartition["DividendClass"][AlicesAddress][BobsAddress] = 100.

Mapping from (TokenHolder, Partition, Operator) to ‘Approved for Partition’ Status

mapping (address => mapping (bytes32 => mapping (address => bool))) internal _authorizedOperatorByPartition;
  • Description: This mapping indicates whether a specific operator is authorized to manage tokens of a token holder within a particular partition.
  • Purpose: It allows token holders to authorize operators for specific partitions, providing more granular control over token management.
  • Example Usage: If Alice authorizes Bob to manage her tokens within the “VotingClass” partition, this mapping would have an entry like _authorizedOperatorByPartition[AlicesAddress]["VotingClass"][BobsAddress] = true.

Mapping from Partition to Controllers for the Partition

mapping (bytes32 => address[]) internal _controllersByPartition;
  • Description: This mapping holds an array of addresses that are designated as controllers for each partition.
  • Purpose: It allows specific controllers to have management rights over certain partitions, enabling partition-specific control by trusted entities.
  • Example Usage: If the regulatory authority needs control over the “EquityClass” partition, their address would be included in the array _controllersByPartition["EquityClass"].

Mapping from (Partition, Operator) to PartitionController Status

mapping (bytes32 => mapping (address => bool)) internal _isControllerByPartition;
  • Description: This mapping indicates whether an address is recognized as a controller for a specific partition.
  • Purpose: It provides a quick way to check if an address has control rights over a particular partition, ensuring that only authorized controllers can perform certain operations.
  • Example Usage: When a new controller is added for the “DividendClass” partition, their address is set to true in this mapping. For example, _isControllerByPartition["DividendClass"][RegulatoryAuthorityAddress] = true.

ERC1820Client and IERC1820Registry

The ERC1820 standard provides a universal registry smart contract where any address (contract or regular address) can register which interface it implements and at which address. This is particularly useful for dynamically discovering contract functionalities and ensuring compatibility with different standards.

In the context of ERC-1400, the ERC1820 registry is used to register the ERC-1400 contract itself as well as various extensions and roles, such as token validators and checkers.

ERC1820Client

The ERC1820Client is a utility contract that provides functions to interact with the ERC1820 registry. It simplifies the process of registering interfaces and querying the registry for interface implementations.

IERC1820Registry

The IERC1820Registry is an interface for the ERC1820 registry contract. It defines the standard functions for registering and querying interface implementations.

How the ERC-1400 Contract is Registered

During the deployment of an ERC-1400 contract, it registers itself with the ERC1820 registry using the ERC1820Client. This registration ensures that the contract can be discovered and interacted with using the ERC-1400 standard.

Registration Code in the Constructor

Here’s an example of how the ERC-1400 contract registers itself:

constructor(
string memory tokenName,
string memory tokenSymbol,
uint256 tokenGranularity,
address[] memory initialControllers,
bytes32[] memory defaultPartitions
) {
_name = tokenName;
_symbol = tokenSymbol;
_totalSupply = 0;
require(tokenGranularity >= 1);
_granularity = tokenGranularity;
    _setControllers(initialControllers);
_defaultPartitions = defaultPartitions;
_isControllable = true;
_isIssuable = true;
// Register contract in ERC1820 registry
ERC1820Client.setInterfaceImplementation(ERC1400_INTERFACE_NAME, address(this));
ERC1820Client.setInterfaceImplementation(ERC20_INTERFACE_NAME, address(this));
// Indicate token verifies ERC1400 and ERC20 interfaces
ERC1820Implementer._setInterface(ERC1400_INTERFACE_NAME);
ERC1820Implementer._setInterface(ERC20_INTERFACE_NAME);
}

In this constructor:

  • The ERC1820Client.setInterfaceImplementation function is called twice to register the ERC-1400 contract as both an ERC1400Token and an ERC20Token.
  • The ERC1820Implementer._setInterface function is called to mark the contract as implementing these interfaces.

How to Register Other Extensions

Other extensions, such as token validators and checkers, can be registered in a similar manner. The ERC-1400 contract includes a function _setTokenExtension to facilitate this process.

Function to Set Token Extensions

Here is an example of how extensions can be registered:

function _setTokenExtension(
address extension,
string memory interfaceLabel,
bool removeOldExtensionRoles,
bool addMinterRoleForExtension,
bool addControllerRoleForExtension
) internal {
address oldExtension = interfaceAddr(address(this), interfaceLabel);
    if (oldExtension != address(0) && removeOldExtensionRoles) {
if(isMinter(oldExtension)) {
_removeMinter(oldExtension);
}
_isController[oldExtension] = false;
}
ERC1820Client.setInterfaceImplementation(interfaceLabel, extension);
if(addMinterRoleForExtension && !isMinter(extension)) {
_addMinter(extension);
}
if (addControllerRoleForExtension) {
_isController[extension] = true;
}
}

In this function:

  • The interfaceAddr function is used to check if there is an existing extension registered for the given interfaceLabel.
  • If an old extension exists and removeOldExtensionRoles is true, the roles of the old extension (like minter or controller) are removed.
  • The new extension is registered using ERC1820Client.setInterfaceImplementation.
  • The new extension can be granted minter and controller roles if specified.

Example Usage of _setTokenExtension

Suppose you want to register a new token validator extension:

function registerTokenValidator(address validator) external onlyOwner {
_setTokenExtension(
validator,
ERC1400_TOKENS_VALIDATOR,
true, // Remove roles from old extension
false, // Do not add minter role for the new extension
true // Add controller role for the new extension
);
}

This function would:

  • Remove the roles of any existing validator extension.
  • Register the new validator extension.
  • Assign the controller role to the new validator extension.

Conclusion

The ERC-1400 standard is a powerful and flexible framework for creating security tokens on the Ethereum blockchain. It enhances the basic ERC-20 functionalities by adding support for partitions, document management, controller operations, and regulatory compliance.

By utilizing the ERC1820 registry, ERC-1400 ensures dynamic discovery and interaction with different token components and extensions. This makes it easier to manage and customize tokens according to specific needs and regulatory requirements.

For more details and to see the implementation, you can refer to the ERC-1400 contract repository by ConsenSys.

Personal Thoughts on ERC-1400

While ERC-1400 offers advanced features for security tokens, it’s important to note that it is not officially included in the Ethereum ERC standards. Despite this, some communities still use it.

My Concerns

Complexity and Stability:

  • ERC-1400 is very complex and can be unstable.
  • You can issue (“mint”) tokens without assigning them to a partition, which can lead to mismanagement of tokens.

Incompatibility with ERC-20:

  • ERC-1400 is not compatible with the ERC-20 standard.
  • This incompatibility means it doesn’t work with most DeFi apps.

As Mohamad, I believe ERC-1400’s complexity and lack of compatibility with ERC-20 make it less practical for many users and developers.

--

--