Using Stateful Smart Contract To Create Algorand Standard Asset
Using Algorand Standard Assets (ASA), one can represent stablecoins, digital art, securities, etc. These are typically created by an Algorand account with a private key however here we do so using a stateful smart contract linked to an escrow account. This opens up the door for complex applications where we can use on-chain data to determine ASA parameters.
We can solve this problem by linking a stateful smart contract to a contract / escrow account . The stateful smart contract stores state that is used to verify the ASA creation parameters and the contract account creates the ASA.
To create an ASA, you would group an application call transaction with an ASA creation transaction using an atomic transfer.
Similarly to send the ASA from the contract account, (if you require on-chain data) you can group an application call transaction with an asset transfer transaction.
The solution demonstrates these features over a very simple example where:
- The application allows you to create assets with names
Xis a digit corresponding to the counter value.
- The application allows anyone to get 1 ASA unit from the escrow account.
Link Stateful and Stateless Smart Contracts
Before we can create and send the ASA using the stateful smart contract, we must first link the contract account to the stateful smart contract. The connection goes both ways: we want to approve transactions from the escrow account (to create/send an ASA) when the application has been called and we want to ensure that the application is linked with an escrow that has this logic.
We can do this by following these steps in order:
- Deploy the application and gets its’ application ID
- Hardcode the application ID into the escrow stateless smart contract and compile it to get the escrow address
- Store the escrow address in the application global state
If we are in the same directory as the PyTeal files, we can first compile the PyTeal stateful smart contract into TEAL using:
python3 stateful.py > stateful.teal python3 clear.py > clear.teal
and deploy the application with:
goal app create --creator <CREATOR-ACCOUNT> --approval-prog ./stateful.teal --global-byteslices 1 --global-ints 1 --local-byteslices 0 --local-ints 0 --clear-prog ./clear.teal
We then use the application ID returned to link the stateless contract to the stateful smart contract. The python file accepts an integer argument for the application ID which is passed to the PyTeal contract account:
python3 contract_account.py <APP-ID> > contract_account.teal
For the last step, we link the stateful smart contract to the stateless smart contract by using a NoOp transaction call to the
goal app call --app-id <APP-ID> --app-arg "str:set_escrow" --app-arg "addr:<ESCROW-ADDRESS>" -f <CREATOR-ACCOUNT>
The logic ensures that only the creator can set the escrow address. We then update the global state and return success:
on_set_escrow = Seq([ Assert(Txn.sender() == Global.creator_address()), App.globalPut(Bytes('escrow'), Txn.application_args), Int(1) ])
Fund Escrow Account
All Algorand contracts have a minimum balance requirement of 100,000 microAlgos, which includes escrow accounts. Therefore, before we can submit any transactions from the escrow account we must first fund it.
There are also minimum balance requirements for every ASA an account creates plus the transaction fees. We could enforce the account that calls the application to pay for these fees (by grouping the requests with an Algo payment transaction to the escrow account) but to simplify the solution, we assume the escrow account has been sufficiently funded in advance.
To fund the escrow account, you can use the following goal command:
goal clerk send -a <AMOUNT> -f <ACCOUNT> -t <ESCROW-ADDRESS>
We can divide the logic used in the PyTeal contracts into the following utility functions:
- create the ASA
- request the ASA
The application contains a counter which is incremented for every new ASA created. Any account can call the application to create a new ASA with asset name
X is a digit corresponding to the counter value.
Note: to simplify the solution, the
X digit wraps around when the counter exceeds 9, e.g. 47 mod 10 = 7.
To create the ASA, we group atomically a call to the stateful smart contract with the string
"create_asa" and an asset creation transaction from the escrow account:
# create transactions goal app call --app-id <APP-ID> --app-arg "str:create_asa" -f <ACCOUNT> -o unsignedtx0.tx goal asset create --creator <ESCROW-ADDRESS> --total <TOTAL> --name "AppASA-<X>" --decimals <DECIMALS> -o unsignedtx1.tx # combine transactions cat unsignedtx0.tx unsignedtx1.tx > combinedtransactions.tx goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx # split transactions goal clerk split -i groupedtransactions.tx -o split.tx # sign transactions goal clerk sign -i split-0.tx -o signout-0.tx goal clerk sign -i split-1.tx -p contract_account.teal -o signout-1.tx # assemble transaction group cat signout-0.tx signout-1.tx > signout.tx # submit goal clerk rawsend -f signout.tx
To verify the asset name, we first transform the counter value into a single-digit number by taking its modulus 10. We then convert the value into a byte character
'1', … using the
Substring opcode over the string
"0123456789" (to ensure we do not calculate the value multiple times, we use scratch space). We then concatenate the
"AppASA-" with the byte character. At this point, we can verify that the result matches the asset name used in the asset creation transaction.
digit_stored.store(App.globalGet(Bytes('counter')) % Int(10)) . . . Eq( Gtxn.config_asset_name(), Concat( Bytes('AppASA-'), Substring( Bytes('0123456789'), digit_stored.load(), digit_stored.load() + Int(1) ) ) )
We finally increment the counter:
increment_counter = App.globalPut( Bytes('counter'), App.globalGet(Bytes('counter')) + Int(1) )
Any account can call the application to request one unit of one ASA created by the escrow account to be transferred to them. Before this, the account must ensure they have opted into the ASA:
goal asset send --assetid <ASSET-ID> -f <ACCOUNT> -t <ACCOUNT> -a 0
After opting in, the account can request the ASA by specifying the ASA using its asset ID. To send the ASA, we group atomically a call to the stateful smart contract with the string
"fund_asa" and an asset transfer transaction from the escrow account:
# create transactions goal app call --app-id <APP-ID> --app-arg "str:fund_asa" -f <ACCOUNT> -o unsignedtx0.tx goal asset send --assetid <ASSET-ID> -f <ESCROW-ADDRESS> -t <ACCOUNT> -a 1 -o unsignedtx1.tx # combine transactions cat unsignedtx0.tx unsignedtx1.tx > combinedtransactions.tx goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx # split transactions goal clerk split -i groupedtransactions.tx -o split.tx # sign transactions goal clerk sign -i split-0.tx -o signout-0.tx goal clerk sign -i split-1.tx -p contract_account.teal -o signout-1.tx # assemble transaction group cat signout-0.tx signout-1.tx > signout.tx # submit goal clerk rawsend -f signout.tx
To ensure a bad actor cannot steal all the assets, we check that the closeout address of the asset transfer is set to the zero address:
asset_close_to_check = Txn.asset_close_to() == Global.zero_address()
We must be careful to check that the asset sender is the zero address, i.e. not a clawback transaction. Otherwise, an account could use the escrow account (if its address is set to the clawback address) to steal another account’s ASA:
Txn.asset_sender() == Global.zero_address()
We can extend the stateful smart contract to determine if the transaction is approved based on the stored state. For example, we can store in the local storage of an account how many ASAs the account can request in total. This is left as an exercise for the reader.
In this solution, we learned how to use a stateful smart contract linked with a contract account to create an ASA. The logic can be extended in many ways when one requires on-chain data to determine ASA parameters.
Real applications would need to add additional restrictions to the above operations. In particular, the asset creation operation may restrict the manager address, total units, …, as well as the total number of ASAs created. The asset creation and transfer may also require payment for the associated fees involved.