Permissioned tokens have a mechanism deciding who can use and hold tokens based on additional requirements. Securities may use the permissioned token mechanism to define complex compliance requirements, such as investor whitelisting and transfer restrictions. For example, only a verified, KYCed users can exchange tokens.
Specifically, for Permissioned Tokens, we want to:
- Control the number of tokens minted for distribution and also allow flexibility for the administrator to issue more tokens in the future.
- Create whitelists of accounts who are approved to buy/hold the tokens.
- Support transfer restrictions (e.g., U.S. vs international).
- Manage lock-ups as needed to ensure there are no transfers before they're allowed.
- Process requirements or needs that come up post-issuance, such as adding or removing investors to the whitelists or creating additional whitelists for new restrictions.
- Take confidence in the ability to remove restrictions in the future if transfers become open or unlimited. The permissioned token Template outlines a design of permissioned tokens with emphasis for shares and equity.
We want to enforce permissioned checks on all token transfer between non-reserve accounts. We will use a stateless TEAL as a clawback, which will execute all transfers. So if user A wants to transfer to user B, (s)he will call a clawback stateless smart contract to transfer from account A to account B. We implement a Permissioned Token template, which executes the following steps:
- Create ASA
- Distribute to initial users
- Move remaining assets to
reserve
account to properly signal future asset issuance. - Freeze all assets OR create the asset default-frozen (clawback can still move frozen tokens).
- Create following stateful smart contracts:
- permissions (
P
) with rules and permission checks - controller (
C
) - which stores theP
address.
- permissions (
- Create a stateless teal (
clawback
) which will check that the C and P is called as well. - Update the clawback address to the contract
clawback
. - Whenever user A want's to transfer some tokens to B, he will need to combine this transaction with a clawback asset transfer transaction.
- Clawback transfer ensures a call to the controller smart contract (
C
). Controller has the application id of permissions/rules smart contract (P
), and hence ensuresP
is called as well.
Smart contracts (in /assets
):
- clawback.py: a stateless smart contract which will be responsible to dispatch permission checks and execute the payments. It's set to be the asset clawback address. Clawback logic ensures that during a token transfer between accounts,
controller
smart contract is called.reserve
account is always allowed to transfer tokens to anyone (bypassing all permission checks). It also handles issuance transaction & update reserve account transaction. - controller.py: This stateful smart contract acts as a controller of the token. It ensures that during a token transfer, smart contract
P
(with rules and permissions) is called. If there are multiple rules contracts, then the controller must ensure that all those contracts are called (it must store the app ids). This smart contract can also be used to kill the token. - permissions.py: This is the rules stateful smart contract. In this template we have two rules:
- An account cannot hold more than 100 tokens
- Both
from
andto
accounts must be whitelisted
- clear_state_program.py: clears app state (returns 1)
assets/asa.yaml
- This the the asset definition file.
Deployment scripts are placed directly in /scripts
:
NOTE: In a real world scenario, transactions involving a multisig account (eg. token can be created and issued by a multisig) will involve an interaction of many users. A user will receive a signed transaction or can create an unsigned transaction. User can sign the transaction using algob sign-multisig
command or by using the signMultiSig
function in a script. Once transaction is signed, we can use executeSignedTxFromFile
function to successfully send transaction to network (eg. deploy token).
- 0-setup-token.js: In this script we deploy the token (as an Algorand Standard Asset) using
deployer.deployASA(..)
function. Check note above if creator is a multisig address. - 1-setup-controller.js: In this script we deploy
controller.py
smart contract (which saves the hardcoded token_id as a template paramteter & sets kill_status to false by default). Only token manager can deploy this contract. - 2-asset-clawback.js: Here we deploy the contract
clawback.py
and add some funds to it. In contract we save token_id, controller_app_id. After that we update the asset clawback to the address of the contract account (using algob ModifyAsset transaction) - 3-setup-permissions.js: Here, we deploy the
permissions.py
smart contract, which has the rules. During permisssions deployment, we save the perm_manager in the global state (while deploying it's passed as a template parameter, but it can be updated by the current permissions manager to another address as well)
-
Asset transfer (
scripts/user/transfer.js
): A non-reserve account can hold few tokens (via issuance) and may wish to transfer some tokens to another non-reserve account. This should be in compliance with the permission smart contractP
(permissions.py
). For token transfer we need a group of at least 4 transactions (and more if there are more than 1 permissions contract): a) call to controller smart contract (this ensures all rules are checked) - signed by asset_sender. b) token transfer transaction from sender -> receiver using clawback (clawback contract ensures controller smart contract is called). c) payment transaction from sender -> clawback (to cover tx fee of the transaction above) d) call to permissions smart contract (to check rules: max token holding by receiver < 100, both from & to are whitelisted) -
Issuance (
/scripts/admin/issue.js
): Issuer can issue/mint tokens. Receiver will need to opt-in to the token before the issuance. For issuance tx, we need a transaction group of: a) Call to the controller smart contract - it checks if token is not killed (as issuance is not allowed if token is killed). It also verifies that the sender is the token reserve. b) Asset transfer transaction from issuer -> receiver using clawback.clawback.py
does the basic sanity checks for issuance_tx. We don't need additional rule checks as issuer sets the rules himself. -
Forced Asset transfer (
scripts/admin/force-transfer.js
): This is similar to asset transfer, but in this case the assets are being moved by asset manager, rather than a token holder. The transaction group is also similar to the one above. Difference is that a call toC
,P
and paying fees of escrow is done by asset manager. Asset manager essentially represents clawback here (revoking tokens from an account). -
Update Asset Reserve (
/scripts/admin/update-reserve.js
): If the reserve account is a multisig address and we want to kick out one address from the multisig, then we will need to update the asset reserve address. One approach is we update the asset reserve using ModifyAsset transaction, and move all tokens from previous reserve -> new reserve. This can be done in an atomic transaction group: a) Force transfer all assets from previous reserve to new reserve - signed by asset manager (requires 3 txns, check Forced Asset Transfer above for more details) b) Asset Modification transaction where we update the asset reserve address to new one. (requires 1 tx) Another approach is to rekey ASA's Reserve account to the newReserve. This way you won't need to move all tokens from old reserve to new reserve. Now for transactions related to the asset reserve (eg. issuance),fromAccountAddr
will be the old reserve address, but signing authority will be new reserve account. -
OptOut (
/scripts/admin/opt-out.js
): User can optOut from the token if he wants to. This could be a simple asset transfer transaction from user -> creator (using user's sk) where asset_amount = 0 & close_remainder_to is asset creator. This tx will opt out a user from the token and transfer all his tokens to the asset creator. NOTE: If asset creator and reserve are different address, then we will need to transfer all tokens from creator to reserve as well (to take those tokens out of circulation/increase total supply). -
Kill Token (
scripts/admin/kill.js
): Token manager can kill the token. If the token is killed then all issuance and token transfer transactions are rejected. User can only opt-out from the token to remove his holding and decrease his account's minimum balance. a) To kill the token, execute an application call tx to the controller smart contract with appArg: 'kill'. Only ASA mangager can do that. Controller updates the global state key (is_token_killed) to true. -
Whitelist User (
scripts/permissions/whitelist.js
): Only a permissions manager (whose address is stored in the permissions smart contract's global state) can add a new user to the whitelist which enables that user to receive or send tokens. For executing this tx, permissions manager calls the permissions smart contract with application arg "add_whitelist", and passes the account he wishes to whitelist in AppAccount array. If tx is successful, then permission smart contract updates the local state ofTxn.accounts[1]
& set whitelisted = true (Int(1)). -
Change Permissions Manager (
scripts/permissions/change-manager.js
): Token manager can change the permissions manager via an application call to the controller smart contract. If the parameters are correct, tx is accepted and controller smart contract updates thepermission_manager
in it's global state.
The solution in this template extends Algorand Dev Office Hour idea presented by Jason - using asset clawback as escrow for permissioned token.
In this template we have a new contract - controller.py
which essentially "controls" the asset/token. This controls the general properties of the asset and also ensures that rule(s) smart contract(s) are called.
Developer hour links:
- Developer Office Hours | Assets and Custom Transfer Logic Using Algorand Smart Contracts: video
- Assets and Custom Transfer Logic
- implementation of the solution above using algob: examples/restricted-assets
Link to spec of current template: https://paper.dropbox.com/doc/Algob-Security-Token-Template-FR2LXhVg3edevYPBQZw6F
Below we describe different use cases implemented by the smart contract suite. We use a function notation for a use-case, along with description of transaction group which has to be created. We implemented that use cases using functions in scripts
directory. For direct integration in smart contracts, you need to construct transactions as described below.
- totalSupply(assetIndex): returns total supply of the asset (asset.total - asaReserveHolding.amount). To retreive the asset holding of an account, use algob balanceOf function in
algob
.
-
issue(address, amount): Group of 2 transactions
- tx1: Call to controller smart contract with a) application arg
str:issue
b) foreign Asset:assetIndex
- signed by asset reserve (issuer). - tx2: Asset clawback transaction from asset reserve to address =
address
, amount =amount
.
- tx1: Call to controller smart contract with a) application arg
-
kill(): Application call to controller with a) appArg
str:kill
and b) foreignAssetassetIndex
- signed by asset manager. -
forceTransfer (fromAddress, toAddress, amount): Group of 4 transactions
- tx1: Call to controller smart contract with a) application arg
str:force_transfer
b) foreign Asset:assetIndex
- signed by asset manager. - tx2: Asset clawback transaction from
fromAddress
totoAddress
, amount =amount
. - tx3: Payment transaction to clawback (contract account) with amount >= fee of tx2
- tx4: Call to permissions smart contract with a) application arg
str:transfer
b) app accounts: [fromAddress
,toAddress
].
NOTE: tx3, tx4 can be signed by anyone but they must be present in group (they validate conditions). The signer will pay transaction fees. If receiver of
forceTransfer
is the current asset reserve then a permissions smart contract call is not required. - tx1: Call to controller smart contract with a) application arg
-
updateReserveByAssetConfig (newReserveAddress): Group of 4 transactions
- tx1: Call to controller smart contract with a) application arg
str:force_transfer
b) foreign Asset:assetIndex
- signed by asset manager. - tx2: Asset clawback transaction from
oldReserveAddress
tonewReserveAddress
, amount =oldReserveHolding.amount
(moving all tokens to new reserve address). - tx3: Payment transaction to clawback (contract account) with amount >= fee of tx2
- tx4: Asset Config transaction updating reserve address to
newReserveAddress
- signed by asset manager.
NOTE: If tx4 is asset config transaction (updating reserve address to newReserveAddress) in
forceTransfer
group, then a permissions smart contract call is not required (as we're moving all tokens in tx2 from old reserve to new one). - tx1: Call to controller smart contract with a) application arg
-
updateReserveByRekeying (newReserveAddress): Transaction rekeying oldReserve account to
newReserveAddress
- signed by old reserve account.
-
transfer (fromAccount, toAddress, amount): Group of 4 transactions
- tx1: Call to controller smart contract with application arg
str:transfer
- signed by fromAccount. - tx2: Asset clawback transaction from
fromAccount.address
totoAddress
, amount =amount
. - tx3: Payment transaction to clawback (contract account) with amount >= fee of tx2
- tx4: Call to permissions smart contract with a) application arg
str:transfer
b) app accounts: [fromAccount.address
,toAddress
].
- tx1: Call to controller smart contract with application arg
-
optOut (account): Asset transfer transaction from
account.address
toaccount.address
, amount = 0, closeRemainderTo = asset.creator. NOTE: User opts out from the permissioned token. Algorand will transfer all his tokens to the ASA creator not the ASA reserve account (by definition of Algorand opt-out transaction).
-
whitelist (permissionsManager, address): NoOp call to the permissions smart contract with a) application arg
str:add_whitelist
b) app accounts: [address
] - must signed by permissionsManager. -
changePermissionsManager (permissionsManager, newManagerAddress): NoOp call to the permissions smart contract with a) application arg
str:change_permissions_manager
b) app accounts: [newManagerAddress
] - signed by permissionsManager (current permissions manager).