Overview
Security Token smart contract implementation from CoMakery (dba Upside). The core purpose of the token is to enforce transfer restrictions for certain groups.
This implementation attempts to balance simplicity and sufficiency for smart contract security tokens that need to comply with regulatory authorities - without adding unnecessary complexity for simple use cases. It implements the ERC-20 token standard with ERC-1404 security token transfer restrictions.
This approach takes into account yet to be standardized guidance from ERC-1400 (which has additional recommendations for more complex security token needs) and ERC-1404 which offers an approach similar to ERC-902. Unfortunately ERC-1404 does not adopt ERC-1066 standard error codes - which this project may adopt in the future. Since no security token standards have reached mass adoption or maturity and they do not fully agree with each other, the token optimizes for a simple and sufficient implementation.
Simplicity is desirable so that contract functionality is clear. It also reduces the number of smart contract lines that need to be secured (each line of a smart contract is a security liability).
Disclaimer
This open or closed source software is provided with no warranty. This is not legal advice. CoMakery (dba Upside) is not a legal firm and is not your lawyer. Securities are highly regulated across multiple jurisdictions. Issuing a security token incorrectly can result in financial penalties or jail time if done incorrectly. Consult a lawyer and tax advisor. Conduct an independent security audit of the code.
On-Chain Holder/Wallet Management
Active Holder/Wallet management is conducted on-chain and autonomously, with multiple admin configurations that allow flexibility of transfer rule configuration. This greatly simplifies the off-chain accounting and effort required from Transfer (and other) Admins, removing the need to track Wallet holdings across different Holders.
Wallets are not programatically prohibited from assuming multiple Admin roles. We advise against this in practice; however, this must be enforced off-chain.
Holders are allowed to keep multiple Wallet addresses, which can be spread across multiple Transfer Groups (in which case, they would be added to each group's holder count) as well as within Transfer Groups. These Wallets are consolidated under a common holderId
.
- Ex.: Holder A can have 4 wallets spread across two Transfer Groups, X and Y. The Holder can have Wallets 1 & 2 in Group X, and Wallets 3 & 4 in Group Y. They will still count overall as one single Holder, but it will also be considered as a unique holder in Group X and one unique holder in Group Y.
To manage these Holders and their Wallets:
- A hook has been overridden (
_update
) formint/transfer/burn/freeze
functions, automatically checking that recipient Wallet addresses of tokens are cataloged and assignedholderId
s as needed. - If a "new" Wallet address receives a token, a new
holderId
is created for that Wallet address. - Admins can also separately create Holders from Wallet addresses and append Wallet addresses to existing Holder accounts
Transfer Restrictions
The Security Token can be configured after deployment to enforce transfer restrictions such as the ones shown in the diagram below. Each Holder's blockchain Wallet address corresponds to a different group.
This is enforced in TransferRules.sol
.
Example
Only transfers between wallet address groups in the direction of the arrows are allowed:
Here's an example overview of how transfer restrictions could be configured and enforced.
The Transfer Admin for the Token Contract can provision wallet addresses to transfer and receive tokens under certain defined conditions. This is the process for configuring transfer restrictions and executing token transfers:
-
An Investor sends their Anti Money Laundering and Know Your Customer (AML/KYC) information to the Transfer Admin or to a proxy vetting service off-chain to verify this information.
- The benefit of using a qualified third party provider is to avoid needing to store privately identifiable information.
- We recommend implementations use off-chain mechanisms (such as a 3rd party AML/KYC provider) that ensure a given address is approved and is a non-malicious smart contract wallet. However, generally multi-signature type wallets must be allowed in order to provide adequate security for investors.
- This smart contract implementation does not provide a solution for collecting AML/KYC information.
-
The Transfer Admin or Wallet Admin calls
setAddressPermissions(buyerAddress, transferGroup, freezeStatus)
to provision their account. Initially this will be done for the Primary Issuance of tokens to Investors where tokens are distributed directly from the Issuer to Holder wallets, where:buyerAddress
: Buyer/Investor wallet address for which to set permissionstransferGroup
: desired transfer group ID for wallet addressfreezeStatus
: boolean flag signifying whether the address is frozen from executing transfers (if set to true)
-
A potential Investor sends their AML/KYC information to the Transfer Admin or Wallet Admin or a trusted AML/KYC provider.
-
The Transfer Admin or Wallet Admin calls
setAddressPermissions(buyerAddress, transferGroup, freezeStatus)
to provision the Investor account. -
At this time (or potentially earlier), the Transfer Admin or Wallet Admin authorizes the transfer of tokens between account groups with
setAllowGroupTransfer(fromGroup, toGroup, afterTimestamp)
. Note that allowing a transfer from group A to group B by default does not allow the reverse transfer from group B to group A. This would need to be configured separately.- Ex.: Reg CF unaccredited Investors may be allowed to sell to Accredited US Investors but not vice versa.
Relevant methods:
/**
* @dev Sets an allowed transfer from a group to another group beginning at a specific time.
* There is only one definitive rule per from and to group.
* @param from The group the transfer is coming from.
* @param to The group the transfer is going to.
* @param lockedUntil The unix timestamp that the transfer is locked until. 0 is a special number. 0 means the transfer is not allowed.
* This is because in the smart contract mapping all pairs are implicitly defined with a default lockedUntil value of 0.
* But no transfers should be authorized until explicitly allowed. Thus 0 must mean no transfer is allowed.
*/
function setAllowGroupTransfer(
uint256 from,
uint256 to,
uint256 lockedUntil
) external onlyTransferAdmin {
/**
* @dev A convenience method for updating the transfer group and freeze status.
* @notice This function has a different signature than the Utility Token implementation
* @param buyerAddr_ The wallet address to set permissions for.
* @param groupId_ The desired groupId to set for the address.
* @param freezeStatus_ The frozenAddress status of the address. True means frozen false means not frozen.
*/
function setAddressPermissions(
address buyerAddr_,
uint256 groupId_,
bool freezeStatus_
) external validAddress(buyerAddr_) onlyWalletsAdminOrTransferAdmin {
WARNING: Maximum Total Supply, Minting and Burning of Tokens
The variable maxTotalSupply
is set when the contract is created and limits the total number of tokens that can be minted. It represents authorized shares and can be updated per company legal documents by setMaxTotalSupply
.
Reserve Admins can mint tokens to and burn tokens from any address. This is primarily to comply with law enforcement, regulations and stock issuance scenarios - but this centralized power could be abused. Transfer Admins, authorized by Contract Admins, can also update the transfer rules at any moment in time as many times as they want.
Token Supply
There are also values that can be read from the smart contract for total token supply, circulating token supply, and unissued token supply. These correspond to the analogous Authorized shares, Outstanding shares, and Unissued shares, respectively, that exist in the context of stocks.
These methods are totalTokenSupply()
, circulatingTokenSupply()
, and unissuedTokenSupply()
.
Issue shares to investors
The reserve admin distributes the unissuedTokenSupply() to investors using either mint()
or mintReleaseSchedule()
, which increases the circulatingTokenSupply()
. This method of token issuance should be used by issuers.
From a company shares perspective there are “authorized” and “outstanding” (issued) shares. For security tokens the authorized and unissued shares should not be held in a blockchain account. If they are unissued they should be distributed using the mint()
or mintReleaseSchedule()
functions. If shares are repurchased or reclaimed they should be retired by burning them. This is because all shares distributed to blockchain addresses receive dividend distributions.
Issued is a synonym for outstanding.
It should always hold true that circulatingTokenSupply == outstanding == issued
, as these terms represent the same concept. Both circulatingTokenSupply()
and totalSupply()
reflect this value on-chain, ensuring consistency across these metrics.
Any “treasury” should not be issued on chain in a blockchain address. authorized == maxTokenSupply
(also synonyms).
Overview of Transfer Restriction Enforcement Functions
From | To | Restrict | Enforced By | Admin Role |
---|---|---|---|---|
Reg D/S/CF | Anyone | Until TimeLock ends | fundReleaseSchedule(investorAddress, balanceReserved, commencementTime, scheduleId, cancelableByAddresses) | Any Admin |
Reg D/S/CF | Anyone | Until TimeLock ends | mintReleaseSchedule(investorAddress, balanceReserved, commencementTime, scheduleId, cancelableByAddresses) | Reserve Admin |
Reg S Group | US Accredited | Forbidden During Flowback Restriction Period | setAllowGroupTransfer(fromGroupS, toGroupD, afterTime) | Transfer Admin |
Reg S Group | Reg S Group | Forbidden Until Shorter Reg S TimeLock Ended | setAllowGroupTransfer(fromGroupS, toGroupS, afterTime) | Transfer Admin |
Issuer | Reg CF with > maximum number of total holders allowed | Forbid transfers increasing number of total Holders (across all groups) above a certain threshold | setHolderMax(maxAmount) | Transfer Admin |
Issuer | Reg CF with > maximum number of Holders per group allowed | Forbid transfers increasing number of total Holders (within each group) above a certain threshold | setHolderGroupMax(transferGroupID, maxAmount) | Transfer Admin |
Stolen Tokens | Anyone | Fix With Freeze, Burn, Reissue | freeze(address, isFrozenFlag); burn(address, amount); mint(newOwnerAddress); | Wallets Admin or Transfer Admin can freeze() and Reserve Admin can do mint() burn() |
Any Address During Regulatory Freeze | Anyone | Forbid all transfers while paused | pause(isPausedFlag) | Transfer Admin |
Any Address During Regulatory Freeze | Anyone | Unpause from a paused state | pause(isPausedFlag) | Transfer Admin |
Anyone | Anyone | Force the transfer of tokens for emergencies | forceTransferBetween(sender, recipient, amount) | Reserve Admin |
Roles
The smart contract enforces specific admin roles. The roles divide responsibilities to reduce abuse vectors and create checks and balances. Ideally each role should be managed by a separate admin with separate key control.
In some cases, such as for the Contract Admin or Wallets Admin, it is recommended that the role's private key is managed through multi-signature (e.g. requiring 2 of 3 or N of M approvers) authentication.
Admin Types
The Admin functionality breaks down into 4 main roles. The contract is configured to expect these wallets. In the contract constructor:
- Contract Admin
- Akin to root level access administrator. Can upgrade internal contract dependencies (ie
RestrictedSwap
,TransferRules
) or grant Admin permissions. Recommended to be a secure multi-sig wallet.
- Akin to root level access administrator. Can upgrade internal contract dependencies (ie
- Reserve Admin
- Receives initial tranche of minted tokens from deployment. Also can adjust the supply of tokens by minting or burning or forcibly initiate transfers.
Via granted roles (from Contract Admin):
- Transfer Admin
- Can set transfer restriction permissions/rules between groups.
- Transfer Admin capabilities are a superset of those of Wallets Admin.
- Wallets Admin
- Can manage Holder/Wallet transfer group assignments.
Typically any legal entity third-party Transfer Agent will need access to both the roles for Transfer Admin and Wallets Admin. However some agents (such as exchanges) will, for example, be able to assign groups to wallets and permission them (as a Wallets Admin) but will not be able to adjust the transfer rules.
Admin Functionality
Function | Contract Admin | Reserve Admin | Transfer Admin | Wallets Admin |
---|---|---|---|---|
grantRole() | yes | no | no | no |
revokeRole() | yes | no | no | no |
upgradeTransferRules() | yes | no | no | no |
pause() or unpause (ie pause(false)) | yes | no | yes | no |
mint() | no | yes | no | no |
burn() | no | yes | no | no |
forceTransferBetween() | no | yes | no | no |
batchMintReleaseSchedule() | no | yes | no | no |
mintReleaseSchedule() | no | yes | no | no |
setMaxTotalSupply() | no | yes | no | no |
setAllowGroupTransfer() | no | no | yes | no |
setHolderMax() | no | no | yes | no |
setHolderGroupMax() | no | no | yes | no |
fundDividend() | no | no | yes | no |
setAddressPermissions() | no | no | yes | yes |
freeze() | no | no | yes | yes |
setTransferGroup() | no | no | yes | yes |
createHolderFromAddress() | no | no | yes | yes |
appendHolderAddress() | no | no | yes | yes |
addHolderWithAddresses() | no | no | yes | yes |
removeHolder() | no | no | yes | yes |
removeWalletFromHolder() | no | no | yes | yes |
batchRemoveWalletFromHolder() | no | no | yes | yes |
createReleaseSchedule() | yes | yes | yes | yes |
batchFundReleaseSchedule() | yes | yes | yes | yes |
fundReleaseSchedule() | yes | yes | yes | yes |
Use Cases
Initial Security Token Deployment
- The Deployer configures the parameters and deploys the smart contracts to a public EVM blockchain. At the time of deployment, the deployer configures a separate Reserve Admin address, a Transfer Admin address, and a Wallets Admin address. This allows the reserve security tokens to be stored in cold storage since the treasury Reserve Admin address private keys are not needed for everyday use by the Transfer Admin.
- The Reserve Admin then provisions a Wallets Admin address for distributing tokens to investors or other stakeholders. The Wallets Admin uses
setAddressPermissions(investorAddress, transferGroup, freezeStatus)
to set address restrictions. - The Transfer Admin authorizes the transfer of tokens between account groups with
setAllowGroupTransfer(fromGroup, toGroup, afterTimestamp)
. - The Reserve Admin then transfers tokens to the Wallets Admin address.
- The Wallets Admin then transfers tokens to Investors or other stakeholders who are entitled to tokens.
Setup For Separate Issuer Private Key Management Roles
By default the reserve tokens cannot be transferred to. To allow transfers the Transfer Admin or Wallets Admin must configure transfer rules using both setAddressPermissions(account, ...)
to configure the individual account rules and setAllowGroupTransfer(...)
to configure transfers between accounts in a group. A group represents a category like US accredited investors (Reg D) or foreign investors (Reg S).
During the setup process to split transfer oversight across three private key holders, the Transfer Admin can setup rules that only allow the Reserve Admin group to only transfer tokens to the Wallets Admin address group. The Wallets Admin should be restricted to a limited maximum balance necessary for doing one batch of token distributions - rather than the whole reserve. The use of a hot wallet Wallets Admin for small balances also makes everyday token administration easier without exposing the issuer's reserve of tokens to the risk of total theft in a single transaction. Each of these private keys may also be managed with a multi-sig solution for added security.
Multi-sig is especially important for the token Reserve Admin and Contract Admin.
Here is how these restricted Admin accounts can be configured:
- Transfer Admin, Reserve Admin and Wallets Admin accounts are managed by separate users with separate keys. For example, separate Nano Ledger S hardware wallets.
- Reserve and Wallets Admin addresses can have their own separate transfer groups.
setAddressPermissions(reserveAdminAddress, reserveAdminTransferGroup, freezeStatus)
setAddressPermissions(walletsAdminAddress, walletsAdminTransferGroup, freezeStatus)
- Reserve Address Group can only transfer to Wallets Admin Groups after a certain Timestamp.
setAllowGroupTransfer(reserveAdminTransferGroup, walletsAdminTransferGroup, afterTimestamp)
- Wallets Admin Address can transfer to investor groups like Reg D and Reg S after a certain Timestamp.
setAllowGroupTransfer(walletsAdminTransferGroup, regD_TransferGroup, afterTimestamp)
setAllowGroupTransfer(walletsAdminTransferGroup, regS_TransferGroup, afterTimestamp)
Then the Wallets Admin can distribute tokens to investors and stakeholders as described below...
Issuing the Token To AML / KYC'd Recipients
-
The Transfer Admin gathers AML/KYC and accreditation information from investors and stakeholders who will receive tokens directly from the Issuer (the Primary Issuance).
-
(optional) set the Holder max if desired
-
Transfer Admin configures approved Transfer Group for Wallets Admin.
-
Transfer Admin then configures approved Transfer Groups for Investor and stakeholders with
setAddressPermissions(address, transferGroup, freezeStatus)
. Based on the AML/KYC and accreditation process the investor can provision the account address with:a) a transfer group designating a regulatory class like "Reg D", "Reg CF" or "Reg S"
b) the freeze status of that address (with true meaning the account will be frozen from activity)]
-
The Transfer Admin then must allow transfers between groups with
setAllowGroupTransfer(walletsAdminGroup, investorAdminGroup...)
-
The tokens can then be transferred from the Issuer's wallet to the provisioned addresses.
-
Transfer Restrictions are detected.
Note that there are no transfers initially authorized between groups. By default no transfers are allowed between groups - all transfer groups are restricted.
Lockup Periods
Lockup periods are enforced via:
setAllowGroupTransfer(fromGroup, toGroup, unixTimestamp)
allows transfers from one Transfer Group to another after the unixTimestamp. If the unixTimestamp is 0, then no transfer is allowed.
Maximum Number of Holders Allowed
By default Transfer Groups cannot receive token transfers. To receive tokens the issuer gathers AML/KYC information and then calls setAddressPermissions()
.
A single Holder may have unlimited Wallet addresses. This cannot be altered. The Issuer can only configure the maximum number of Holders allowed (via Transfer Admin) by calling setHolderMax
. By default, this limit is set to 2**255-1
.
setHolderMax(amount)
Maximum Number of Holders Allowed Per Group
Transfer Admin can configure the maximum number of allowed Holders per group by calling setHolderGroupMax
. By default, the holderGroupMax
for each group is set to 0, meaning that it won't be applied as a restriction.
Group 0 (the default Holder group) is the only group for which the holder group max variable cannot be applied (an unlimited number of Group 0 Holders are allowed in perpetuity).
setHolderGroupMax(groupID, amount)
Remove holder and wallet from holder
To efficiently remove unused holders and their associated wallets, transfer or wallet admins can call the removeHolder
function, which iterates through all linked wallets and removes them. However, for holders with unusually large number of wallets, to avoid reaching block gas limit, it is possible first using batchRemoveWalletFromHolder
and then calling removeHolder
.
If a user does not have ownership of a specific wallet, it can still be removed from the holder's list individually using the removeWalletFromHolder
function.
setTransferGroup
vs setAddressPermissions
setAddressPermissions
is very similar to setTransferGroup
, but contains a third argument with frozen
status. This status can allow or deny transfers to the provided user.
setAddressPermissions
is usually used after KYC/AML verification to activate a particular wallet for transfers. So in cases where you do not need to change the frozen
status, you should use setTransferGroup
. The frozen
status restrictions described in the TransferRules
contract.
Timelock Cancellations and Transfers
Cancel Timelock
In the case of cancellations, the transfer restrictions must be enabled between the initial target recipient and the reclaimTo address designated by the canceler.
Timelocks can be configured to be cancelled by a specific canceler address. This canceler can designate a separate reclaim address to receive the locked token portion of the remaining timelock, pending allowed group transfers as described above. The remaining unlocked portion will be transferred directly to the initial target recipient of the original vesting.
Transfer Timelock
For unlocked tokens within a timelock, the initial target recipient can choose to transfer unlocked tokens directly to another recipient. This is a convenience atop the typical transfer method.
In the case of a timelock transfer, the initial target recipient of the timelock must be in a group able to transfer tokens to the new recipient of the tokens. The initial target recipient must also be in a group able to transfer tokens to the new recipient.
Example Transfer Restrictions
Example: Investors Can Trade With Other Investors In The Same Group (e.g. Reg S)
To allow trading in a group:
- Call
setAddressPermissions(address, transferGroup, freezeStatus)
for trader Wallets in the group setAllowGroupTransfer(fromGroupX, toGroupX, groupTimeLock)
for Wallets associated with groupIDs (for example, Reg S)- A token transfer for an allowed group will succeed if:
- the timelock conditions have passed
- the recipient of a token transfer does not result in a total holder count in a given group that exceeds the defined
holderGroupMax
(if configured, ieholderGroupMax
set to > 0) - the recipient of a token transfer does not result in a total global holder count that exceeds the defined
holderMax
Example: Avoiding Flowback of Reg S "Foreign" Assets
To allow trading between Foreign Reg S account addresses but forbid flow back to US Reg D account addresses until the end of the Reg D lockup period
- Call
setAddressPermissions(address, groupIDForRegS, freezeStatus)
to configure settings for Reg S investors - Call
setAddressPermissions(address, groupIDForRegD, freezeStatus)
to configure settings for Reg D investors setAllowGroupTransfer(groupIDForRegS, groupIDForRegS, addressTimelock)
allow Reg S trading- A token transfer for between allowed groups will succeed if:
- the
addressTimelock
time has passed; and - the recipient of a token transfer is not frozen (ie
freezeStatus
isfalse
).
- the