Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Solution Thumbnail

Securities and Permissioned Tokens

Overview

This article presents a design of permissioned / securities tokens on Algorand. We will implement company shares using ASA and smart contracts with Algo Builder.

Info

Algo Builder is a framework and contains a set of templates to quickly develop dApps on the Algorand blockchain. Learn more by viewing the Algo Builder Tutorials series.

Requirements

  • Good knowledge about Blockchain and Algorand.
  • Detailed introduction to algob
  • Detailed knowledge about assets, accounts, transactions and signatures.
  • Detailed knowledge about statless and stateful algorand smart contracts.

Securities and Compliance

One of the features which is highly appreciated, but not well understood on Algorand is the design of permissioned tokens. The term “security” refers to a fungible, negotiable financial instrument that holds some type of monetary value. It represents an ownership position in a publicly-traded corporation via stock, a creditor relationship with a governmental body or a corporation represented by owning that entity’s bond, or rights to ownership as represented by an option (many tokens fall in the last definition) source. Securities are designed specifically to give owners all kinds of options - buy, sell, hold, take cash dividends or give holders ownership rights and debt rights. Securities are often used to raise capital in public and private markets.

There are primarily three types of securities:

  • equity — which provides ownership rights to holders
  • debt — essentially loans repaid with periodic payments
  • hybrids — which combine aspects of debt and equity

Public sales and securities trading are heavily regulated by a relevant organization in each jurisdiction, eg SEC (in USA), FINAM (Switzerland).

A compliance program is a set of internal policies and procedures of a company to comply with laws, rules, and regulations or to uphold business reputation and stay in accordance with the law.

Security tokens are associating regulated financial instruments with blockchain ‘tokens’.

Permissioned token

Permissioned tokens are a special kind of token with a built in control mechanism. Usually, the mechanism is limiting the token usage based on additional requirements defined by the business strategy and regulations.

Security tokens can be implemented using permissioned token mechanism to define complex compliance program requirements, such as investor whitelisting and transfer restrictions. For example, only a verified, KYC’d users can exchange tokens.

Use Cases

We can implement various mechanisms in permissioned tokens:

  • 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 and 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
  • implement new permission requirements 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;
  • asses investor risk profile using external oracle data.
  • in general: implement compliance program rules

Solution

Design

In the rest of the article we will present a Permissioned token template and how it can be used with securities. More specifically we will implement a token representing a TESLA-Algo-INC company shares. The implementation of the following design is available in the Algo Builder Examples.

We define the following blockchain account types:

  • owner — a legal entity being in charge of shares creation (usually an account representing a company). Owner appoints an issuer to be in charge of shares issuance
  • issuer — a legal entity that develops, registers and sells securities to finance its operations. In Algorand, it is represented as an asset reserve account
  • investor — a legal entity (natural person or juridical person) holding or willing to buy shares
  • clawback — an account executing shares transfer

Shares will be represented as ASA. We want to enforce permissioned checks for transfers shares. We do that by freezing the ASA (we create the asset as default-frozen). Only ASA clawback account can transfer frozen asset. We will use logic signature (stateless smart contract) as the clawback account. So if a user A wants to transfer to a user B, it will be an Asset Transfer transaction using clawback smart contract. The transaction will look as follows: sender = clawback logic sig, asset_sender = asset_owner, asset_receiver = recipient. NOTE: transfers to or from the issuer account also have to use clawback and are always accepted (the clawback logic sig always accepts such transfers).

The clawback logic signature ensures that the controller smart contract is called.

Along with a stateless TEAL as clawback, we will use stateful smart contracts (apps):

  • permission.py — set of permission checks (rules) for a compliance plan. In the presented design, all rules are implemented in one smart contract. As discussed later, we can have multiple permission contracts joined together in the controller contract.
  • controller.py — it will store ID of the permission app and make sure it’s called. Moreover it will define role based access control for updating the configuration.

In the presented example, we will implement 2 conditions in a single permission contract:

  • whitelisted accounts: both fromAddress and toAddress must be whitelisted
  • no account can hold more than 100 tokens (so during the transfer we must check that the recipient won’t have more than 100 tokens)

Permissions contracts can be composed and shared with different apps. We envision that the permissions check will evolve in separate businesses and will offer the permissions checks as a service on blockchain.

In the image below we have two companies selling securities on blockchain:

  • Tesla INC — tokenize and sells the company shares
  • SwissGold INC —tokenizes and sells a gold share

Both will have their own issuer, but will use the same permissions services for KYC and and AML checks.

EditorImages/2021/06/29 14:49/diag.png

Implementation

Project Structure

algob project:
├── assets
   ├── asa.yaml
   ├── clawback.py
   ├── controller.py
   ├── permissions.py
   ├── clear_state_program.py
├── scripts
   ├── 0-setup-token.js
   ├── 1-setup-controller.js
   ├── 2-asset-clawback.js
   ├── 3-setup-permissions.js
|   ├── admin
      ├── issue.js
      ├── force-transfer.js
      ├── kill.js
      ├── update-reserve.js
|   ├── permissions
      ├── change-perm-manager.js
      ├── whitelist.js
|   ├── user
      ├── opt-out.js
      ├── transfer.js
├── test
   ├── JS test files
├── package.json

Token

The token should provide all legal information about the issuer and the underlying security. Token is an Algorand Standard Asset defined in assets/asa.yaml. ASA fields worth to mention:

  • url: It must link to the document specifying all legally required information as well as metadata (information about tokens, distribution description, code repository ..etc). The document should be immutable. The document can be encrypted. We recommend storing the document in IPFS.
  • metadataHash: is a blake2b hash of the document linked by url. If the document is encrypted, the hash should be computed from the unencrypted version.
  • total: amount must envision all future needs and legally specified. In Algorand, all tokens must be created at the beginning and they will be stored the a reserve address. The process of distributing new tokens is done by creating a new supply from the tokens stored in the reserve address.
  • reserve: An account (key or lsig) which keeps all possible token supply. We recommend to use multisig key or lsig (logic signature).

Smart contracts

We implement the following smart contracts:

  • permissions.py: Stateful smart contract with rules described above (whitelisting and max 100 token limit).
  • controller.py: Smart contract linking the permissions apps.
  • clawback.py: Stateless smart contract used as the clawback logic sig.
  • clear_state_program.py: clears app state (returns 1)

EditorImages/2021/06/29 14:50/security_token_diag.png

Setup

  1. Owner creates assets (deploys token) with sets freeze attribute to true.
  2. Set asset freeze address to Zero Account - this will prevent anyone to unfreeze assets.
  3. Opt in initial users (including issuer account)
  4. Send all supply form owner to issuer.
  5. Distribute to initial users using the issuer account.
  6. Create a controller stateful smart contract (controller.py). During deployment, no permissions contract is added to the controller.
  7. Create clawback logic signature (clawback.py) which will be used as the asset clawback account. It ensures that controller.py is called.
  8. Update the asset clawback address to the address of clawback.py contract.
  9. Create a stateful contract permissions.py (which implements the compliance plan rules).
  10. Set permissions contract application id in the controller (done by a NoOp call to controller.py by controller_manager)

All operations above are implement in the deployment scripts (stored in /scripts):

  • Deploy token(ASA)
  • Deploy controller smart contract
  • Setup clawback
  • Deploy permissions smart contract
  • Set permissions application id in controller

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 between many users. A user will receive a signed transaction or create a new one. 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).

Below we will describe in details each deployment procedure:

In the examples below, owner and issuer are both represented as the same account: alice.

Deploy token(ASA)

We will use deployer object available in algob scripts:

const owner = deployer.accountsByName.get('alice');
// deploy ASA by owner account
return await deployer.deployASA(tesla', { creator: owner });

Deploy controller smart contract

In this section we deploy controller.py smart contract. Below we outline the controller smart-contract initialization section. The token_id (asset ID) is passed as a template parameter, moreover we will set an app parameter kill_status = false. The kill status is used to indicate if the token is active. Killed token can’t be used. Only token manager (ASA.manager field) can deploy this contract.

# retreive asset manager from Txn.ForeignAssets[0]
assetManager = AssetParam.manager(Int(0))
on_deployment = Seq([
    assetManager, # load asset manager from store
    Assert(And(
        Txn.assets.length() == Int(1),
        Txn.assets[0] == Int(TOKEN_ID),

        # Controller should be deployed by ASA.manager
        Txn.sender() == assetManager.value()
    )),
    # set kill_status to false(0)
    App.globalPut(var_is_killed, Int(0)),
    Return(Int(1))
])

algob deployment script:

const tesla = deployer.asa.get('tesla'); // load asset info from checkpoint

const templateParam = {
  TOKEN_ID: tesla.assetIndex
};

await deployer.deploySSC(
'controller.py', // approval program
'clear_state_program.py', // clear program
{
  sender: owner,
  localInts: 0,
  localBytes: 0,
  globalInts: 2, // 1 to store kill_status, 1 for storing permissions_app_id
  globalBytes: 0,
  foreignAssets: [tesla.assetIndex] // pass token_id in foreign assets array
}, {}, templateParam); // pass token_id as a template paramenter

Setup clawback

In this section we setup the clawback.py logic signature. In the lsig TEAL code we set token_id and controller_app_id using algob external parameters . After that we update the ASA clawback address to the generated lsig address (using algob ModifyAsset transaction)

// load asset, controller ssc info from checkpoint
const tesla = deployer.asa.get('tesla');
const controllerInfo = deployer.getSSC('controller.py', 'clear_state_program.py');

// Compile and fund clawback account. Logic sig is represent by a TEAL code.
// NOTE: we always need to use same template parameters otherwise the generated
//       lsig will be different.
// We use algob template functionality
// https://algobuilder.dev/guide/py-teal.html#external-parameters-support
const clawbackParams = { // template params
    TOKEN_ID: tesla.assetIndex,
    CONTROLLER_APP_ID: controllerInfo.appID
};

await deployer.fundLsig('clawback.py',
{ funder: owner, fundingMicroAlgo: 5e6 }, {}, clawbackParams); // sending 5 Algo

// Update clawback address to clawback lsig
const clawbackLsig = await deployer.loadLogic('clawback.py', clawbackParams);
const clawbackAddress = clawbackLsig.address();
const assetConfigParams = {
    type: TransactionType.ModifyAsset,
    sign: SignType.SecretKey,
    fromAccount: owner,
    assetID: tesla.assetIndex,
    fields: { clawback: clawbackAddress },
    payFlags: { totalFee: 1000 }
};
await executeTransaction(deployer, assetConfigParams);

Deploy permissions smart contract

Now we deploy the permissions.py smart contract, which executes the compliance plan. During permissions deployment, we save the perm_manager in the global state (on deployment it’s passed as a template parameter, but it can be updated to another address later on by the current permissions manager).

permissions.py

permissions_manager = Bytes("manager")  # permissions manager (manages contract permissions)
max_tokens = Bytes("max_tokens") # maximum token transfer rule
whitelist_count = Bytes("whitelist_count") # whitelisted accounts counter
# During deployment
# * max_tokens is set to 100
# * whitelist_counter is intialized to 0
# * save permissions manager in global state
on_deployment = Seq([
    Assert(basic_checks), # closeRemTo, rekey ..etc
    App.globalPut(max_tokens, Int(100)),
    App.globalPut(whitelist_count, Int(0)),
    App.globalPut(permissions_manager, Addr(PERM_MANAGER)),
    Return(Int(1))
])

algob script:

const controllerSSCInfo = deployer.getSSC('controller.py', 'clear_state_program.py');
// We use algob template functionality
// https://algobuilder.dev/guide/py-teal.html#external-parameters-support
const templateParam = { PERM_MANAGER: owner.addr };

/** Deploy Permissions(rules) smart contract **/
const permissionSSCInfo = await deployer.deploySSC(
'permissions.py', // approval program
'clear_state_program.py', // clear program
{
  sender: owner,
  localInts: 1, // 1 to store whitelisted status in local state
  localBytes: 0,
  globalInts: 2, // 1 to store max_tokens, 1 for storing total whitelisted accounts
  globalBytes: 1 // to store permissions manager
}, {}, templateParam); // pass perm_manager as a template param (to set during deploy)
console.log(permissionSSCInfo);

Set permissions application id in controller

After deploying the contracts, we set permissions application_id in controller’s global state.

const tesla = deployer.asa.get('tesla');
// retrive the permissions smart contract object from checkpoint
const permissionSSCInfo = deployer.getSSC('permissions.py', 'clear_state_program.py');
const appArgs = [
  'str:set_permission',
  `int:${permissionSSCInfo.appID}`
];

await executeTransaction(deployer, {
  type: TransactionType.CallNoOpSSC,
  sign: SignType.SecretKey,
  fromAccount: owner, // ASA manager account
  appId: controllerSSCInfo.appID,
  payFlags: { totalFee: 1000 },
  appArgs: appArgs,
  foreignAssets: [tesla.assetIndex] // controller sc verifies if correct token is being used + asa.manager is correct one
});

controller.py

assetManager = AssetParam.manager(Int(0))
set_permission_contract = Seq([
    assetManager, # load asset_manager (from Store) of Txn.ForeignAssets[0]
    Assert(And(
        Txn.application_args.length() == Int(2),
        Txn.assets.length() == Int(1),
        Txn.assets[0] == Int(TOKEN_ID),
        Txn.sender() == assetManager.value(),
    )),
    # Add permissions(rules) smart contract app_id in global state
    App.globalPut(permission_id, Btoi(Txn.application_args[1])),
    Return(Int(1))
])

Interaction

  • Issuance
  • Whitelisting
  • Transfer (between non-reserve accounts)
  • Force transfer (asset manager revoking few tokens from accA to accB)

Issuance

Issuer can issue/mint tokens. In this implementation issuer is the reserve account. Recipient will need to opt-in to the token before the issuance. Since all tokens are frozen, a clawback lsig is required to transfer the tokens. Transaction group has the following composition:

  • tx1: Call to controller smart contract with application-arg = str:issue, foreign-asset = assetIndex. Must be signed by asset reserve account (sender = reserve_account).
  • tx2: ASA transfer from reserve to recipient using ASA clawback. clawback.py lsig does the basic sanity checks & ensures controller is called.

We don’t need additional rule checks as issuer sets the rules himself.

algob script:

// load asset, controller info from checkpoint
const tesla = deployer.asa.get('tesla');
const controllerSSCInfo = deployer.getSSC('controller.py', 'clear_state_program.py');

const issuanceParams = [
  /*
   * tx 0 - Call to controller stateful smart contract with application arg: 'issue'
   * The 'issue' branch ensures that sender is the token reserve and token is not killed
   * Issuance tx will be rejected if token has been killed by the manager */
  {
    type: TransactionType.CallNoOpSSC,
    sign: SignType.SecretKey,
    fromAccount: asaReserve,
    appId: controllerSSCInfo.appID,
    payFlags: { totalFee: 1000 },
    appArgs: ['str:issue'],
    foreignAssets: [tesla.assetIndex]
  },
  /*
   * tx 1 - Asset transfer transaction from sender -> receiver. This tx is executed
   * and approved by the clawback lsig (clawback.teal). The clawback lsig address is the
   * address which transfers the frozen asset (amount = amount) from accA to accB.
   * Clawback ensures a call to controller smart contract during token transfer. */
  {
    type: TransactionType.RevokeAsset,
    sign: SignType.LogicSignature,
    fromAccountAddr: clawbackAddress,
    recipient: address,
    assetID: tesla.assetIndex,
    revocationTarget: asaReserve.addr, // tx will fail if assetSender is not token reserve address
    amount: amount,
    lsig: clawbackLsig,
    payFlags: { totalFee: 1000 }
  }
];

await executeTransaction(deployer, issuanceParams);

Whitelisting

Permissions smart contract checks 2 things to approve a transfer: user is whitelisted and user balance is smaller than 100. Note: user needs to opt-in to the permissions smart contract first. Whitelisting an address is done by NoOP tx: call to the permissions smart contract with app-arg = str:address_whitelist and app-accounts = [userAddress]. Must be signed by permissionsManager. If tx is successful, then permission smart contract updates Txn.accounts[1] (the userAddress) local state by setting whitelisted = 1.

algob script:

// owner account was set as the permissions_manager during deploy
const whiteListParams = {
  type: TransactionType.CallNoOpSSC,
  sign: SignType.SecretKey,
  fromAccount: permissionsManager, // permissions manager account (fails otherwise)
  appId: permissionSSCInfo.appID,
  payFlags: { totalFee: 1000 },
  appArgs: ['str:add_whitelist'],
  accounts: [address] // pass address to add to whitelisted addresses
};

await executeTransaction(deployer, whiteListParams);

permissions.py:

add_whitelist = Seq([
    Assert(And(
        Txn.accounts.length() == Int(1),
        basic_checks,

        # verify txn sender is the permissions manager
        Txn.sender() == App.globalGet(permissions_manager),
    )),

    If(
        # increment counter if account is not already whitelisted
        App.localGet(Int(1), Bytes("whitelisted")) == Int(0),
        App.globalPut(whitelist_count, App.globalGet(whitelist_count) + Int(1)),
        Return(Int(1))
    ),

    # update Txn.accounts[1] local state to set whitelisted status as true(1)
    App.localPut(Int(1), Bytes("whitelisted"), true),
    Return(Int(1))
])

Transfer (between non-reserve accounts)

Every transfer between non-reserve accounts has to pass permissions check to assure token compliance. Clawback lsig is used to allow transfers: all tokens are frozen so only clawback can move tokens. The lsig validates the transfer only if the transaction is a part of a group of 4 (or more if there are more than 1 permissions contract) transactions:

  • tx1: Call to controller smart contract with app-arg = str:transfer signed by fromAccount (asset sender).
  • tx2: ASA transfer transaction from sender to receiver using ASA clawback. The clawback contract ensures right contract composition.
    Asset clawback transaction from fromAccount.address to toAddress, amount = amount.
  • tx3: ALGO payment transaction to clawback to cover tx2 fee (tx3.amount >= tx2.fee). Anyone can make a payment, i.e this tx can be signed by anyone.
  • tx4: Call to permissions smart contract to check required permissions, with app-arg = str:transfer and app-accounts = [fromAccount.address, toAddress]. Can be signed by anyone.

algob script:

const tesla = deployer.asa.get('tesla');

const txGroup = [
  /*
   * tx 0 - Call to controller stateful smart contract with application arg: 'transfer'
   * The contract ensures that there is a call to permissions smart contract in the txGroup,
   * so that rules are checked during token transfer.
   */
  {
    type: TransactionType.CallNoOpSSC,
    sign: SignType.SecretKey,
    fromAccount: from,
    appId: controllerSSCInfo.appID,
    payFlags: { totalFee: 1000 },
    appArgs: ['str:transfer']
  },
  /*
   * tx 1 - Asset transfer transaction from sender -> receiver. This tx is executed
   * and approved by the clawback lsig (clawback.py). The lsig address is
   * also the clawback address which transfers the frozen asset from accA to accB.
   * Clawback ensures a call to controller smart contract during token transfer. */
  {
    type: TransactionType.RevokeAsset,
    sign: SignType.LogicSignature,
    fromAccountAddr: clawbackAddress,
    recipient: toAddr,
    assetID: tesla.assetIndex,
    revocationTarget: from.addr,
    amount: amount,
    lsig: clawbackLsig,
    payFlags: { totalFee: 1000 }
  },
  /*
   * tx 2 - Payment transaction of 1000 microAlgo to cover clawback transaction cost (tx 1)
   * NOTE: It can be signed by any account, but it must be present in group. */
  {
    type: TransactionType.TransferAlgo,
    sign: SignType.SecretKey,
    fromAccount: from,
    toAccountAddr: clawbackAddress,
    amountMicroAlgos: 1000,
    payFlags: { totalFee: 1000 }
  },
  /*
   * tx 3 - Call to permissions stateful smart contract with application arg: 'transfer'
   * The contract ensures that both accA & accB is whitelisted and asset_receiver does not hold more than 100 tokens. */
  {
    type: TransactionType.CallNoOpSSC,
    sign: SignType.SecretKey,
    fromAccount: from,
    appId: permissionsSSCInfo.appID,
    payFlags: { totalFee: 1000 },
    appArgs: ['str:transfer'],
    accounts: [from.addr, toAddr] //  AppAccounts (pass asset sender & receiver address)
  }
];

await executeTransaction(deployer, txGroup);

permissions.py

# fetch asset_holding.balance from Txn.accounts[2] (asset_receiver)
asset_balance = AssetHolding.balance(Int(2), Gtxn[1].xfer_asset())

# Transfer token from accA -> accB. Both A, B are non-reserve accounts.
# Expected arguments (fetched from Txn.Accounts array):
# * fromAccountAddress
# * toAccountAddress
transfer_token = Seq([
    asset_balance, # load asset_balance of asset_receiver from store
    Assert(And(
        Gtxn[1].type_enum() == TxnType.AssetTransfer, # this should be clawback call

        # verify [from, to] addresses (from Txn.accounts) of current_tx
        # should be same as [asset_sender, asset_receiver]
        Txn.accounts[1] == Gtxn[1].asset_sender(),
        Txn.accounts[2] == Gtxn[1].asset_receiver(),

        # rule 1 - check balance of receiver after receiving token <= 100(max_tokens)
        asset_balance.value() <= App.globalGet(max_tokens),

        # rule 2 - [from, to] accounts must be whitelisted
        # NOTE: Int(0) == Txn.Sender(), Int(1) == Txn.accounts[1]
        App.localGet(Int(1), Bytes("whitelisted")) == true, # from account must be whitelisted
        App.localGet(Int(2), Bytes("whitelisted")) == true  # to account must be whitelisted
    )),
    Return(Int(1))
])

Force transfer

This is similar to the transfer use-case, 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 transfer - the difference is that a call to controller is done by asset manager. Asset manager essentially represents clawback here (ceasing tokens from the fromAccount).

Group of 4 transactions is required:

  • tx1: Call to controller smart contract with app-arg = str:force_transfer and foreign-asset = assetIndex, signed by asset manager.
  • tx2: Asset clawback transaction from fromAddress to toAddress, amount = amount. The clawback contract ensures right contract composition.
  • tx3: ALGO payment transaction to clawback to cover tx2 fee (tx3.amount >= tx2.fee). Anyone can make a payment
  • tx4: Call to permissions smart contract to check required permissions, with app-arg = str:transfer and 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 the permissions smart contract call is not required.

algob script:

const asaManager = deployer.accountsByName.get('alice'); // alice is set as the permissions_manager during deploy
const tesla = deployer.asa.get('tesla');
const controllerSSCInfo = deployer.getSSC('controller.py', clearStateProgram);
const permissionsSSCInfo = deployer.getSSC('permissions.py', clearStateProgram);

// notice the difference in calls here: stateful calls are done by token manager here
// and from, to address are only used in asset transfer tx
const forceTxGroup = [
  /*
   * tx 0 - Call to controller stateful smart contract (by ASA.manager)
   * with application arg: 'force_transfer'. The contract ensures that there
   * is a call to permissions smart contract in the txGroup, so that rules
   * are checked during token transfer. */
  {
    type: types.TransactionType.CallNoOpSSC,
    sign: types.SignType.SecretKey,
    fromAccount: asaManager,
    appId: controllerSSCInfo.appID,
    payFlags: { totalFee: 1000 },
    appArgs: ['str:force_transfer'],
    foreignAssets: [tesla.assetIndex] // to verify token reserve, manager
  },
  /*
   * tx 1 - Asset transfer transaction from sender -> receiver. This tx is executed
   * and approved by the clawback lsig (clawback.teal). The clawback lsig address is the
   * address which transfers the frozen asset (amount = amount) from accA to accB.
   * Clawback ensures a call to controller smart contract during token transfer. */
  {
    type: types.TransactionType.RevokeAsset,
    sign: types.SignType.LogicSignature,
    fromAccountAddr: clawbackAddress,
    recipient: toAddr,
    assetID: tesla.assetIndex,
    revocationTarget: fromAddr,
    amount: amount,
    lsig: clawbackLsig,
    payFlags: { totalFee: 1000 }
  },
  /*
   * tx 2 - Payment transaction of 1000 microAlgo to cover clawback transaction cost (tx 1).
   * NOTE: It can be signed by any account, but it should be present in group. */
  {
    type: types.TransactionType.TransferAlgo,
    sign: types.SignType.SecretKey,
    fromAccount: asaManager,
    toAccountAddr: clawbackAddress,
    amountMicroAlgos: 1000,
    payFlags: { totalFee: 1000 }
  },
  /*
   * tx 3 - Call to permissions stateful smart contract with application arg: 'transfer' */
  {
    type: types.TransactionType.CallNoOpSSC,
    sign: types.SignType.SecretKey,
    fromAccount: asaManager,
    appId: permissionsSSCInfo.appID,
    payFlags: { totalFee: 1000 },
    appArgs: ['str:transfer'],
    accounts: [fromAddr, toAddr] //  AppAccounts (pass asset sender & receiver address)
  }
];

await executeTransaction(deployer, forceTxGroup);

controller.py

# Check permissions smart contract is called and it's application index is correct
verify_perm_is_called = And(
    # verify rules call (ensure permissions smart contract is being called in 4th tx)
    Gtxn[3].type_enum() == TxnType.ApplicationCall,
    Gtxn[3].application_id() == App.globalGet(permission_id),
)

# retreive asset manager, reserve from Txn.ForeignAssets[0]
assetManager = AssetParam.manager(Int(0))
assetReserve = AssetParam.reserve(Int(0))

# Force transfer (clawback) some tokens between two accounts.
# Only accepted if token is not killed and sender is asset manager.
# NOTE: If the asset_receiver is the reserve address (current one, or the new one being set
# in the asset config txn in group while updating reserve), then we don't need to verify
# permissions is called - it can bypass rule(s) checks.
force_transfer = Seq([
    assetManager,
    assetReserve,
    Assert(And(
        App.globalGet(var_is_killed) == Int(0), # check token is not killed
        verify_basic_calls,
        Txn.sender() == assetManager.value(), # force_transfer is only allowed by asset manager
    )),
    Return(
        If(
            Or(
                # If the receiver is the reserve address - old, or the new one being updated in the assset config tx (Gtxn[3]),
                # then it can bypass permission checks
                Gtxn[1].asset_receiver() == assetReserve.value(),
                If(Global.group_size() > Int(3), Gtxn[1].asset_receiver() == Gtxn[3].config_asset_reserve(), Int(0))
            ),
            Int(1),
            # else verify that permissions ssc is called
            verify_perm_is_called
        )
    )
])

Tests

Tests using @algo-builder/runtime are also added. An example test is described below.

Tokens can only be issued by token reserve account (positive and negative test).

it('should issue token if sender is token reserve', () => {
  // setup token & deploy contracts
  ctx = new Context(master, alice, bob, elon);

  // Can issue after opting-in
  ctx.optInToASA(elon.address);

  const prevElonAssetHolding = ctx.getAssetHolding(elon.address);
  assert.equal(prevElonAssetHolding.amount, 0n);

  // issue 20 tokens to elon from reserve
  ctx.issue(asaReserve.account, elon, 20);
  ctx.syncAccounts(); // refresh accounts

  assert.equal(
    ctx.getAssetHolding(elon.address).amount,
    prevElonAssetHolding.amount + 20n
  );
});

it('should reject issuance tx if sender is not token reserve', () => {
  ctx.optInToASA(elon.address);

  // verify bob is not asa.reserve
  const asaDef = ctx.getAssetDef();
  const asaReserve = ctx.getAccount(asaDef.reserve);
  assert.notEqual(asaReserve, bob.address);

  assert.throws(() => ctx.issue(bob.account, elon, 20),
    'RUNTIME_ERR1009: TEAL runtime encountered err opcode');
});

More tests(happy and failing paths) can be found here.

Caveat

In the current version we only support a fixed number of linked permissions contracts - the latest TEAL(v3) does not support arrays and looping so we can’t have arbitrary number of linked permissions contracts. User can update the existing permissions contract, or set a new one (overwriting previous one) in controller.py. In the future we are planning to implement use-case for having dynamic number of permissions contracts.

References

  • More interaction use-cases include force-transfer (revoke tokens from accA to accB by asset manager), kill (kill token by asa.manager), update-reserve (updating reserve account to another address), change-permissions-manager, optOut etc.
    Implementation can be found in Algo Builder Examples.
  • Design Spec
  • Use Case API
  • Permissioned Token Freezing - an alternative design and implementation based on Algorand Dev Office Hour presentation.