Smart ASA
Tags: algorand, blockchain
Published: 2022-11-01 14:20:14

## Implementing Royalty Transfers and Asset delegation of ASA using a Smart ASA contract.


##Overview


##Introduction
   

  The Algorand Standard Asset (ASA) is an excellent building block for on-chain applications. It is battle-tested and widely supported by SDKs, wallets, and dApps.


  However, the ASA lacks in flexibility and configurability. For instance, once issued it can’t be re-configured (its unit name, decimals, maximum supply). Also, it is freely transferable (unless frozen). This prevents developers from specifying additional business logic to be checked while transferring it (think of royalties or asset_delegation).ARC-0020 allows creators to add extra functionality to their tokens that is not possible with ASAs by leveraging smart contracts. This particular contracts adds the below additional functionalities.




* Transferring a set amount of the asset whenever any type of asset transfer occurs between two holders of the asset


*Enabling other holders of the asset to spend a preset amount of your assets.

  This solution was built using Algorand’s Smart Contracts generated with PyTeal (TEAL v7).
  
  

##Design Overview

 **Assumptions**
 
 In this solution we assume that every transfer of this asset is always greater than the royalty amount   set.The royalty amount can't also be changed after setting it on deployment.

 **State**
  
  The smart contract 14 global variables where 6 are **integers** and 8 are **bytes**
        1.**smart_asa_id**-algorand standard asset Id this contract controls
        2.**royalty_amount**-amount of asa paid to the creator of the contract for every asset transfer
        3.**frozen**-freeze transfer of these assets
        4.**total**-total supply of asset
        5.**decimals**-number of digits to use after the decimal point when displaying the asset
        6.**default_frozen**- freeze holdings for this asset by default.
        7.**unit_name**-name of a unit of this asset
        8.**name**-name of the asset
        9.**url**-specifies a URL where more information about the asset can be retrieved
        10.**metadata_hash**- 32-byte hash of some metadata that is relevant to the asset and contract.
        11.**manager_addr**-address of the account that can manage the configuration of the asset
        12.**reserve_addr**-address of the account that holds the reserve (non-minted) units of the asset
        13.**freeze_addr**-address of the account used to freeze holdings of this asset
        14.**clawback_addr**-address of the account that can clawback holdings of this asset.

 Opted-In accounts local storage are also used as follows:
        1.**smart_asa_id**-asa this user has opted into.Must be equal to that controlled by the smart contract
        2.**frozen**-frozen status of account.
        3.The rest of the users local storage is used to for asset delegation

**Operations**

  Our smart contract supports 3 major methods:
1. asset_transfer-transfer assets between two accounts(opted into the asset and contract) and enforces a royalty fee transfer to the creator
2. asset_delegate- allow another account (opted into the contract and asset) to spend certain amounts of your tokens.
3. get_delegate_allowance-retrieve the amount allowed to be spent by a delegated account  
4. delegate_asset_transfer-the delegated account initiates a transfer of the asset on behalf of the owner.




## PyTeal Code
Let us get into the actual pyteal code needed for the implementation of the above methods

Initial deployment of the contract is handled as follows

```py 
@Subroutine(TealType.none)
def set_global_vars(total: Expr, decimals: Expr, default_frozen: Expr, unit_name: Expr, name: Expr, url: Expr, metadata_hash: Expr, manager_addr: Expr, reserve_addr: Expr, freeze_addr: Expr, clawback_addr: Expr):
    return Seq(
        App.globalPut(Bytes("total"), total),
        App.globalPut(Bytes("decimals"), decimals),
        App.globalPut(Bytes("default_frozen"), default_frozen),
        App.globalPut(Bytes("unit_name"), unit_name),
        App.globalPut(Bytes("name"), name),
        App.globalPut(Bytes("url"), url),
        App.globalPut(Bytes("metadata_hash"), metadata_hash),
        App.globalPut(Bytes("manager_addr"), manager_addr),
        App.globalPut(Bytes("reserve_addr"), reserve_addr),
        App.globalPut(Bytes("freeze_addr"), freeze_addr),
        App.globalPut(Bytes("clawback_addr"), clawback_addr),
    )

# store the deployer of the contract in global storage and initialize global vars
handle_Creation = Seq(
    App.globalPut(Bytes("smart_asa_id"), Int(0)),
    App.globalPut(Bytes("royalty_amount"), Int(2)),
    App.globalPut(Bytes("frozen"), Int(0)),
    set_global_vars(Int(0), Int(0), Int(0), Bytes(""), Bytes(""), Bytes(""), Bytes(
        ""), Global.zero_address(), Global.zero_address(), Global.zero_address(), Global.zero_address()),
    Approve()
)
```
The above sequence just simply initialied value for the smart contract global variables

###ABI SUBROUTINES

**asset_transfer**
``` py
@ABIReturnSubroutine
def asset_transfer(xfer_asset: abi.Asset, asset_amount: abi.Uint64, asset_sender: abi.Account, asset_receiver: abi.Account, royalty_receiver: abi.Account):
    is_sender = Txn.sender() == asset_sender.address()
    asset_frozen = App.globalGet(Bytes("frozen"))
    is_smartASA_id = xfer_asset.asset_id() == smart_asa_id
    is_clawback = Txn.sender() == current_clawback_addr
    sender_frozen = App.localGet(asset_sender.address(), Bytes("frozen"))
    receiver_frozen = App.localGet(asset_receiver.address(), Bytes("frozen"))
    # only reserve addr can mint token and the tokens must come from the smart contract
    is_minting = And(
        Txn.sender() == current_reserve_addr,
        asset_sender.address() == Global.current_application_address()
    )
    # only reserve addr can burn tokens and reserve can only burn its own tokens to be sent to
    is_burning = And(
        is_sender,
        asset_sender.address() == current_reserve_addr,
        asset_receiver.address() == Global.current_application_address()
    )
    users_opted_in = And(
        smart_asa_id == App.localGet(
            asset_sender.address(), Bytes("smart_asa_id")),
        smart_asa_id == App.localGet(
            asset_receiver.address(), Bytes("smart_asa_id")),
    )
    return Seq(
        Assert(
            And(
                smart_asa_id,
                is_smartASA_id,
                Len(asset_sender.address()) == Int(32),
                Len(asset_receiver.address()) == Int(32),
                asset_amount.get() > current_royalty_amount,
                royalty_receiver.address() == Global.creator_address()
            )
        ),
        # if it is a normal transaction and not clawback addr
        If(And(is_sender, Not(is_clawback))).Then(
            Assert(
                And(
                    Not(sender_frozen),
                    Not(receiver_frozen),
                    Not(asset_frozen),
                    users_opted_in)
            )
        ).ElseIf(is_minting).Then(
            # make sure mint doesnt carry it over the total
            Assert(
                And(
                    Not(asset_frozen),
                    Not(receiver_frozen),
                    smart_asa_id == App.localGet(
                        asset_receiver.address(), Bytes("smart_asa_id")),
                    calculate_circulating_supply(xfer_asset.asset_id(
                    ))+asset_amount.get() <= App.globalGet(Bytes("total"))
                )

            )
        ).ElseIf(is_burning).Then(
            Assert(
                And(
                    Not(asset_frozen),
                    Not(sender_frozen),
                    smart_asa_id == App.localGet(
                        asset_sender.address(), Bytes("smart_asa_id")),
                )
            )
        ).Else(
            Assert(And(is_clawback, users_opted_in)),
        ),
        # Group transaction to transfer asset and pay royalty sum to creator
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.AssetTransfer,
                TxnField.xfer_asset: smart_asa_id,
                TxnField.asset_amount: asset_amount.get()-current_royalty_amount,
                TxnField.asset_sender: asset_sender.address(),
                TxnField.asset_receiver: asset_receiver.address(),
                TxnField.fee: Int(0),
            }
        ),
        InnerTxnBuilder.Next(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.AssetTransfer,
                TxnField.xfer_asset: smart_asa_id,
                TxnField.asset_amount: current_royalty_amount,
                TxnField.asset_sender: asset_sender.address(),
                TxnField.asset_receiver: royalty_receiver.address(),
                TxnField.fee: Int(0),
            }
        ),
        InnerTxnBuilder.Submit(),
    )

```
 
The above subroutine simply checks if valid conditions are met for the transfer to occur.The royalty fee is removed from the asset amount to be transferred and sent to the creator of the contract.The asset transfer transaction and royalty transaction are then grouped together and submitted.

----------



**asset_delegate**


```py
@ABIReturnSubroutine
def asset_delegate(delegate_asset: abi.Asset, amount: abi.Uint64, spender: abi.Account):
    asset_frozen = App.globalGet(Bytes("default_frozen"))
    is_smartASA_id = smart_asa_id == delegate_asset.asset_id()
    owner_optedIn = smart_asa_id == App.localGet(
        Txn.sender(), Bytes("smart_asa_id"))
    owner_frozen = App.localGet(Txn.sender(), Bytes("frozen"))
    owner_balance = AssetHolding.balance(
        Txn.sender(), delegate_asset.asset_id()
    )
    spender_optedIn = smart_asa_id == App.localGet(
        spender.address(), Bytes("smart_asa_id"))
    spender_frozen = App.localGet(spender.address(), Bytes("frozen"))
    return Seq(
        # check spender and owner are opted in and are not frozen and you cant delegate yourself
        Assert(
            And(
                smart_asa_id,
                spender.address() != Txn.sender(),
                Not(asset_frozen),
                is_smartASA_id,
                owner_optedIn,
                Not(owner_frozen),
                spender_optedIn,
                Not(spender_frozen)
            )
        ),
        owner_balance,
        # assert that amount to be delegated is not more than user balance
        Assert(
            And(
                owner_balance.hasValue(),
                amount.get() <= owner_balance.value()
            )
        ),
        App.localPut(Txn.sender(), spender.address(), amount.get()),
        Approve()
    )
```
The above subroutine is used to delegate assets to other accounts to spend.The contract asserts that the spender and owner(sender of the transaction) are opted into the contract and are not frozen. It also ensures that the amount delegated is less than the asset balance of the owner. It then puts a key-value pair in the sender accounts local storage where the key is the address of the sender and the value is the asset amount that can be spent by the delegate.

----------


**get_delegate_allowance**

```py
@ABIReturnSubroutine
def get_delegate_allowance(delegate_asset: abi.Asset, owner: abi.Account, spender: abi.Account, *, output: abi.Uint64):
    is_smartASA_id = delegate_asset.asset_id() == smart_asa_id
    owner_optedIn = smart_asa_id == App.localGet(
        owner.address(), Bytes("smart_asa_id"))
    spender_optedIn = smart_asa_id == App.localGet(
        spender.address(), Bytes("smart_asa_id"))
    return Seq(
        Assert(
            And(
                smart_asa_id,
                is_smartASA_id,
                owner_optedIn,
                spender_optedIn
            ),
        ),
        output.set(App.localGet(owner.address(), spender.address()))
    )

```
The above method is used to get the current allowance of a delegate spender of an asset for the owner of the asset.

----------



```py
@ABIReturnSubroutine
def delegate_asset_transfer(xfer_asset: abi.Asset, asset_amount: abi.Uint64, asset_owner: abi.Account, asset_receiver: abi.Account, royalty_receiver: abi.Account):
    asset_frozen = App.globalGet(Bytes("default_frozen"))
    is_smartASA_id = xfer_asset.asset_id() == smart_asa_id
    owner_optedIn = smart_asa_id == App.localGet(
        asset_owner.address(), Bytes("smart_asa_id"))
    owner_frozen = App.localGet(asset_owner.address(), Bytes("frozen"))
    spender_optedIn = App.localGet(Txn.sender(), Bytes("smart_asa_id"))
    spender_frozen = App.localGet(Txn.sender(), Bytes("frozen"))
    spender_allowance = App.localGet(asset_owner.address(), Txn.sender())
    return Seq(
        Assert(
            And(
                smart_asa_id,
                Not(asset_frozen),
                is_smartASA_id,
                owner_optedIn,
                Not(owner_frozen),
                spender_optedIn,
                Not(spender_frozen),
                asset_amount.get() <= spender_allowance,
                asset_amount.get() > current_royalty_amount,
                royalty_receiver.address() == Global.creator_address()
            )
        ),
        App.localPut(asset_owner.address(), Txn.sender(),
                     spender_allowance-asset_amount.get()),
        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.AssetTransfer,
                TxnField.xfer_asset: smart_asa_id,
                TxnField.asset_amount: asset_amount.get()-current_royalty_amount,
                TxnField.asset_sender: asset_owner.address(),
                TxnField.asset_receiver: asset_receiver.address(),
                TxnField.fee: Int(0),
            }
        ),
        InnerTxnBuilder.Next(),
        InnerTxnBuilder.SetFields(
            {
                TxnField.type_enum: TxnType.AssetTransfer,
                TxnField.xfer_asset: smart_asa_id,
                TxnField.asset_amount: current_royalty_amount,
                TxnField.asset_sender: asset_owner.address(),
                TxnField.asset_receiver: royalty_receiver.address(),
                TxnField.fee: Int(0),
            }
        ),
        InnerTxnBuilder.Submit(),

    )
```
This method is used by a delegated spender to transfer the smart-asa on behalf of the owner.The contract asserts that the spender is opted into the contract and asset,the asset amount to be transferred is not more than the preset amount and the royalty receiver address is valid. The asset is transferred and the spender's local state is updated with the new balance of the account.

Note we have the following constants in out pyteal contract


* is_creator = Txn.sender() == Global.creator_address()
* is_manager = Txn.sender() == App.globalGet(Bytes("manager_addr"))
* smart_asa_id = App.globalGet(Bytes("smart_asa_id"))
* current_royalty_amount = App.globalGet(Bytes("royalty_amount"))
* current_clawback_addr = App.globalGet(Bytes("clawback_addr"))
* current_reserve_addr = App.globalGet(Bytes("reserve_addr"))
* current_freeze_addr = App.globalGet(Bytes("freeze_addr"))
* current_manager_addr = App.globalGet(Bytes("manager_addr"))


**The approval program** 
    The routing of calls of the contract and generation of approval and clear program is handled as follows
```py
# handle routing logic of your contract
router = Router(
    name="Smart ASA",
    bare_calls=BareCallActions(
        no_op=OnCompleteAction(
            action=handle_Creation, call_config=CallConfig.CREATE
        ),
        opt_in=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        clear_state=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        close_out=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        # Prevent updating and deleting of this application
        update_application=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),
        delete_application=OnCompleteAction(
            action=Reject(), call_config=CallConfig.CALL
        ),

    )
)

router.add_method_handler(asset_create)
# router.add_method_handler(asset_config)
router.add_method_handler(get_asset_config)
router.add_method_handler(
    asset_optin, method_config=MethodConfig(opt_in=CallConfig.CALL))
router.add_method_handler(asset_transfer)
# router.add_method_handler(asset_freeze)
# router.add_method_handler(get_asset_frozen)
# router.add_method_handler(get_account_frozen)
# router.add_method_handler(get_circulating_supply)
# router.add_method_handler(asset_destroy)
# router.add_method_handler(
#     asset_optout, method_config=MethodConfig(close_out=CallConfig.CALL))
router.add_method_handler(asset_delegate)
router.add_method_handler(get_delegate_allowance)
router.add_method_handler(delegate_asset_transfer)


approval_program, clear_state_program, contract = router.compile_program(
    version=7, optimize=OptimizeOptions(scratch_slots=True)
)


with open("contracts/app.teal", "w") as f:
    f.write(approval_program)

with open("contracts/clear.teal", "w") as f:
    f.write(clear_state_program)


with open("contracts/contract.json", "w") as f:
    f.write(json.dumps(contract.dictify(), indent=4))

```


###USAGE
 The smart-ASA can be deployed and tested locally or testned.Watch this [video](https://youtu.be/5jf6CZ6znpg) for steps on testing locally.