ethereum/EIPs

ERC 1594: Core Security Token Standard

adamdossa opened this issue · 12 comments


eip: ERC-1594
title: Core Security Token Standard (part of the ERC-1400 Security Token Standards)
author: Adam Dossa (@adamdossa), Pablo Ruiz (@pabloruiz55), Fabian Vogelsteller (@frozeman), Stephane Gosselin (@thegostep)
discussions-to: #1411
status: Draft
type: Standards Track
category: ERC
created: 2018-09-09
require: ERC-20 (#20), ERC-1066 (#1066)


Simple Summary

This standard sits under the ERC-1400 (#1411) umbrella set of standards related to security tokens.

Provides a standard to support off-chain injection of data into transfers / issuance / redemption and the ability to check the validity of a transfer distinct from it's execution.

Abstract

Incorporates error signalling, off-chain data injection and issuance / redemption semantics.

This standard inherits from ERC-20 (#20) and could be easily extended to meet the ERC-777 (#777) standard if needed.

Motivation

Accelerate the issuance and management of securities on the Ethereum blockchain by specifying a standard interface through which security tokens can be operated on and interrogated by all relevant parties.

Security tokens differ materially from other token use-cases, with more complex interactions between off-chain and on-chain actors, and considerable regulatory scrutiny.

The ability to provide data (e.g. signed authorisation) alongside transfer, issuance and redemption functions allows security tokens to more flexibly implement transfer restrictions without depending on on-chain whitelists exclusively.

Using ERC-1066 (#1066) to provide reason codes as to why a transfer would fail, without requiring a user to actually try and execute a transfer, allows for improved UX and potentially saves gas on what would otherwise be failed transfers.

Formalising issuance and redemption semantics (similar to minting / burning) provides visibility into the total supply of the token and how it has changed over time.

Requirements

See ERC-1400 (#1411) for a full set of requirements across the library of standards.

The following requirements have been compiled following discussions with parties across the Security Token ecosystem.

  • MUST have a standard interface to query if a transfer would be successful and return a reason for failure.
  • MUST emit standard events for issuance and redemption.
  • MAY require signed data to be passed into a transfer transaction in order to validate it on-chain.
  • SHOULD NOT restrict the range of asset classes across jurisdictions which can be represented.
  • MUST be ERC-20 (#20) compatible.
  • COULD be ERC-777 (#777) compatible.

Rationale

Transfer Restrictions

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.

These conditions could be related to metadata of the securities being transferred (i.e. whether they are subject to a lock-up period), 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 / ERC-777 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 and off-chain.

For tokens representing securities the standard introduces a function canTransfer which provides a more general purpose way to achieve this when the reasons for failure are more complex; and a function of the whole transfer (i.e. includes any data sent with the transfer and the receiver of the securities).

In order to support off-chain data inputs to transfer functions, transfer functions are extended to transferWithData / transferFromWithData which can optionally take an additional bytes _data parameter.

In order to provide a richer result than just true or false, a byte return code is returned. This allows us to give a reason for why the transfer failed, or at least which category of reason the failure was in. The ability to query documents and the expected success of a transfer is included in Security Token section.

Specification

Restricted Transfers

canTransfer / canTransferFrom

Transfers of securities may fail for a number of reasons, for example relating to:

  • the identity of the sender or receiver of the tokens
  • limits placed on the specific tokens being transferred (i.e. lockups on certain quantities of token)
  • limits related to the overall state of the token (i.e. total number of investors)

The standard provides an on-chain function to determine whether a transfer will succeed, and return details indicating the reason if the transfer is not valid.

These rules can either be defined using smart contracts and on-chain data, or rely on _data passed as part of the transferWithData function which could represent authorisation for the transfer (e.g. a signed message by a transfer agent attesting to the validity of this specific transfer).

The function will return both a ESC (Ethereum Status Code) following the EIP-1066 standard, and an additional bytes32 parameter that can be used to define application specific reason codes with additional details (for example the transfer restriction rule responsible for making the transfer operation invalid).

If bytes _data is empty, then this corresponds to a check on whether a transfer (or transferFrom) request will succeed, if bytes _data is populated, then this corresponds to a check on transferWithData (or transferFromWithData) will succeed.

canTransfer assumes the sender of tokens is msg.sender and will be executed via transfer or transferWithData whereas canTransferFrom allows the specification of the sender of tokens and that the transfer will be executed via transferFrom or transferFromWithData.

function canTransfer(address _to, uint256 _value, bytes _data) external view returns (byte, bytes32);
function canTransferFrom(address _from, address _to, uint256 _value, bytes _data) external view returns (byte, bytes32);

transferWithData

Transfer restrictions can take many forms and typically involve on-chain rules or whitelists. However for many types of approved transfers, maintaining an on-chain list of approved transfers can be cumbersome and expensive. An alternative is the co-signing approach, where in addition to the token holder approving a token transfer, and authorised entity provides signed data which further validates the transfer.

The bytes _data allows arbitrary data to be submitted alongside the transfer, for the token contract to interpret or record. This could be signed data authorising the transfer (e.g. a dynamic whitelist) but is flexible enough to accomadate other use-cases.

transferWithData MUST emit a Transfer event with details of the transfer.

function transferWithData(address _to, uint256 _value, bytes _data) external;

transferFromWithData

This is the analogy to the transferWithData function.

msg.sender MUST have a sufficient allowance set and this allowance must be debited by the _value.

function transferFromWithData(address _from, address _to, uint256 _value, bytes _data) external;

Token Issuance

isIssuable

A security token issuer can specify that issuance has finished for the token (i.e. no new tokens can be minted or issued).

If a token returns FALSE for isIssuable() then it MUST always return FALSE in the future.

If a token returns FALSE for isIssuable() then it MUST never allow additional tokens to be issued.

function isIssuable() external view returns (bool);

issue

This function must be called to increase the total supply.

The bytes _data parameter can be used to inject off-chain data (e.g. signed data) to authorise or authenticate the issuance and receiver of issued tokens.

When called, this function MUST emit the Issued event.

function issue(address _tokenHolder, uint256 _value, bytes _data) external;

Token Redemption

redeem

Allows a token holder to redeem tokens.

The redeemed tokens must be subtracted from the total supply and the balance of the token holder. The token redemption should act like sending tokens and be subject to the same conditions.

The Redeemed event MUST be emitted every time this function is called.

As with transferWithData this function has a bytes _data parameter that can be used in the token contract to authenticate the redemption.

function redeem(uint256 _value, bytes _data) external;

redeemFrom

This is the analogy to the redeem function.

msg.sender MUST have a sufficient allowance set and this allowance must be debited by the _value.

The Redeemed event MUST be emitted every time this function is called.

function redeemFrom(address _tokenHolder, uint256 _value, bytes _data) external;

Interface

/// @title IERC1594 Security Token Standard
/// @dev See https://github.com/SecurityTokenStandard/EIP-Spec

interface IERC1594 is IERC20 {

    // Transfers
    function transferWithData(address _to, uint256 _value, bytes _data) external;
    function transferFromWithData(address _from, address _to, uint256 _value, bytes _data) external;

    // Token Issuance
    function isIssuable() external view returns (bool);
    function issue(address _tokenHolder, uint256 _value, bytes _data) external;

    // Token Redemption
    function redeem(uint256 _value, bytes _data) external;
    function redeemFrom(address _tokenHolder, uint256 _value, bytes _data) external;

    // Transfer Validity
    function canTransfer(address _to, uint256 _value, bytes _data) external view returns (bool, byte, bytes32);
    function canTransferFrom(address _from, address _to, uint256 _value, bytes _data) external view returns (bool, byte, bytes32);

    // Issuance / Redemption Events
    event Issued(address indexed _operator, address indexed _to, uint256 _value, bytes _data);
    event Redeemed(address indexed _operator, address indexed _from, uint256 _value, bytes _data);

}

References

Hey,

thanks for all of your efforts.

I have a question: Isn't

function canTransferFrom(address _from, address _to, uint256 _value, bytes _data) external view returns (byte, bytes32);

underspecified? Consider the following scenario:

  • A approves B to withdraw 1 token
  • B is blacklisted and therefore shouldn't be allowed to do any transfers => canTransferFrom(A, B, 1) would deny a transfer (return0x10)
  • To withdraw the token, B simply calls transferFrom(A, C, 1) to some
    address C it controls
  • This triggers canTransferFrom(A, C, 1) which allows the transfer (returns 0x11)
  • => the policy inside canTransferFrom cannot detect that B triggered this even though B was
    blacklisted

Therefore, my proposal would be to extend canTransferFrom to:

function canTransferFrom(address _from, address _to, address _forwarder, uint256 _value, bytes _data) external view returns (byte, bytes32);
0age commented

Can’t you infer B from msg.sender? It seems to me that this actually highlights an important distinction: forwarders / operators should have their own blacklist policy independent of blacklisted receivers. (However, this is more of an implementation detail than an interface requirement.)

However, there may be an argument for including an overloaded version of canTransferFrom (and probably canTransfer as well) in order to allow third parties to check if a transfer would succeed when originating from a different caller. As the caller will generally be the same for both, keeping things simple for the default case and avoiding duplication of msg.sender in calldata seems like a good idea.

Can’t you infer B from msg.sender?

Sure. msg.sender can be inferred as long as you stay within the same contract, but I thought it would be more explicit to include it, to show that it might influence the check.

You are right that canTransfer would then also have to be adjusted.

I am just envisioning a scenario where these checks are done by a separate contract which might make sense for reasons of modularity, reusability and upgradability. But I definitely also see your point about simplicity.

Great work @adamdossa . We're pretty in line with this design at Meridio, and the community's move to decompose ERC-1400. A few comments around issuance/redemption as part of the standard:

  • If issuance/redemption is part of the required interface and isIssuable() is defined, then shouldn't afinishIssuance() function be included in the standard? Even if you deem its inclusion out of the scope of an interface, I think it would help explain a non-intuitive concept for readers of the standard.
  • Curious to hear why "issue" and "redeem" were chosen terminology given the prominence of "mint" and "burn." I understand it may be more in line with industry terminology, but on the other hand I would argue "redemption" does not imply convey burn/destroy-tokens functionality at first glance.
  • I'm still hesitant to vie for 1066 status codes in a core standard given their WIP status. Including a Bool as the first returned variable of canTransfer() would be much easier to implement and adopt, and it would still allow people to query transfer success. With a bool as a first parameter, you could still allow for optional 1066 and application specific status codes. Dependency on 1066 comes with an uncertain ETA for completion, and security token projects are going to market soon.

i.e. function canTransfer(address _to, uint256 _value, bytes _data) external view returns (bool success, byte statusCode, bytes32 reason);

A peer at Meridio (Asha Dakshi) pointed out that from an audit perspective, controllerTransfer might be preferable for fund recovery, since events could be emitted on controllerTransfer would show matching debits/credits and sender/recipient. Not sure if controllerTransfer() should necessarily be part of the base standard, but I guess the same could be argued for issuance/redemption. In either case, I'm okay with this being in the standard. The implementation decision should probably be left to the issuer and regulator. @0age interested to hear your thoughts on this.

Is there consensus on whether canTransfer/canTransferFrom should include a bool as a return parameter?

The boolean is included in the "Interface" section of this document, but omitted in the "canTransfer / canTransferFrom" section.

Where are the Jurisdictions defined/registered?

I think canTransferTo would make more sense, since it is the receiver who is restricted rather than the sender. so if the receiver is in the white list, then the transfer can complete.

@cyberience A transfer can fail for various reasons. A whitelisted address is definitely one of those reasons, which essentially add restrictions to who you can send the tokens to. At the same time, a transfer can fail due to insufficient balance from the sender.

Hi folks, we've recently open sourced our implementation of ERC1594 (and ERC1644) here: http://github.com/tenx-tech/tenx-token

Cheers!

There has been no activity on this issue for two months. It will be closed in a week if no further activity occurs. If you would like to move this EIP forward, please respond to any outstanding feedback or add a comment indicating that you have addressed all required feedback and are ready for a review.

This issue was closed due to inactivity. If you are still pursuing it, feel free to reopen it and respond to any feedback or request a review in a comment.

Thanks to everyone involved so far! This is really great work.

Germany has recently made a big move forward and actually put a law in place governing security tokens. As this new law also promotes interoperability of tokens from different issuers, I would like to open up the discussion again.

To speed up the adoption of the standard, I would like to propose the following changes:

  • Similar to ERC-20 I think it makes sense to only focus on the holder of tokens as a target user group of the standard. Therefore, I propose to remove theissue() function. I do see a value in standardizing the operation of a security token but I don’t see many use cases for that right now. On the other hand, enabling exchanges and custodians to work with security tokens more easily seems to be important. But for this, focus on the token holder is sufficient.
  • The redeem() and redeemFrom() functions are based on the assumption that only the holder of a token has the permission to trigger a redemption (either directly or via an allowance). This makes sense from a blockchain community perspective, but “in the wild” it’s common to allow an issuer or a delegated entity to trigger a redemption without the consent of the holder being recorded on the blockchain (similar to the way issuances work). Therefore, I propose to remove the redeem() and redeemFrom() functions following the logic for removing the issue() function as described above.
  • I’m convinced that the Redeemed() and Issued() events are really useful, but I’d like to propose to remove the operator address from the events as this seems not to be relevant from a token holders perspective and would better fit within a “security token operator standard”

Looking forward to hear your thought on this!