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
addressC
it controls - This triggers
canTransferFrom(A, C, 1)
which allows the transfer (returns0x11
) - => 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);
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 ofcanTransfer()
would be much easier to implement and adopt, and it would still allow people to query transfer success. With abool
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 the
issue()
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()
andredeemFrom()
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 theredeem()
andredeemFrom()
functions following the logic for removing theissue()
function as described above. - I’m convinced that the
Redeemed()
andIssued()
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!