Solutions
No Results
Solution

Assets and Custom Transfer Logic

Overview

Using Algorand’s native layer one Assets, developers can create a token that represents either a real world or digital good, service, or resource in a matter of minutes. No smart contract is required. These assets are very powerful and allow for flexibility in how they are traded and controlled. Typically assets are traded as any other token on the chain. All that is required is to issue an asset transfer transaction.

EditorImages/2020/12/22 18:26/1.png

This is fine for most cases, but what if you want some custom logic to execute to approve the transfer. This may be the case when you have one of the following scenarios.

  • KYC/AML - The user’s identity must be verified before the transaction is approved
  • Extra Fees Required - Such as taxes, commission on real estate, or some basis point fee must be paid.

These are not the only times you may want to use this pattern in a transaction but represent some key areas where some logic (within a contract) approves the transaction.

EditorImages/2020/12/22 18:28/2.png

This solution explains the process of how this can be done in Algorand using a Smart Contract Application.

This application will involve using many of the Algorand technologies, including stateless smart contracts, stateful smart contracts, atomic transfers, standard assets, and asset transactions. We will begin by covering the overall application design and then dive into the specifics on how to build the application.

Problem Solution

Using Algorand’s Atomic Transaction feature allows multiple transactions to be submitted at one time and if any of the transactions fail, then they all fail. So, one way of solving this issue is to group an asset transfer transaction with a call to a stateful smart contract and submit them simultaneously. The only caveat to this is that nothing prevents the asset transfer transaction being submitted by itself.

To get around this, you can use a couple Asset properties when configuring the token. Each Asset in Algorand has 4 configurable addresses that can be changed. These are the Manager Address, The Reserve Address, The Freeze Address, and the Clawback Address. The Manager is responsible for configuring the asset and can change any of the other three addresses. The Reserve account is the account to hold un-minted tokens. The Freeze account is responsible for freezing and unfreezing an asset. Note that a frozen asset can not be traded and can be the default for a created asset. Also the freeze account can freeze or unfreeze specific accounts. The Clawback address can revoke an Asset from any account and send that asset to any other account, even if the asset is frozen.

So one solution to solve the issue presented in the opening of the article is to create the Asset default frozen, meaning only the clawback account can transfer the token. Then assign the Clawback address to a stateless smart contract escrow account. The logic can then be placed in the stateless smart contract. If you also need onchain data, the stateless smart contract can enforce a call to a stateful smart contract. So for example, assume Alice wants to send an asset to Bob, She can make a call to a stateful contract to check if its ok to transfer an Asset and atomically group that with another transaction from the escrow stateless clawback account to move the token from her account to Bob’s account. The Stateless contract would also verify that the stateful contract was also called. If your logic does not require any stateful data, you can do everything in the stateless contract, which further simplifies the solution.

EditorImages/2020/12/22 18:36/3.png

In this scenario, the clawback address pays the transaction fee for the transfer from Alice to Bob. So the code can be modified to check for a payment transaction from Alice to the Escrow to refund the transaction fee.

EditorImages/2020/12/22 18:36/4.png

So the total solution involves three transactions which must be atomically grouped.

EditorImages/2020/12/22 18:37/5.png

Now we still have to do a bit of configuration on the asset to make sure that only logic in the smart contracts are deciding on the viability of the transaction. First we need to lock the freeze account, so the manager can then never change it in the future. This is simply done by setting the freeze account to “” (this sets the account to the Zero Account) using goal. This will lock the asset freeze account for the life of the asset. In addition you can then lock the manager account in the same way, so the clawback address will always be the stateless smart contract escrow account.

Info

Learn more about the Zero Account

Info

There are other ways to implement this solution including unfreezing the asset, calling the contract, transferring the asset, and then refreezing the asset, which will take at least 6 transactions. Another option is to not use an ASA and create the asset fully in a stateful smart contract.

Design Overview

To illustrate the solution provided above, the following sections implement this scenario using an example. The example uses a stateful smart contract that uses a level system. The level is a simple integer and represents the required level a user must have to transfer a specific asset. Users who opt into the stateful smart contract have their level stored in local storage. So for a given asset we store globally, the required level to transfer the asset and for each user we store locally, their current level for the asset. So in the previous section, Alice and Bob would have an integer value corresponding to their current level to trade the frozen asset. If both of their levels are high enough the transfer will be successful (The Stateful Contract will approve the transaction). If either one of their levels is not high enough the transfer will not be successful (The Stateful Contract will reject the transaction).

To implement this solution, three basic operations are required to be implemented in the stateful smart contract. The first operation is only executable by the stateful smart contract creator and it allows setting the level for a specific asset for a given user. The second operation is only executable by the stateful smart contract creator as well and it allows clearing the level for a specific user. The final operation is a call that checks if an asset transfer is ok. This operation can be called by anyone wishing to transfer an asset. This operation verifies both the asset sender and receiver levels are higher than or equal the required level for the asset.

Contract Creation

An account first has to create the smart contract. This account is primarily responsible for setting and clearing the level of a user who opts into the contract. Note that for users the local storage variable name is “Accred-Level” and its value will always be an integer. As part of creating the contract the code expects two integers. One is the asset ID this contract functions for and the other is the required “Accred-Level” to trade the asset. Using the ‘goal’ command tool, creating this contract will look similar to the following.

goal app create --creator <CREATOR-ACCOUNT> --app-arg "int:<ASSETID>” --app-arg "int:<LEVEL>" --approval-prog ./poi.teal --global-byteslices 1 --global-ints 2 --local-byteslices 0 --local-ints 1  --clear-prog ./poi-clear.teal 

This stateful smart contract uses three global variables. One (byteslice) to store the creator address, one (int) to store the asset id, and one (int) to store the required level for trading it.

This contract code does the following.

  • Verify that the contract is actually being created with this transaction.
  • Verify that two arguments are passed to the stateful smart contract.
  • Store the transaction sender’s address in the global variable “Creator”.
  • Store the Asset ID in the global variable “AssetID”.
  • Store the required level in the global variable “Asset Level”.

// Creator Address
// check if the app is being created
// if so save creator
int 0
txn ApplicationID
==
bz not_creation
txn NumAppArgs
int 2
==
bz failed
byte "Creator"
txn Sender
app_global_put
byte "AssetID"
txna ApplicationArgs 0
btoi
app_global_put
byte "AssetLevel"
txna ApplicationArgs 1
btoi
app_global_put
int 1
return

Set User Level

This method of the stateful smart contract simply requires a string argument specifying the method “set-level” and an integer argument specifying what to set the level to. The account whose level is being set must be added to the accounts array for the specific transaction. Using goal the command will look similar to the following.

 goal app call --app-id <APPID> --app-account=<ACCCOUNT-TO-SET> --app-arg "str:set-level" --app-arg "int:2" --from=<SMART-CONTRACT-CREATOR>

Where APPID is the stateful smart contract ID.

EditorImages/2020/12/22 18:43/6.png

This contract code does the following checks.

  • Verify that two arguments are passed to the stateful smart contract.
  • Verity that the creator of the smart contract is the sender of the transaction.
  • Only one transaction is being submitted.
  • Set the user’s (passed in accounts array) level (stored as variable “Accred-Level”)

//only callable by creator
//just one arg
set-level:
txn NumAppArgs
int 2
==
byte "Creator"
app_global_get
txn Sender
==
&&
global GroupSize
int 1
==
&&
bz failed
int 1 //first account in accounts array
byte "Accred-Level"
txna ApplicationArgs 1
btoi
app_local_put
int 1
return

Clear User Level

This method of the stateful smart contract simply requires a string argument specifying the method “clear”. The account whose level is being cleared must be added to the accounts array for the specific transaction. Using goal the command will look similar to the following.

goal app call --app-id <APPID> --app-account=<ACCCOUNT-TO-CLEAR> --app-arg "str:clear"  --from=<CREATOR-ACCOUNT>

Where APPID is the stateful smart contract ID.

EditorImages/2020/12/22 18:45/7.png

This contract code does the following checks.

  • Verify that one argument is passed to the stateful smart contract.
  • Verify that the creator of the smart contract is the sender of the transaction.
  • Only one transaction is being submitted.
  • Deletes the local storage variable “Accred-Level” for the user.

clear:
txn NumAppArgs
int 1
==
byte "Creator"
app_global_get
txn Sender
==
&&
global GroupSize
int 1
==
&&
bz failed
int 1 //first account in accounts array
byte "Accred-Level"
app_local_del
int 1
return

Check Level And Transfer Asset

This method represents the chief function of the stateful smart contract. That is to check that three transactions are submitted that represent:

  • Call to the stateful smart contract to check the sender and receiver are the proper level.
  • Asset Transfer transaction using Clawback account from Asset Sender to Receiver.
  • Payment transaction to cover the fee from Asset Sender to the Clawback Address.

The call to the stateful smart contract must pass the string argument “check-level” and add the Asset Receiver to the accounts array. The transaction should be sent from the account that wants to transfer the asset. Using goal, this transaction will be similar to the following.

goal app call --app-id <APPID>  --app-arg "str:check-level" --app-account <ASSET-RECEIVER> --from <ASSET-SENDER>

This transaction should be grouped atomically with the other two transactions. Using goal this would be similar to the following.

# Three Transactions
goal app call --app-id <APPID>  --app-arg "str:check-level" --app-account <ASSET-RECEIVER> --from <ASSET-SENDER> --out=unsginedtransaction1.tx
goal asset send -a 1000 --assetid <ASSETID> -f <ASSET-SENDER> -t <ASSET-RECEIVER> --clawback <ESCROW-ADDRESS> --out=unsginedtransaction2.tx
goal clerk send --from=<ASSET-SENDER> --to=<ESCROW-ADDRESS> --amount=1000 --out=unsginedtransaction3.tx

# Combine and Group Transactions
cat unsginedtransaction1.tx unsginedtransaction2.tx unsginedtransaction3.tx > combinedtransactions.tx
goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx 

# Split and Sign Transactions
goal clerk split -i groupedtransactions.tx -o split.tx 
goal clerk sign -i split-0.tx -o signout-0.tx
goal clerk sign --program ./clawback-escrow.teal -i split-1.tx -o signout-1.tx 
goal clerk sign -i split-2.tx -o signout-2.tx

# Combine and send signed transactions
cat signout-0.tx signout-1.tx signout-2.tx  > signout.tx
goal  clerk rawsend -f signout.tx

Where APPID is the stateful smart contract ID and ASSETID, is the asset being traded.

EditorImages/2020/12/22 18:48/8.png

This contract code does the following checks.

  • Verify that three transactions are in the group.
  • Verify the first is a stateful contract call.
  • Verify the second in an Asset Transfer.
  • Verify the third is a Payment transaction.
  • Verify that the current transaction is the stateful contract call.

// check level should be called
// with the following txes
// tx 0 - check level smart contract call - signed by sender of asset
// tx 1 - clawback transactions that moves the frozen asset from 
// sender to receiver - signed by clawback-escrow
// tx 2 - payment transaction from sender to clawback-escrow 
// to pay for the fee of the clawback

check-level:
// check number and types of txes
global GroupSize
int 3
==
gtxn 0 TypeEnum
int appl
==
&&
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 2 TypeEnum
int pay
==
&&
// Verify that this is index the stateful contract call
txn GroupIndex
int 0
==
&&
bz failed

  • Next check basic guidelines for making sure that none of the transactions are a rekey or closeout transaction.

// check no rekeying etc
gtxn 0 RekeyTo
global ZeroAddress
==
gtxn 1 RekeyTo
global ZeroAddress
==
&&
gtxn 2 RekeyTo
global ZeroAddress
==
&&
gtxn 0 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 1 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 2 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 0 AssetCloseTo
global ZeroAddress
==
&&
gtxn 1 AssetCloseTo
global ZeroAddress
==
&&
gtxn 2 AssetCloseTo
global ZeroAddress
==
&&
bz failed

  • Get the global variable for the AssetID and store it in scratch space to be used later.

// get asset id from global
int 0 //current app
byte "AssetID"
app_global_get_ex
bz failed
// store assetid
store 11

  • Verify first transaction is the call to this stateful smart contract.
  • Verify that the first and last transactions are sent by the Asset Sender.

//check first transaction
//check level smart contract call - signed by asset sender
gtxn 0 ApplicationID
global CurrentApplicationID
==
gtxn 0 Sender
gtxn 2 Sender
==
&&
gtxn 0 Sender
gtxn 1 AssetSender
==
&&
bz failed

  • Verify the first account in the accounts array is indeed the AssetReceiver.
  • The Transferred Asset is the Asset ID specified in Global state.

//check second transaction
// tx 1 - clawback transactions that moves the frozen 
// asset from sender to receiver - signed by clawback-escrow
// verify the account sent in the accounts array is 
// actually the receiver of the asset in asset xfer
gtxn 0 Accounts 1
gtxn 1 AssetReceiver
==
gtxn 1 XferAsset
load 11
==
&&
bz failed

  • Verify that the Clawback Address is the Receiver of the Payment transaction.
  • Verify that the fee for the Asset Transfer is covered by the Payment transaction.

//check third transaction
// tx 2 - payment transaction from sender 
// to clawback-escrow to pay for the fee of the clawback
gtxn 1 Sender
gtxn 2 Receiver
==
// verify the fee amount is good
gtxn 2 Amount
gtxn 1 Fee
>=
&&
bz failed

  • Verify Asset Sender’s level is higher than or equal to the required level.
  • Verify Asset Receiver’s level is higher than or equal to the required level.

// now handle the level check
// get asset sender stored accredidation level
int 0 //sender
gtxn 0 ApplicationID
byte "Accred-Level"
app_local_get_ex
bz failed // must have an accred level set
// top of the stack has the current user level
// load asset required level
byte "AssetLevel"
app_global_get
>=
bz failed // users level not high enough
// get asset receiver stored accredidation level
int 1 //asset receiver account
txn ApplicationID
byte "Accred-Level"
app_local_get_ex
bz failed // must have an accred level set
// top of the stack has the current user level
// load asset required level
byte "AssetLevel"
app_global_get
>=
bz failed // users level not high enough
int 1
return

Stateless Escrow Contract

The stateless escrow contract account that is set to the asset’s clawback address contains many of the same checks as the stateless contract. The primary difference is that the escrow must be tied to the stateful contract and know the Asset ID that it is enforcing. The escrow code is shown below. Note the first four lines store the stateful smart contract ID and the Asset ID. When deploying you will need to modify these variables.

#pragma version 2
// Application ID - Must be changed before compiling
int 3
store 10
// Asset ID - Must be changed before compiling
int 1
store 11

global GroupSize
int 3
==
gtxn 0 TypeEnum
int appl
==
&&
gtxn 1 TypeEnum
int axfer
==
&&
gtxn 2 TypeEnum
int pay
==
&&
// Verify this is indeed the asset transfer tx
txn GroupIndex
int 1
==
&&
bz failed


// check no rekeying etc
gtxn 0 RekeyTo
global ZeroAddress
==
gtxn 1 RekeyTo
global ZeroAddress
==
&&
gtxn 2 RekeyTo
global ZeroAddress
==
&&
gtxn 0 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 1 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 2 CloseRemainderTo
global ZeroAddress
==
&& 
gtxn 0 AssetCloseTo
global ZeroAddress
==
&&
gtxn 1 AssetCloseTo
global ZeroAddress
==
&&
gtxn 2 AssetCloseTo
global ZeroAddress
==
&&
bz failed


//Check first transaction
//check level smart contract call - signed by asset sender
gtxn 0 ApplicationID
load 10
==
gtxn 0 Sender
gtxn 2 Sender
==
&&
gtxn 0 Sender
gtxn 1 AssetSender
==
&&
bz failed

//Check second transaction
// tx 1 - clawback transactions that moves the frozen asset 
// from sender to receiver - signed by clawback-escrow
// verify the account sent in the accounts array is 
// actually the receiver of the asset in asset xfer
gtxn 0 Accounts 1
gtxn 1 AssetReceiver
==
gtxn 1 XferAsset
load 11
==
&&
bz failed

//Check third transaction
// tx 2 - payment transaction from sender to clawback-escrow 
// to pay for the fee of the clawback
gtxn 1 Sender
gtxn 2 Receiver
==
// verify the fee amount is good
gtxn 2 Amount
gtxn 1 Fee
>=
&&
bz failed
int 1
return

failed:
int 0
return

Some Caveats

The application was deployed and tested on a private network and used the goal command line tool to call it. All goal commands could be recreated using any of the SDKs. See the developer documentation for more details.

Conclusion

This application pattern presented in this example can be used for many types of applications that want to implement custom logic with an Asset transfer. The example used both stateful and stateless contracts. If no global or local state is required, the application can be simplified to two transactions and use only one stateless contract. All the source code is available on Github.

stateful smart contracts

algorand standard assets

atomic transfer

smart contracts

December 23, 2020