Overview
This is a 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 |
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 |
pausePaymentPeriod() | yes | no | yes | no |
unpausePaymentPeriod() | yes | no | yes | no |
pausePaymentAfter() | yes | no | yes | no |
setReclaimerAddress() | yes | no | no | no |
shiftInterestAccrualEnd() | yes | no | yes | no |
createPaymentPeriod() | no | no | yes | no |
updateTotalAccruedInterestForPeriod() | no | no | yes | no |
fundInterest() | no | no | yes | no |
reclaimInterest() | no | no | yes | no |
reclaimInterestForAllRecipients() | no | no | yes | no |
reclaimTotalInterest() | no | no | yes | no |
fundPrincipal() | no | no | yes | no |
reclaimPrincipal() | no | no | yes | no |
earlyRepayment() | yes | no | yes | no |
forceClaim() | no | no | yes | no |
forceClaimForPeriod() | no | no | yes | no |
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
Example: Exchanges Can Register Omnibus Accounts
Centralized exchanges can register custody addresses using the same method as other users. They contact the Issuer to provision accounts and the Transfer Admin or Wallets Admin calls setAddressPermissions()
for the exchange account.
When customers of the exchange want to withdraw tokens from the exchange account they must withdraw into an account that the Transfer Admin has provisioned for them with setAddressPermissions()
.
Talk to a lawyer about when exchange accounts may or may not exceed the maximum number of holders allowed for a token.
Group 0 Transfer Restrictions
In general, it is possible to allow Group 0 transfers. They are configurable in the same way that other transfer restrictions across different groups can be configured. This allowance is required for certain real-world situations. For example, if all tokens are freely tradeable under Reg A+ or a public filing, then Group 0 transfers can be allowed.
Transfers Can Be Paused To Comply With Regulatory Action
If there is a regulatory issue with the token, all transfers may be paused by the Transfer Admin calling pause()
. During normal functioning of the contract pause()
should never need to be called.
The pause()
mechanism has been implemented into the RestrictedLockupToken
and RestrictedSwap
, and InterestPayment
contracts.
Recovery From A Blockchain Fork
Issuers should have a plan for what to do during a blockchain fork. Often security tokens represent a scarce off chain asset and a fork in the blockchain may present ambiguity about who can claim an off chain asset. For example, if 1 token represents 1 ounce of gold, a fork introduces 2 competing claims for 1 ounce of gold.
In the advent of a blockchain fork, the issuer should do something like the following:
- have a clear and previously defined way of signaling which branch of the blockchain is valid
- signal which branch is the system of record at the time of the fork
- call
pause()
on the invalid fork (Transfer Admin) - use
burn()
andmint()
to fix errors that have been agreed to by both parties involved or ruled by a court in the issuers jurisdiction (Reserve Admin)
Law Enforcement Recovery of Stolen Assets
In the case of stolen assets with sufficient legal reason to be returned to their owner, the issuer can call freeze()
(Wallets Admin, Transfer Admin), burn()
, and mint()
(Reserve Admin) to transfer the assets to the appropriate account.
Although this is not in the spirit of a cryptocurrency, it is available as a response to requirements that some regulators impose on blockchain security token projects.
Asset Recovery In The Case of Lost Keys
In the case of lost keys with sufficient legal reason to be returned to their owner, the issuer can call freeze()
, burn()
, and mint()
to transfer the assets to the appropriate account. This opens the issuer up to potential cases of fraud. Handle with care.
Once again, although this is not in the spirit of a cryptocurrency, it is available as a response to requirements that some regulators impose on blockchain security token projects.
Swap
Swap functionality is described in RestrictedSwap.sol
.
RestrictedSwap provides a secure means of swapping tokens between known holders of the primary Restricted token and any payment ERC-20 tokens (ie USDC, DAI, etc).
Note: Swap is not intended between two Restricted tokens. Rather, it is intended as a purchase of Restricted tokens using ERC-20 tokens (AKA payment token or quote token). This is enforced in the swap contract itself, by preventing ERC-1404 compatible tokens (checked via ERC-165 interface support) from being used as payment. However, other types of Restricted tokens may not explicitly support the ERC-1404 interface and thus cannot be checked programatically; therefore, this functionality should be used with caution and payment token types should always be verified first.
How It Works
As a prerequisite, users of the swap functionality are assumed to have passed any necessary off-chain AML/KYC and deemed valid holders. Contracts are also assumed to have been configured properly with admin-granted roles and transfer restrictions in place. In particular, transfers must be allowed between the groups of Seller
(of Restricted token) and Buyer
.
Configuring a Purchase of Restricted Token
As a holder (Buyer
) of an ERC-20 payment token (ie USDC, DAI, etc), one can configure a purchase order for a specified amount of Restricted token with a known party (Seller
). Here is an example involving USDC:
Buyer
must know in advance specifically how much Restricted token they'd like to receive, how much USDC they are willing to pay, and theSeller
on the other end.Buyer
must approve the Swap contract itself to handle their USDC in the purchase amount required.Buyer
then configures an open purchase order by callingconfigureBuy
with the following parameters. A Swap ID is emitted in an event.- amount of Restricted token desired
- address of
Seller
- amount of payment token (USDC) willing to swap
- address of payment token contract
Seller
must approve the Swap contract itself to handle their Restricted token in the sell amount required by the configured swap.Seller
can complete this order by callingcompleteSwapWithRestrictedToken
with the emitted Swap ID.
When transfer restrictions are validated between Buyer
and Seller
, the swap is completed and assets are actually transferred between transacting parties.
Configuring a Sale of Restricted Token
As a holder of the Restricted token, one can also configure a sell order for a specified amount of desired payment ERC-20 token. It is extremely similar to the example above, except terms of the sale must be specified in advance. Here is an example involving USDC:
Seller
must know in advance specifically how much Restricted token they'd like to sell, how much USDC they require, and theBuyer
on the other end.Seller
must approve the Swap contract itself to handle their Restricted token in the sell amount desired.Seller
then configures an open sell order by callingconfigureSell
with the following parameters. A Swap ID is emitted in an event.- amount of Restricted token to sell
- address of
Buyer
- amount of payment token (USDC) required
- address of payment token contract
Buyer
must approve the Swap contract itself to handle their USDC in the amount specified by the configured swap.Buyer
can complete this order by callingcompleteSwapWithQuoteToken
with the emitted Swap ID.
When transfer restrictions are validated between Buyer
and Seller
, the swap is completed and assets are actually transferred between transacting parties.
Lockup
A "vesting" smart contract that can implement token lockup with scheduled release and distribution:
- Can enforce a scheduled release of tokens (e.g. investment lockups)
- Smart contract enforced lockup schedules are used to control the circulating supply and can be an important part of tokenomics.
- Some lockups are cancelable - such as employee vestings. When canceled, unlocked tokens remain in the recipients account address and locked tokens are returned to an address specified by the administrator that has an appropriate transfer group to receive the tokens.
Note that tokens in lockups cannot be burned by admins to avoid significant complexity. In order to burn those tokens, first cancelTimelock
so they are removed from the timelock, and then burn them.
superBalanceOf
There are 3 classes of token types: simple tokens, unlocked tokens, and locked tokens. Locked and unlocked tokens are tokens supplied to the user via either fundReleaseSchedule
or mintReleaseSchedule
, but still evaluated within the timelock.
Unlocked tokens become removed from the timelock once they are transferred, and then are converted into simple tokens. A user's full balance of tokens consists of the sum of their simple, unlocked, and locked tokens. Cancelling a timelock also converts the unlocked tokens into simple tokens that become burnable.
Convenience Balance Methods
unlockedBalanceOf
: tokens that are ONLY supplied viafundReleaseSchedule
ormintReleaseSchedule
that have been unlocked but not yet transferred out (ie not yet "removed" from the timelock). Includes unlocked tokens across all timelocks.superBalanceOf
: simple tokens belonging to the wallet. Does not include locked or unlocked tokens still within timelocks.unlockedTotalBalanceOf
: the sum ofunlockedBalanceOf
andsuperBalanceOf
, ie the total amount of tokens available to the wallet to transfer. Includes unlocked tokens across all timelocks.lockedBalanceOf
: tokens that are ONLY supplied viafundReleaseSchedule
ormintReleaseSchedule
that are still locked. Includes unlocked tokens across all timelocks.balanceOf
: the sum ofunlockedBalanceOf
,lockedBalanceOf
, andsuperBalanceOf
. The total balance belonging to the wallet. Not all of this balance is tradeable though, as this includes locked tokens too.lockedBalanceOfTimelock
: locked balance for a wallet within a specific timelock.unlockedBalanceOfTimelock
: unlocked balance for a wallet within a specific timelock.
fundReleaseSchedule vs mintReleaseSchedule
Lockups, also referred to as token release schedules, are distinguished between funded and minted types. Funded release schedules are done using already-minted tokens that are owned by any admin. These are akin to a type of token transfer and thus must adhere to transfer restrictions.
Release schedules can also be minted directly to recipients by the Reserve Admin. In this case, as it is akin to token minting, it does not have to adhere to transfer restrictions, and is purely under the discretion of the Reserve Admin.
This is described in RestrictedLockupToken.sol
.
Interest Payment
The Interest Payment functionality (InterestPayment.sol
) provides token administrators with the ability to manage and distribute interest payments to token holders based on their token ownership over specific time periods. The contract tracks interest accrual periods and allows token holders to claim their interest when it becomes available.
Key Features
- Configurable interest accrual start and end timestamps
- Ability to create multiple payment periods with different interest amounts
- Admin can fund interest payments and force claim interest on behalf of token holders
- Token holders can claim interest based on their ownership during specific periods
- Support for principal amount funding and claiming at maturity
- Comprehensive admin controls for managing interest distributions
How It Works
Interest payments are distributed to recipients based on their proportional ownership of the RestrictedLockupToken at specific time periods. The contract keeps track of token ownership through the SnapshotsPeriods contract, which records token balances at different timestamps.
- Transfer Admin funds the interest payment contract with a payment token (e.g., USDC) by calling
fundInterest()
. - Transfer Admin creates payment periods using
createPaymentPeriod()
, defining the start and end timestamps and the total interest amount for the period. - Token holders can claim their interest by calling
claimInterest()
orclaimInterestForPeriod()
. - The contract calculates the amount of interest due based on the token holder's ownership percentage during the period.
- Interest is transferred to the token holder from the contract.
Principal Amount Management
The contract also supports principal amount management:
- Transfer Admin can fund principal amounts using
fundPrincipal()
. - Token holders can claim their principal at maturity by calling
claimPrincipal()
. - The principal amount per token is set during contract deployment.
This diagram illustrates the principal funding and claiming process:
-
Approve Funding: The Transfer Admin first approves the Interest Payment contract to spend payment tokens (like USDC) from their account.
-
Fund Principal: The Transfer Admin calls
fundPrincipal()
with the amount to fund, which must be divisible byrestrictedToken.totalSupply()
. -
Wait for Maturity: Principal can only be claimed after reaching the maturity date (defined by
interestAccrualEndTimestamp
). -
Token Holder Approval: To claim principal, the token holder must first approve the Interest Payment contract to transfer their security tokens.
-
Claim Principal: The token holder calls
claimPrincipal()
, which calculates their entitled principal amount based on their token balance. -
Token Transfer: The security tokens are transferred from the token holder to the Interest Payment contract, effectively redeeming them.
-
Principal Payment: The corresponding principal amount in payment tokens is transferred to the token holder.
This process allows token holders to redeem their security tokens for the principal amount at maturity, similar to how a bond works in traditional finance.
Advanced Administrative Functions
The Interest Payment contract provides several advanced administrative functions to adjust the terms of the interest and principal payments after deployment:
Extending Maturity (shiftInterestAccrualEnd)
The shiftInterestAccrualEnd(timestamp)
function allows the Contract Admin or Transfer Admin to extend the maturity date by updating the interestAccrualEndTimestamp
. This is particularly useful when:
- The original maturity date needs to be extended due to business requirements
- The loan or bond term is being renegotiated
- Regulatory requirements necessitate a modification to the maturity date
The new timestamp must be greater than the current interestAccrualEndTimestamp
, ensuring that maturity can only be extended, not reduced.
Managing Interest for Payment Periods
Creating Payment Periods (createPaymentPeriod)
When creating payment periods with createPaymentPeriod(startTimestamp, endTimestamp, totalAccruedInterest)
, the following requirements must be met:
- Periods must be created sequentially, with no gaps between them
- The end timestamp of one period must be the start timestamp of the next period
- The first period must start at
interestAccrualStartTimestamp
- Each period's end timestamp must be a multiple of
interestRatePeriodSeconds
from the start timestamp, except for the final period which may end atinterestAccrualEndTimestamp
- Periods cannot be created for timestamps before
interestAccrualStartTimestamp
or afterinterestAccrualEndTimestamp
This sequential creation ensures continuous tracking of token ownership across the entire interest accrual timeframe.
Updating Accrued Interest for a Period (updateTotalAccruedInterestForPeriod)
The updateTotalAccruedInterestForPeriod(periodIdx, amount)
function allows the Transfer Admin to modify the total accrued interest for a specific payment period. This enables:
- Correcting interest calculation
- Adjusting interest amounts due to changing market conditions
- Setting interest to zero for periods where no interest should accrue
Important: The new amount must be greater than or equal to the sum of already claimed and reclaimed interest for that period. This prevents retroactive reduction of interest that has already been claimed.
Setting a period's interest to zero effectively means that no token holder will accrue interest during that period, regardless of their token ownership. This can be useful for:
- Implementing grace periods
- Reflecting periods of non-performance
- Handling special situations where interest payments should be suspended
These administrative functions provide considerable flexibility in managing the lifecycle of interest-bearing security tokens, allowing adjustments to both principal and interest terms while maintaining the integrity of the payment system.
Reclaiming Interest and Principal
In cases where interest or principal funds need to be reclaimed, administrators can use several functions to reclaim unused funds. This is often necessary in scenarios such as contract migration, correction of funding errors, or legal compliance requirements.
Reclaiming Interest
The Interest Payment contract provides multiple ways to reclaim interest:
- Setup: Contract Admin sets the reclaimer address that will receive all reclaimed funds
- Pause: Transfer Admin pauses payment for the specific period being reclaimed
- Reclaim Options:
reclaimInterest(wallet, paymentPeriodIdx)
: Reclaims unclaimed interest from a specific wallet for a periodreclaimInterestForAllRecipients(paymentPeriodIdx)
: Reclaims all unclaimed interest for an entire periodreclaimTotalInterest(amount)
: Reclaims a specific amount of unused interest
- Unpause: Transfer Admin unpauses payment for the period after reclaiming is complete
Reclaiming Principal
The principal amount can also be reclaimed if needed:
- Setup: Contract Admin sets the reclaimer address that will receive reclaimed funds
- Pause: Transfer Admin pauses the entire contract
- Reclaim: Transfer Admin reclaims all unused principal funds
- Unpause: Transfer Admin unpauses the contract after reclaiming is complete
Important Considerations
- A valid reclaimer address must be set before any reclaiming can occur
- Pausing before reclaiming prevents users from claiming during the reclaim process
- Unpausing after reclaiming allows normal operations to resume
- Only Transfer Admin can reclaim funds, while Contract Admin can set the reclaimer address
- Reclaimed funds are transferred directly to the reclaimer address
Early Repayment
The Interest Payment contract supports early repayment functionality, allowing Contract Admin or Transfer Admin to trigger an early maturity event. This feature is useful in scenarios such as:
- Refinancing debt at a lower interest rate
- Restructuring financial obligations
- Implementing callable bonds that can be paid off before maturity
- Responding to changes in market conditions
The earlyRepayment()
function performs two key actions:
- Sets the interest accrual end timestamp to the current timestamp, effectively stopping any further interest accrual
- Pauses payment after the current timestamp to provide administrative control over final payments
When early repayment is triggered:
- Interest stops accruing immediately, freezing the total interest amount
- Token holders can still claim their accrued interest up to the repayment date
- Principal becomes available for claiming immediately (assuming it has been funded)
- The EarlyRepayment event is emitted, recording the admin address and timestamp
This feature provides important flexibility in managing debt instruments represented by the token, allowing administrators to respond to changing financial conditions while preserving token holders' rights to receive their accrued interest and principal.
Force Claiming Interest
The Interest Payment contract provides administrative functions to force claim interest on behalf of wallet addresses. This is particularly useful in scenarios such as:
- Processing interest distributions for wallets that may not have the means to claim independently
- Managing automated distributions for institutional or managed accounts
- Handling special settlement cases requiring administrative intervention
- Distributing interest from paused payment periods for specific wallets
Two administrative force claim functions are available:
Force Claim for Specific Period (forceClaimForPeriod)
The forceClaimForPeriod(wallet, periodIdx, amount)
function allows the Transfer Admin to force claim interest for a specific wallet from a specific payment period:
- If the amount parameter is set to 0, all available interest for that wallet and period will be claimed
- If the amount parameter is greater than 0, only that amount will be claimed (up to the available claimable amount)
- This function works even for paused payment periods, providing administrative override capability
- The
ForceClaimed
event is emitted with details of the transfer admin, recipient wallet, amount, and period index
Force Claim Across All Periods (forceClaim)
The forceClaim(wallet, amount)
function allows the Transfer Admin to force claim interest for a specific wallet across all eligible payment periods:
- Similar to
forceClaimForPeriod
, if amount is 0, all available interest is claimed - If amount is greater than 0, the function will claim up to that amount, potentially across multiple periods
- The function processes periods sequentially, and will stop once the specified amount is reached
- This function also works for paused payment periods
- The
ForceClaimed
event is emitted for each period from which interest is claimed
Admin Functions
The Interest Payment contract has several admin functions for managing interest payments:
createPaymentPeriod(startTimestamp, endTimestamp, totalAccruedInterest)
: Creates a new payment period.updateTotalAccruedInterestForPeriod(periodIdx, amount)
: Updates total accrued interest for the period.fundInterest(amount)
: Funds the contract with interest payment tokens.claimInterest()
: Allows token holders to claim all available interest across all payment periods based on their token ownership.claimInterestForPeriod(periodIdx)
: Enables token holders to claim interest for a specific payment period based on their token ownership during that period.batchClaimInterestForPeriods(periodIdxs[])
: Allows token holders to claim interest for multiple payment periods in a single transaction, improving gas efficiency.reclaimInterest(wallet, paymentPeriodIdx)
: Reclaims unclaimed interest from a wallet for a specific period.reclaimInterestForAllRecipients(paymentPeriodIdx)
: Reclaims all unclaimed interest for a period.reclaimTotalInterest(amount)
: Reclaims a specific amount of unused interest.pausePaymentPeriod(periodIdx)
andunpausePaymentPeriod(periodIdx)
: Pauses or unpauses claiming for a specific period.pausePaymentAfter(timestamp)
: Pauses claiming after the given timestamp. To unpause, set the timestamp to 0.pause(isPaused)
: Pauses or unpauses the entire contract.shiftInterestAccrualEnd(timestamp)
: Updates the end timestamp for interest accrual for all payment periods. This allows the Transfer Admin to extend the maturity day.setReclaimerAddress(reclaimerAddress)
: Sets the address that is authorized to receive reclaimed interest. This address must be set before any reclaim operations can be performed.fundPrincipal(amount)
: Transfers principal payment tokens to the contract for distribution to token holders at maturity.claimPrincipal()
: Allows token holders to claim their principal amount at maturity based on their token holdings.reclaimPrincipal()
: Enables the Transfer Admin to reclaim unclaimed principal amounts from the contract.earlyRepayment()
: Allows the Contract Admin or Transfer Admin to trigger early repayment of the loan.forceClaim(wallet, amount)
: Allows transfer admin to force claim interest for a specific wallet across all payment periods, with optional amount capforceClaimForPeriod(wallet, periodIdx, amount)
: Enables transfer admin to force claim interest for a specific wallet and payment period, with optional amount cap
Interest Accrual and Calculation
The contract provides several view functions to check accrued interest:
totalAccruedInterest()
: Returns the total accrued interest at the current time.totalAccruedInterestAt(timestamp)
: Returns the total accrued interest at a specific timestamp.totalAccruedInterestForPeriod(periodIdx)
: Returns the accrued interest for a specific period.accruedInterest(account)
: Returns the accrued interest for a specific account.accruedInterestAt(account, timestamp)
: Returns the accrued interest for a specific account at a specific timestamp.paymentPeriodsCount()
: Returns the total number of payment periods that have been created.periodTotalInterest(periodIdx)
: Returns the total interest amount that was accrued during a specific payment period.periodTotalClaimedInterest(periodIdx)
: Returns the total amount of interest that has been claimed by token holders for a specific payment period.periodTotalReclaimedInterest(periodIdx)
: Returns the total amount of interest that has been reclaimed by the admin for a specific payment period.periodAvailableInterest(periodIdx)
: Returns the amount of interest that is still available to be claimed for a specific payment period.periodDuration(periodIdx)
: Returns the duration (in seconds) of a specific payment period.periodStartTimestamp(periodIdx)
: Returns the start timestamp of a specific payment period.periodEndTimestamp(periodIdx)
: Returns the end timestamp of a specific payment period.unclaimedAmountAt(receiver, timestamp)
: Returns the amount of unclaimed interest for a specific receiver at a given timestamp.unclaimedAmountForPeriod(receiver, periodIdx)
: Returns the amount of unclaimed interest for a specific receiver in a specific payment period.claimedAmountForPeriod(receiver, periodIdx)
: Returns the amount of interest that has been claimed by a specific receiver in a specific payment period.reclaimedAmountForPeriod(receiver, periodIdx)
: Returns the amount of interest that has been reclaimed from a specific receiver in a specific payment period.usedAmountForPeriod(receiver, periodIdx)
: Returns the total amount of interest that has been either claimed or reclaimed for a specific receiver in a specific payment period.accountTotalClaimedAmount(receiver)
: Returns the total amount of interest that has been claimed by a specific receiver across all payment periods.accountTotalReclaimedAmount(receiver)
: Returns the total amount of interest that has been reclaimed from a specific receiver across all payment periods.fundedPrincipalAmount()
: Returns the total amount of principal that has been funded to the contract.reclaimPrincipalAmount()
: Returns the total amount of principal that has been reclaimed by the admin.claimedPrincipalAmount()
: Returns the total amount of principal that has been claimed by token holders.totalAvailablePrincipalAmount()
: Returns the total amount of principal that is still available to be claimed.availablePrincipalAmount(account)
: Returns the amount of principal that is available to be claimed by a specific account.
Interest is accrued linearly over the payment period and is calculated based on the token holder's ownership during that period.
Error Handling
Administrative Errors
InterestPayment_InvalidRestrictedLockupTokenAddress
: Thrown when attempting to deploy with an invalid token addressInterestPayment_InvalidTransferAdminAddress
: Thrown when an invalid transfer admin address is providedInterestPayment_InvalidPaymentPeriodSeconds
: Thrown when payment period is set to zeroInterestPayment_InvalidReclaimerAddress
: Thrown when attempting to set a zero address as reclaimerInterestPayment_InvalidTimestamp
: Thrown when providing an invalid timestamp for payment pausingInterestPayment_InvalidPaymentToken
: Thrown when payment token address is invalid
Configuration Errors
InterestPayment_InvalidInterestAccrualStartTimestamp
: Thrown when start timestamp is set to zeroInterestPayment_InvalidInterestAccrualEndTimestamp
: Thrown when end timestamp is set to zeroInterestPayment_InvalidInterestAccrualPeriod
: Thrown when start timestamp is greater than or equal to end timestampInterestPayment_CannotUnpauseAfterMaturity
: Thrown when attempting to unpause payments after maturityInterestPayment_PaymentNotPausedAfter
: Thrown when trying to unpause when not paused
Period Management Errors
InterestPayment_InvalidPeriod
: Thrown when referencing a non-existent periodInterestPayment_StartTimestampGreaterThanEndTimestamp
: Thrown when creating a period with start after endInterestPayment_StartTimestampBeforeAccrualStartTimestamp
: Thrown when period starts before overall interest accrual startInterestPayment_EndTimestampGreaterThanAccrualEndTimestamp
: Thrown when period ends after overall interest accrual endInterestPayment_NoPaymentPeriods
: Thrown when attempting to claim interest when no payment periods have been createdInterestPayment_PeriodDurationNotMultipleOfInterestRatePeriod
: Thrown when a payment period's duration does not align with the configured interest rate period. For example, if the interest rate period is set to 1 day (86400 seconds), then all payment periods must start and end at the same time of day. If interest accrual begins at2025-01-01 12:00:00
, then valid period boundaries would be2025-01-01 12:00:00
to2025-01-02 12:00:00
,2025-01-02 12:00:00
to2025-01-03 12:00:00
, etc. This ensures consistent interest accrual calculations across all periods.InterestPayment_PeriodAlreadyExists
: Thrown when attempting to create a duplicate periodInterestPayment_PeriodNotNext
: Thrown when attempting to create a payment period that does not immediately follow the previous period. For example, if the last payment period ends at2025-02-14 12:00:00
, the next period must start at exactly that timestamp. Creating a period starting at2025-02-24 12:00:00
would fail because it creates a gap in the payment schedule. This ensures continuous, sequential payment periods without any gaps in the interest accrual timeline.InterestPayment_InvalidTotalAccruedInterest
: Thrown when setting accrued interest below already claimed/reclaimed amountInterestPayment_PaymentPeriodPaused
: Thrown when attempting to claim from a paused period
Funding and Claiming Errors
InterestPayment_InvalidAmount
: Thrown when attempting to fund with zero amountInterestPayment_TokenSupplyIsZero
: Thrown when token supply is zero during principal fundingInterestPayment_PrincipalAmountNotDivisibleByTokenSupply
: Thrown when principal amount isn't divisible by token supplyInterestPayment_InvalidToken
: Thrown when using an invalid token for paymentInterestPayment_InvalidFeeApplied
: Thrown when token transfer results in unexpected balanceInterestPayment_NoFundsToClaim
: Thrown when attempting to claim with no available fundsInterestPayment_NotEnoughFundsToClaim
: Thrown when attempting to force claim more than availableInterestPayment_InvalidUnclaimedAmount
: Thrown when unclaimed calculation produces invalid resultInterestPayment_NotEnoughFundedPrincipal
: Thrown when attempting to claim more principal than availableInterestPayment_MaturityNotReached
: Thrown when attempting to claim principal before maturity
These errors help ensure the contract operates correctly and provide clear feedback when operations cannot be completed. They enhance security by preventing invalid state changes and offer better debugging information than generic reverts.
Access Control
To decreasing the contract size we provide an optimized version of Access Control. This is similar to the linux file permission bitmask exposed in the chmod
command line function.
Our implementation uses binary bitmask determine the access control roles for an address. This optimizes the gas cost and the size of the smart contract code itself.
This is described in EasyAccessControl.sol
.
How it works
We use a uint8
binary representation of a number, such as 01010101
to represent the roles IDs within the access controls.
Roles are defined by a specific bit position in the bit storage representation.
As example decimal 1 is 00000001
in binary mode, 2 is 00000010
, 3 is 00000011
, 4 is 00000100
, 7 is 00000111
etc.
We describe the roles in use as:
uint8 constant CONTRACT_ADMIN_ROLE = 1; // 0001
uint8 constant RESERVE_ADMIN_ROLE = 2; // 0010
uint8 constant WALLETS_ADMIN_ROLE = 4; // 0100
uint8 constant TRANSFER_ADMIN_ROLE = 8; // 1000
If you want to use the unused bits in the future to add new roles you can add something like
uint8 constant NEW_ROLE = 16; // 000010000
uint8 constant SECOND_ROLE = 32; // 000100000
...etc
You can grant multiple roles by adding the role number values together to get the correct bitmask representation like this:
uint8 constant WALLET_AND_TRANSFER_ADMIN_ROLE = 12; // 0001100
or
uint8 constant WALLET_AND_TRANSFER_ADMIN_ROLE = WALLETS_ADMIN_ROLE | TRANSFER_ADMIN_ROLE; // 0001100
For manipulating binary numbers you can use binary operators *&, | and ^.
Example:
uint8 constant CONTRACT_ADMIN_ROLE = 1; // 0001
uint8 constant RESERVE_ADMIN_ROLE = 2; // 0010
uint8 constant WALLETS_ADMIN_ROLE = 4; // 0100
uint8 constant TRANSFER_ADMIN_ROLE = 8; // 1000
WALLETS_ADMIN_ROLE | TRANSFER_ADMIN_ROLE == 1100 // adding of bits, granting multiple roles
AnyNumber & WALLETS_ADMIN_ROLE > 0 // checking if AnyNumber contains 0100 bit, it can be used for checking the role
The contract implements simple methods to manipulate Access Controls and check the roles. Note that granting new and revoking existing roles must be done in separate transactions.
Batched functions also allow for multiple roles to be granted or revoked in a single transaction.
function batchGrantRoles(address[] calldata addresses, uint8[] calldata roles) public onlyContractAdmin
function grantRole(address addr, uint8 role) public validRole(role) validAddress(addr) onlyContractAdmin
function batchRevokeRoles(address[] calldata addresses, uint8[] calldata roles) public onlyContractAdmin
function revokeRole(address addr, uint8 role) public validRole(role) validAddress(addr) onlyContractAdmin
function hasRole(address addr, uint8 role) public view validRole(role) validAddress(addr) returns (bool)
Appendix
Roles Matrix
Admin permissions are defined with the following binary values. Roles are defined as the OR
of a set of admin roles:
Contract Admin: 0001
Reserve Admin: 0010
Wallets Admin: 0100
Transfer Admin: 1000
Role Integer | Admin Roles | Bit Mask Representation |
---|---|---|
0 | None (Default) | 0000 |
1 | Contract Admin | 0001 |
2 | Reserve Admin | 0010 |
3 | Contract Admin + Reserve Admin | 0011 |
4 | Wallets Admin | 0100 |
5 | Contract Admin + Wallets Admin | 0101 |
6 | Reserve Admin + Wallets Admin | 0110 |
7 | Contract Admin + Reserve Admin + Wallets Admin | 0111 |
8 | Transfer Admin | 1000 |
9 | Contract Admin + Transfer Admin | 1001 |
10 | Reserve Admin + Transfer Admin | 1010 |
11 | Contract Admin + Reserve Admin + Transfer Admin | 1011 |
12 | Wallets Admin + Transfer Admin | 1100 |
13 | Contract Admin + Wallets Admin + Transfer Admin | 1101 |
14 | Reserve Admin + Wallets Admin + Transfer Admin | 1110 |
15 | All Roles (Contract, Reserve, Wallets, Transfer) | 1111 |
Post-Deployment Configuration
After smart contract deployment, there are certain configurations to complete before token issuance and trading can occur smoothly.
-
Grant Transfer Admin Role
Although Transfer Admin is a required deployment parameter for the InterestPayment contract, it still must be specifically set in the Token contract separately, by the Contract Admin.
This is important so that an admin exists which can configure group transfers.
-
Allow Group Transfers
The Transfer Admin can then allow group transfers, for example between all traders within Group 1. This will allow transfers to occur between all traders placed in Group 1.
This is important so that transfers of tokens are enabled between designated groups.
-
Whitelist Funder [optional]
The Transfer Admin can then set the group of the admin funder address, so that
fundReleaseSchedule
can be invoked. Note that Reserve Admin canmintReleaseSchedule
to any recipient, regardless of group transfer restrictions. However, funding release schedules viafundReleaseSchedule
, for tokens that have already been minted, must adhere to transfer restrictions between the funding admin and timelock recipient.
Meta Transactions
Meta Transactions are enabled for RestrictedLockupToken.sol
. Transactions can be signed off-chain, and then a third party (the relayer) can pay the transaction fees on behalf of the user. This greatly simplifies the user experience, and also allows batching of multiple transactions natively via the forwarder contract. In particular, this simplifies the wallet whitelisting process by allowing the relayer to process multiple transactions on behalf of the Transfer Admin.
A custom ERC2771 forwarder contract has been created to allow for unique, but non-sequential nonce management. The forwarder is based on OpenZeppelin's ERC2771Forwarder.sol contract, except with custom nonce management. This ensures replay protection while also allowing multiple nonces to be utilized within the same batch of transactions without the need to order them.
See contracts/metatx/ERC2771CustomForwarder.sol
and contracts/utils/Nonces.sol
for more details.
Here is an example flow:
-
The EIP-712 format is used for off-chain signing. The domain specifies the context of the signing, including the contract name, version, chain ID, and verifying contract address (which is the address of the ERC2771 Forwarder). The typed structured data consists of the fields:
from
(tx signer),to
(target token contract),value
(amount of coin provided),gas
,nonce
(unique but not sequential),deadline
(timestamp deadline for expiration), anddata
. -
The Relayer (any third party payer wallet) must then be provided that signed transaction.
-
The Relayer then executes the meta transaction on behalf of the Transfer Admin signer by calling
execute
orexecuteBatch
on theERC2771Forwarder
contract with the signed transaction. -
The forwarder contract will forward the transaction to the target RestrictedLockupToken contract to execute the whitelisting transaction.
Snapshots Periods
The SnapshotsPeriods contract (SnapshotsPeriods.sol
) provides a robust mechanism for tracking token balances over time periods, enabling functionality like historical balance lookups and time-weighted ownership calculations. This is particularly valuable for applications that require historical data verification such as voting rights, interest distributions, and other time-sensitive token operations.
Key Features
- Historical balance tracking for any RestrictedLockup-compatible token
- Time-weighted ownership calculation across custom periods
- Past balance lookup for both individual accounts and total supply
- Automatic period management for token transfers, mints, and burns
How It Works
SnapshotsPeriods works by maintaining a historical record of token balances and calculating ownership based on time periods:
- When token transfers, mints, or burns occur, the
RestrictedLockupToken
contract calls theonUpdate
function in SnapshotsPeriods. - SnapshotsPeriods creates and maintains periods that record the token balances and timestamps for both individual accounts and the total supply.
- For time-weighted operations, the contract calculates ownership as the product of token amount and time period length.
Time-Weighted Ownership
A key feature of SnapshotsPeriods is the ability to calculate time-weighted ownership, which is essential for applications like interest distribution based on token holding duration:
The time-weighted ownership is calculated by:
- Tracking when token balances change for each account
- Creating period entries that record the balance, start time, and end time
- Calculating the ownership as
amount * (endTime - startTime)
- Summing these values to determine the total ownership over specific time ranges
Primary Functions
The SnapshotsPeriods contract provides these primary functions:
getPastBalanceOf(token, account, timestamp)
: Retrieves the balance an account had at a specific past momentgetPastTotalSupply(token, timestamp)
: Retrieves the total token supply at a specific past momentownershipForPeriod(token, account, startTimestamp, endTimestamp)
: Calculates time-weighted ownership for an account over a given periodtotalOwnershipForPeriod(token, startTimestamp, endTimestamp)
: Calculates the total time-weighted ownership over a given periodcalculateOwnershipForPeriod(amount, periodStart, periodEnd)
: Helper function to calculate ownership weight asamount * (periodEnd - periodStart)
Integration with Interest Payment
SnapshotsPeriods is a critical component for the Interest Payment system, as it provides the historical data needed to calculate interest distributions based on token ownership over specific time periods:
- Interest Payment periods are created with specific start and end timestamps
- When tokens are claimed, the SnapshotsPeriods contract calculates each account's proportional ownership during that period
- Interest is distributed according to the proportion of time-weighted ownership
This mechanism ensures fair distribution of interest based on both the amount of tokens held and the duration they were held during each interest accrual period.