ERC-7518 Dynamic compliance security token (Dynamic Compliant Interop Security Token)
Review · Tokens · created 2023-09-14
A standard that extends ERC-1155 to manage security tokens within a single contract where rights and regulatory rules differ by partition. It provides token locking, address freezing, forced transfer, dividend payment, and signed voucher based compliance checks.
Why it is needed
Tokenizing a real world asset such as real estate or a fund requires complying with securities regulations. Features are needed to block transfers based on investor eligibility, lock tokens for a holding period, recover tokens from an investor who lost their key, or freeze accounts that violate regulations. However, existing token standards do not standardize these features, so each issuer must implement them differently, making it hard for wallets or trading services to integrate in a common way. Also, even for the same asset, tokens must be managed separately by investor group under different regulations, such as accredited investors in the US versus overseas investors, and deploying a separate contract for each group increases cost and management complexity. This standard solves this by dividing a single contract into partitions by tokenId, applying different regulatory rules per partition, and providing compliance features through a common interface.
What to implement
event TokensLocked(address indexed account, uint indexed id, uint256 amount, uint256 releaseTime);: An event emitted when an account's tokens are locked until a specific release time.event TokensForceTransferred(address indexed from, address indexed to, uint indexed id, uint256 amount);: An event emitted when an administrator forcibly transfers tokens.event AddressFrozen(address indexed account, bytes data);: An event emitted when an account is frozen.event PayoutDelivered(address indexed from, address indexed to, uint256 amount);: An event emitted when a payment such as a dividend is delivered.function transferableBalance(address account, uint id) external view returns (uint);: Returns the amount of an account's balance for a given token ID that can currently be transferred. This is the total balance minus the locked balance.function lockedBalanceOf(address account, uint256 id) external view returns (uint256);: Returns the locked balance of an account for a given token ID.function restrictTransfer(uint id) external returns (bool);: Restricts transfers for an entire token ID.function removeRestriction(uint id) external returns (bool);: Lifts a transfer restriction placed on a token ID. Reverts if the ID was never restricted.function safeTransferFrom(address _from, address _to, uint256 _id, uint256 _value, bytes calldata _data) override external;: Transfers tokens. canTransfer must be called before the transfer to confirm it is allowed.function canTransfer(address from, address to, uint id, uint amount, bytes calldata data) external view returns (bool status);: Checks whether a transfer is allowed. Verifies whether both addresses are frozen, whether the token ID is restricted, and the transferable balance.function lockTokens(address account, uint id, uint256 amount, uint256 releaseTime) external returns (bool);: Locks a set amount of an account's tokens until a specified release time.function unlockToken(address account, uint256 id) external;: Releases all locked tokens whose release time has passed.function forceTransfer(address from, address to, uint256 id, uint256 amount, bytes memory data) external returns (bool);: Forcibly transfers tokens, bypassing normal transfer restrictions. Used for key loss recovery or regulatory action.function freezeAddress(address account, bytes calldata data) external returns (bool);: Freezes an account, blocking transfers and receipt of payments.function unFreeze(address account, bytes memory data) external returns (bool);: Unfreezes a frozen account.function payout(address calldata to, uint256 calldata amount) public returns (bool);: Sends a payment such as a dividend or interest to one address. Reverts if the receiving address is frozen.function batchPayout(address[] calldata to, uint256[] calldata amount) public returns (bool);: Pays out individual amounts to multiple addresses in a single transaction.
In depth
Using tokenId as the partition structure
Each individual tokenId of ERC-1155 is used as a partition with its own rights and rules. Tokens with the same tokenId are fungible with each other and not fungible with other tokenIds. Issuers distinguish stock classes, regulatory jurisdictions, issuance rounds, and so on by issuing them under different tokenIds. As an example from the source, you can separate partitions for US accredited investors (Reg D) and non-US investors (Reg S), issuing tokens into the relevant partition based on investor eligibility, and once the holding period ends, merge them by creating a new consolidated partition where holders deposit their existing partition tokens and receive the consolidated token. For commercial real estate, each floor could have its own partition, with each partition split into multiple tokens to represent fractional ownership.
Locking and transferable balance
lockTokens must lock a set amount of an account's tokens until releaseTime. It must revert if an attempt is made to lock more than the held balance, and only an authorized party should be able to call it. transferableBalance must return the transferable balance calculated as balanceOf(account, id) - lockedBalanceOf(account, id). unlockToken must release all tokens whose release time is at or before block.timestamp, and if there are no tokens to release, it is recommended to revert to save gas. This is used to implement vesting or mandatory holding periods.
canTransfer check rules
canTransfer is a view function that determines whether a transfer is allowed. It must check that from and to are not frozen addresses, that the given id is not restricted, and that amount is within the transferable balance, and may call an external contract to verify this if needed. It must not change state or produce side effects. safeTransferFrom must call canTransfer before proceeding with the transfer, and must revert if the recipient address is the zero address or the balance is insufficient. Implementations that use vouchers also verify the signature, expiration, and binding to from, to, id, and amount of the signed voucher received in the data argument.
Forced transfer and address freezing
forceTransfer moves tokens bypassing normal transfer restrictions and approval checks. It must revert if the from address is not frozen, and must also revert if the to address is frozen. In other words, the order is to first freeze the problem account with freezeAddress and then force the transfer. The source gives the scenario of freezing the account of an investor confirmed to be involved in money laundering or similar and moving tokens to the issuer's or a recovery agent's address, as well as use for recovery in case of key loss. freezeAddress must block transfers and receipt of payments for the account, and unFreeze lifts the freeze. Both functions require access control so that only an authorized party can use them.
Compliance approval extension (IERC7518Authorization)
As an optional extension, it defines an EIP-712 formatted voucher that authorizes a single transfer. The ComplianceAuthorization struct consists of the fields token, authority, operationHash, policyId, contextHash, nonce, validAfter, and validBefore, where token must equal address(this) and the validity period is from validAfter (inclusive) to validBefore (exclusive). operationHash is computed by binding the caller, the parties, the id, the amount, and application data, so it cannot be reused for a different transfer. The voucher is carried in the ERC-1155 data argument inside a standard envelope beginning with the magic value bytes4(0x7518a001). On transfer, after the voucher is validated, it must be marked Consumed within the same transaction and before the external recipient hook is called, to prevent reuse. revokeAuthorization changes an unused voucher to Revoked, and both Consumed and Revoked are kept as final states. If authority is a contract, its signature must be verified via ERC-1271, and the EIP-712 domain must be disclosed via ERC-5267.
Asset profile / role / action record extensions
There are three further optional extensions. IERC7518AssetProfile is for asset lookups: assetKey is the keccak256 hash of the abi.encode of chainid, the token contract, tokenId, and partitionId, and assetProfile returns an assetId that identifies the same economic asset even across contract upgrades, the current policy policyId, and profileHash, the keccak256 hash of the profileURI document. identifier looks up values from identification schemes such as ISIN, DTI, or CUSIP. IERC7518Roles defines four role keys, REGISTRAR, TRANSFER_AGENT, PAYING_AGENT, and COMPLIANCE_AUTHORITY, and must emit RoleUpdated on every role change. IERC7518ActionRecord records asset actions such as splits, tender offers, and voting; status ranges from 0 Unknown to 4 Cancelled, and a completed or cancelled action cannot be reverted to a previous state. When extensions are implemented together, combined rules apply. For example, if the authorization extension and the roles extension are used together, authority must hold the COMPLIANCE_AUTHORITY role at the time the voucher is consumed.
Interoperability with other standards (wrapping)
Defines IERC1155Wrapper, a wrapping interface for wrapping tokens of other standards into this standard. wrapToken locks the original token in an on-chain vault-style contract and mints an equal amount of this standard's token, while unwrapToken burns this standard's token and returns the original. When unwrapping, it must be confirmed that the token is not subject to this standard's lock function. For ERC-20 issuances, the entire supply can be mapped to a single partition, and it is recommended to use tokenId = 0 as the default. ERC-721 adapters map one partition per original tokenId, and ERC-1155 adapters map identical tokenId partitions when supply and rights are the same.
Interface identifiers and proposal status
Implementations must implement ERC-1155 and ERC-165, and supportsInterface must return true for the ERC-1155 identifier 0xd9b67a26 and the IERC7518 identifier 0x5a264a5d. Optional extensions each have their own separate identifiers. IERC7518AssetProfile is 0xe8aa3d31, IERC7518Roles is 0x779da628, IERC7518Authorization is 0x5b880cc8, and IERC7518ActionRecord is 0x9118aff3. Support for one extension does not imply support for another. Note that this proposal is currently in Review status, so its contents may still change.
Things to watch out for
- Access control for core functions: Functions with major impact on balances and transfers, such as forceTransfer, freezeAddress, and lockTokens, must be restricted to authorized parties through appropriate access control such as role-based permissions.
- Off-chain voucher security: The compliance service that issues vouchers must have a security system in place to prevent unauthorized issuance or tampering, and the contract must thoroughly verify the authenticity and validity of a voucher before allowing a transfer.
- Replay prevention: The EIP-712 domain, validity period, operation hash, and authorization status must be verified. An authorization must be marked as consumed before the ERC-1155 recipient hook is called, and if the transfer is reverted, the consumption mark must also be reverted.
- Contract signer risk: ERC-1271 signature verification can call untrusted code. Gas must be limited, invalid return values must be handled, and reentrancy must be prevented from altering authorization or transfer state.
- Payout security: payout and batchPayout must be restricted to authorized parties, and it must be confirmed that the total payout does not exceed the available balance. Access control and input validation are key to preventing fraudulent payouts.