-
Notifications
You must be signed in to change notification settings - Fork 293
Home
OEP: 506 Title: Security Token Offering Standard Author: javajoker < [email protected]>, tanyuan < [email protected]>, Honglei-Cong< [email protected]> Type: Standard Status: Draft Created: 2018-11-29
This OEP Proposal standardizes a series of integral standard interfaces for Security Token Offering and captures other requirements such as KYC in additional to the token interface . It will facilitate the implementation of security token, which is considered to be a subclass of partially fungible token here, with smart contracts and already existing standard services on Ontology platform.
A related set of standard interfaces are defined here, not just the interface for token, to capture the whole life-cycle requirements of STO. Existing standard services of Ontology platform are leveraged to make this proposal an integral part of the ecosystem.
Several already existing services are reused here, which makes the proposal an integral part of the eco-system and ease the development of smart contracts.
ONT ID, which defines the format and CRUD operations for digital identity(ONT ID) https://github.com/ontio/ontology-DID/blob/master/docs/en/DID-ONT-method.md
ONT ID Claim, which defines the format of ONT ID claims and related services. https://github.com/ontio/ontology-DID/blob/master/docs/en/claim_spec.md
Auth Contract, which defines a basic means of ACL for function calls of smart contracts, based on ONT ID. https://github.com/ontio/ontology-smartcontract/blob/master/smartcontract/native/auth/auth.md
Several related interfaces are defined here, that is, KYC Service, Security Token.
This proposal contains more than what you can find in the ERC-1400 specification or so. We do so since we recognize that KYC constitutes an important and indispensable part of STO.
Though several different interfaces are defined here, one can still choose to implement them in one smart contract or so. Since the regulation rules are prone to change often, it is possible to delegate some of the regulation compliance checking to another smart contract, which can be upgraded independently.
We admit that those given interfaces can’t cover every possible business scenarios in themselves. But used as the basic building blocks, more functionality could be built upon them, in separate smart contracts. For example, dividend distribution or stock splits could easily be implemented in a higher level.
This interface provides the operations on KYC related information. When implementing this services, it is recommended that ONT ID and ONT CLAIM services are fully utilized. This interface defines the necessary enhancement to those basic services according to the requirements of STO business.
The prototypes of function calls in interface are always the CRUD operations on some data model. So the really essential things are the data model. For KYCServices, the data models are described in detail through table 1 to table 4.
name | type | description |
address | []byte | primary key |
ontId | string | the corresponding ONT ID of the address |
pubKeyId | int | the public key id in the ONT ID document |
privkeyOwnerId | string | usuallyit should be the same as ontId. But in some case the private key may be ownedby an Exchange. Then it should be the ontId of the exchange |
Only the ontId owner can CRUD. All rolescan Read. |
Basic ONT ID service just provide a means for one to control his own ONT ID via a list of public/private key pairs. Since a public key can not be directly calculated from an ontology address, and the latter is more commonly used in token smart contracts, it is necessary to keep a mapping between address and {ontId, pubKeyId} to ease the use of ONT ID service and ONT CLAIM services. In this way, an address is related to an ontId, which can further be attested via the ONT CLAIM service. In this way, KYC can be conducted and KYC information can be retrieved by the regulatory authorities.
Only ontId owner can change his/her ONT ID document. Entries to this address to ontId mapping can also be added/removed/modified by ontId owner. When doing so, one should demonstrate that he/she owns one of the PK listed in the associated DID Document, just like the case of the modification to the DID document. The relationship between the pubKey and the address should always be checked before addition/modification.
In some case, the address is generated by an Exchange and the ontId owner does not have the private key of the address. The exchange can tell the ontId owner the associated public key and the ontId of the exchange. The ontId owner can then add the address to the mapping.
name | type | description |
ontId | string | primary key |
roleName | string | |
claimId | string | |
claimAttestTime | uint64 | claim attest time in unix format |
claimExpireTime | uint64 | claim expiration time in unix format |
claimAttesterOntId | string | the attester of the claim |
extraClaimInfo | string | extra claim information can be put into this field when necessary. |
Only user with Regulator role can CRUD. All roles can Read. Normally, just being a common accredited investor is not sufficient to be a KYC provider.
The enum value of roleName is left to the choice of token implementer. Possible values are {“TokenIssuer”, “KYC_Provider”, “Exchange”, “Qualified_Investor”, “Regulator”, “Auditor”}
It may be implemented as a cascade of mappings: {ontId →{roleName →{claimiD → claim attributes}}}
|
This data structure records the claims supporting the ontId to take up a specific role named by roleName. For example, to become accredited investor, one should pass necessary KYC and investment qualification tests, and be attested by some ontId with required role.
Each ontid can have multiple roles, each supported by several attested claims. For example, being accredited investor, one could have obtained different qualifications from different Exchanges.
Auth services does records the {ontId, roleName} mapping, but does not expose a query interface for a list of roles one can take. So the information in table 4 should be maintained and used carefully together with the Auth service.
Since auth service is provided at the smart contract level, it is recommended to introduce a proxy smart contract which exposes all the token related interfaces and concentrates all the ACL control.
In order to keep the privacy, the ONT CLAIM service does not store the detail of the claim on the blockchain. In the runtime, the presence of just one attested record with an un-expired state, along with the supporting result returned from the ONT CLAIM service will be enough to support an ontId to have a role.
In brief, from an address one can map it to an ontId, and then to a list of possible roles taken by that address. And one can further find out whether this role is supported by valid claims or not.
The following two pieces of information are introduced for regulatory purpose.
name | type | description |
address | []byte | primary key |
operatorOntId | string | string suspended by whom |
memo | string | anything worth mention, for example, it can describes the start time of the suspension and the reason for that. A suspended address should cease to behave actively as normal. According to the punishment actions taken, it may or may not be able to maintain its original token positions further. |
Only regulator can CRUD. All roles can Read. |
name | type | description |
ontId | string | primary key |
operatorOntId | string | string suspended by whom |
memo | string | anything worth mention, for example, it can describes the start time of the suspension and the reason for that. A suspended ontId should cease to behave actively as normal. According to the punishment actions taken, it may or may not be able to maintain its original token positions furthur. |
Only regulator can CRUD. All roles can Read. |
A regulator may choose to suspend the use of an address or even an ontId due to illegal actions found in that address. The presence of a record in this kind of data structure means a still effective suspension. The primary key field presented here may not have a corresponding record in table 1 or in other services, due to the possible delete operation taken. But such record still exists to serve as a reminder of the punishment actions taken by the regulator.
func setAddrToOntId(operator []byte, address []byte, pubKeyId int, privKeyOwnerId string) bool event setAddrToOntId(operator []byte, address []byte, ontId string, pubKeyId string, privKeyOwnerId string)
Operator set the mapping from address
to ontId
, specify pubKeyId
as the corresponding public key id for that address
and privKeyOwnerId
as the ONT ID of the real private key owner of the address. The ontId
and pubKeyId
should already exists in the ONT ID service. Throw an exception when anything goes wrong, for example, when the format of the parameters is not correct, or the pubKeyId does not match the given address, or the roles of the caller are not allowed to make this call.
The parameters operator
and address
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func getAddrToOntId(address []byte) (ontId string, pubKeyId string, privKeyOwnerId string)
type claimInfo struct {
claimAttestTime uint64
claimExpireTime uint64
claimAttesterOntId string
extraClaimInfo string
}
func setClaimInfo(operator []byte, ontId string, roleName string, claimId string, info claimInfo) bool
event setClaimInfo(operator []byte, ontId string, roleName string, claimId string, info claimInfo)
operator
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func getClaimInfo(ontId string, roleName string, claimId string) (info claimInfo)
func isValidClaim(ontId string, roleName string, claimId string) bool
func getOntIdSuspendStatus(ontId string) (yes bool, operatorOntId string, memo string)
func setAddressSuspendStatus(operator []byte, address []byte, suspend bool, memo string) bool
event setAddressSuspendStatus(operator []byte, address []byte, suspend bool, memo string)
func getOntIdSuspendStatus(ontId string) (yes bool, operatorOntId string, memo string)
func setOntIdSuspendStatus(operator []byte, ontId string, suspend bool, memo string) bool
event setOntIdSuspendStatus(operator []byte, ontId string, suspend bool, memo string)
This interface provides the basic operations on token definition, token transferation and investor position management, and is roughly comparable to the ERC 1400 specification found iin Ethereum.
The SecurityToken interface manages several pieces of information. One is for the basic information of tokens, and one is the token positions for each address, organized into different tranches. The interface also defines a basic description for tranches.
Tranche is a very important feature for partially fungible tokens, which is a super class of security token. Tranche can be viewed as a sub-account for address, so (address, tranche) combines to uniquely determines a record of token position. Tranche can also be viewed as a sub-class of security token, in this case, {address} alone is the owner’s account, and {tranche} just stands for the owned property.
Stocks issued can be classified as preferred stocks or common stocks. For preferred stocks, a bunch of sub-types can be defined and may be restricted only by the creativity of the issuer. Most preferred stocks may have fix dividend rate, whereas most common stocks may not have a fixed dividend rate. The voting privileges for different types of stocks may also vary quite differently. So the real meaning of different tranche is left for the implementer. In this specification, tranche is uniquely defined by an uint64. It is recommended to implement the tranche as a combination of bit masks, but it is not a must.
name | type | description |
name | string | |
symbol | string | |
decimals | uint8 | |
totalSupply | big.Int | sum of supplies of all tranches. |
defaultTranche | uint64 | A default tranche can be defined globally to simplify the function calls and make them looks like that of fungible tokens. |
documents | map[string]DocInfo type DocInfo struct{ uri string hashType uint8 hash []byte validTime uint64 } | fingerprint and uri of supporting documents for the token document name should contain version information in it. validTime is the time when this document becomes legal and effective. |
extraTokenInfo | map[string]string | extra token information can be kept in this mapping and can be used for different purpose, such as mere display or referenced in the smart contract. Its interpretation is left for the token implementer |
tokenStatus | uint8 | it may (but not forced) be modeled as a series of bit masks, with one mask stands for legal approval/dis-approval, and with one mask stands for suspend/unspspend.But its real interpretation is left for the token implementer |
The issuer can modify the information. Regulators can change the status too. |
name | type | description |
tranche | uint64 | primary key |
description | string | descriptions for display purpose |
trancheSupply | big.Int | supply of this tranche. May vary over time. |
lockUntil | uint64 | unix timestamp. The tokens in this tranche cannot be transferred out before that time. It is the default setting for this tranche, can be override by the meta data associated with a specific position. sufficiently small number(1, for example) means not locked. 0xFFFFFFFFFFFFFFFF means lock until unlocked manually |
extraMeta | map[string]interface{} | Other meta data. Defined and interpreted by impementer. May contain the callable/non-callable feature, fixed dividend rate, conversion rate to common stocks or so. Since not all features are commonly used, so they are put here for extension purpose only. |
Only the issurer can modify this information. |
name | type | description |
address | []byte | primary key |
tranche | uint64 | |
balance | balance | |
lockUntil | uint64 | unix timestamp. The tokens in this position cannot be transferred out before that time. Normally it will be 0, which means just see the lockUntil setting of the tranche. Other values will take precedence of the setting of the tranche. Not affected by transfer in operations, but restrict the behavior of transfer out operations. This field should be explictly tagged by regulator or so to explicitly lockup one’s special position. |
allowance | map[address] big.Int | allows other address to transfer no more than this amount of tokens on behalf of the owner. |
func name() (string)
func symbol() (string)
func decimals() (uint8)
func totalSupply() (big.Int)
func totalSupply() (big.Int)
type DocInfo struct {
uri string
hashType uint8
hash []byte
validTime uint64
}
func getDocument(doc string) (info DocInfo)
func setDocument(doc string, info DocInfo) bool
event setDocuent(operator []byte, doc string, info DocInfo)
func getExtraTokenInfo(key string) string
func setExtraTokenInfo(key string, val string) bool
event setExtraTokenInfo(operator []byte, key string, val string)
func getTokenStatus() uint8
func setTokenStatus(newState uint8) bool
event setTokenStatus(operator []byte, newState uint8)
func transfer(fromTranche uint64, from []byte, to[]byte, toTranche uint64, srcAmount big.Int, byte[] data) bool
func transfer(form []byte, to[]byte, srcAmount big.Int, byte[] data) bool
event transfer(from []byte, fromTranche uint64, to[]byte, toTranche uint64, srcAmount big.Int)
The succinct form of transfer is just an abbreviation of the extensive form without explicit fromTranche and toTranche. Those two parameters are implicitly set to the global default tranche.
func approve(approver []byte, spender []byte, tranche uint64, value big.Int, byte[] data) bool
func approve(approver []byte, spender []byte,value big.Int, byte[] data) bool
event approve(approver []byte, spender []byte, tranche uint64, value big.Int)
The parameters approver
and spender
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func transferFrom(tranferer []byte, from []byte, fromTranche uint64, to[]byte, toTranche uint64, srcAmount big.Int) bool
func transferFrom(tranferer []byte, from []byte, to[]byte, srcAmount big.Int) bool
event transferFrom(tranferer []byte, from []byte, fromTranche uint64, to[]byte, toTranche uint64, srcAmount big.Int)
Normally, to should be a valid investor or issuer. As a special case, an all 0 black hole address which can be controlled by nobody can be used in this function as a means to burn token. So we do not introduce a separate burn() function here.
The succinct form of transferFrom is just an abbreviation of the extensive form without explicit fromTranche and toTranche. Those two parameters are implicitly set to the global default tranche.
The parameters tranferer
, from
and to
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func allowance(owner []byte, tranche uint64, spender []byte) (remaining big.Int)
func allowance(owner []byte,spender []byte) (remaining big.Int)
func balanceOf(owner []byte, tranche uint64) (balance big.Int)
func balanceOf (owner []byte) (balance big.Int)
func lockPosition(address []byte, tranche uint64, lockUntil uint64) bool
func lockPosition(address []byte, lockUntil uint64) bool
event lockPosition(address []byte, tranche uint64, lockUntil uint64)
The parameters address
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func canTransfer(from []byte, fromTranche uint64, to[]byte, toTranche uint64, srcAmount big.Int) bool
func canTransfer(from []byte, to[]byte, srcAmount big.Int) bool
The parameters from
and to
SHOULD be 20-byte address. If not, this method SHOULD throw an exception.
func getTrancheInfo(tranche uint64) (description string, supply big.Int, lockUntil uint64)
description
,supply
andlockUntil
for the given tranche
func setTrancheInfo(tranche uint64,description string, supply big.Int, lockUntil uint64) bool
event setTrancheInfo(operator []byte, tranche uint64,description string, supply big.Int, lockUntil uint64)
func getTrancheExtraMeta(tranche uint64) (key []string, val []interface{} )
key
andval
for the given tranche
func setTrancheExtraMeta(tranche uint64, key string, val interface{}) bool
event setTrancheExtraMeta(operator []byte, tranche uint64, key string, val interface{})