Using a Smart Contract to Spawn Additional Smart Contracts
With TEAL version 6, Algorand smart contracts can make app to app calls using inner transactions. This is a critical component in many multi contract applications and having this ability offers developers a great deal of flexibility in how they design their blockchain apps. This change allows developers to not only call another smart contract, but to also use a smart contract to create a new contract, call it and then even delete the contract. This effectively gives you the ability to spawn short lived contracts that can be used and then destroyed, perhaps in a single atomic transaction group.
In this article, we will walk through an example of a parent contract that spawns additional child contracts, calls the newly created child contract and finally destroys the child contract.
The example in this article uses the Application Binary Interface(ABI). For more information about how to use ABI with your smart contracts see this article, the ABI specification, or the developer documentation. Developers should use the Atomic Transaction Composer(ATC) to create proper ABI-based transactions. For more information on ATC, see the developer documentation. In this example a shell script is used in conjunction with the
goal command tool, as we are focusing on the contracts.
Run The Example
The code for this example is located on the Algorand DevRel github repository. To quickly run this example, first verify that you have the sandbox running in
dev mode. To understand how to install and run the sandbox see the [sandbox readme]https://github.com/algorand/sandbox#readme). Once sandbox is installed and running, do the following from a terminal.
git clone https://github.com/algorand-devrel/parent-child-contracts cd parent-child-contracts
Edit the demo.sh script and set the appropriate location of your
Run the script.
This demo shell script deploys the parent contract on the sandbox, funds the contract, deploys a child contract through the parent contract, calls the child contract, and then destroys the child contract through the parent contract.
The child contract code in this example simply allows calling it with a string argument and it will reverse and return the string.
This example is not production code and is for educational purposes only. Many security checks are not included.
The parent smart contract supports three ABI methods (
delete). These three methods can be used to first create the child contract, possibly update the source for the child contract and finally delete the child contract. Their respective ABI method descriptors are shown below.
Deploy ABI method
The deploy method call takes a payment transaction as the first parameter, which is used to cover the minimum balance required by the parent contract to create the child contract. Note that this will be dependent on the amount of state the child contract uses. The next two parameters contain the compiled bytes for the approval and clear programs for the smart contract. Finally, the method should return an integer containing the app ID of the newly created child contract.
Update ABI method
The update method takes the application of the child contract as the first parameter. The next two parameters contain the compiled bytes for the updated approval and clear programs. The method will return true if the update is successful.
Destroy ABI method
The destroy method only takes the application id of the child contract to delete. If successful the method returns true.
Implementing the Parent Contract
The following section explains how the parent contract is implemented.
Before implementing each of the methods we need some initial code to setup the parent contract.
#pragma version 6 txn ApplicationID bz handle_setup txn OnCompletion int UpdateApplication == bnz handle_update
First the application ID of the parent contract is checked to see if it has been created yet. This just effectively tells us if the parent contract has already been deployed. If this is the first time the code has been executed (ApplicationID will be 0) we branch to the
OnCompletion transaction property is checked to see if the transaction is an update to the parent smart contract. If so we branch to the
The next part of the contract is specifically used to route specific ABI methods to the appropriate code to execute.
method "deploy(pay,byte,byte)uint64" txn ApplicationArgs 0 == bnz method_deploy method "update(application,byte,byte)bool" txn ApplicationArgs 0 == bnz method_update method "destroy(application)bool" txn ApplicationArgs 0 == bnz method_destroy err handle_setup: int 1 return handle_update: txn Sender global CreatorAddress == return
When using the ABI we rely on method descriptors, to facilitate checking against these descriptors the
method TEAL opcode can be used. With ABI methods, the descriptor is placed in the first application argument. This allows comparing this argument to the exact descriptor defined in the ABI method specification. The above code is looking to branch on either
update’, ordestroy`. Finally in the above code the setup or update from the initial code is handled. In the case of setup (i.e. when we first deploy the parent contract) the contract just returns with 1 on top of the stack, allowing the transaction to be successful. The update code handles the situation when the parent contract code is being updated. This code simply checks that the code can only be updated by the account that originally created the contract.
The deploy method implementation
The actual implementation of the deploy method is implemented with the following code. This code expects the first transaction in the group to be a payment transaction that funds the parent smart contract, so it can create the child contract.
method_deploy: // Check sender is funding the application with // enough Algo to deploy a smart contract. txn GroupIndex int 1 - dup gtxns Receiver global CurrentApplicationAddress == assert gtxns Amount int 100000 >= assert
This code gets the current position index of the application call within the transaction group. Next it subtracts 1 because the payment transaction precedes the application call by 1 position. The
dup command simply makes a copy of the final number, which should leave two integers on top of the stack. Both of these are equal and represent the position of the payment transaction in the group. The code then uses the top most integer to reference into the transaction group (using
gtxns opcode) and look at the receiver of the previous transaction. It should be the address of the parent smart contract. If not, the code will fail at the
assert opcode. This code will leave the final integer on the top of the stack that represents the previous transaction in the group. Using this index the
gtxns opcode is used again to look up the last transaction’s amount value. If this is greater than or equal to 100000 microalgos the code will continue. If not the code will fail at the second
Next the inner transaction is created to deploy the child contract.
// Begin the inner transaction to deploy the new smart contract // using the approval and clear programs passed in as arguments. itxn_begin int appl itxn_field TypeEnum int NoOp itxn_field OnCompletion // Get the length and extract it, removing the first 2 bytes. txn ApplicationArgs 1 dup len int 2 swap substring3 itxn_field ApprovalProgram // Get the length and extract it, removing the first 2 bytes. txn ApplicationArgs 2 dup len int 2 swap substring3 itxn_field ClearStateProgram int 0 itxn_field Fee itxn_submit
This code uses the
itxn opcodes to setup and transmit the inner transaction. See the developer documentation for more details on inner transactions.
This code first sets the type of transaction to be an application transaction, sets the sub type of application transaction to be a
NoOp, which is a general call to a smart contract, sets the approval program property to the bytes stored in the second argument, sets the clear program property to the bytes stored in the third argument, sets the transaction fee for the inner transaction to 0 (this means the transaction calling this method in the parent contract must pay the fee for the inner transaction), and then submits the transaction.
One important note here is that when the ABI encodes byte array parameters, the length of the byte array is stored in the first two bytes of the array. To deploy the child contract we need to strip off those first two bytes. This code handles that operation.
txn ApplicationArgs 1 dup len int 2 swap substring3
If the submit fails, the parent application call will fail. If its successful, the code handles the ABI return type. In this case its an integer containing the value of the created child contract.
// Using the ARC4 return string, concat the newly created appID. byte 0x151f7c75 itxn CreatedApplicationID itob concat log int 1 return
When using the ABI, return types are prepended with a set of bytes
0x151f7c75. In the above code these bytes are placed on top of the stack and the
itxn CreatedApplicationID opcode is used to get the child contract’s application ID. This is first converted to bytes using
itob and concatenated (
concat opcode) to the above bytes. The
log opcode logs the result to the completed transaction. The SDKs or goal look for the prepended bytes in the logs to extract the result of the method call. Finally a 1 is pushed to the strack and the
return opcode is used to exit the method successfully.
The update method implementation
This method is used by the parent contract to update the smart contract code of the child contract. The update method is almost identical to the deploy method. The primary differences are that the application ID of the child contract is placed is in the second argument, and the approval and clear programs are in the third and forth argument. This method also returns a true value when successful, instead of the application ID of the child contract. The sub application transaction type is also set to
UpdateApplication, which is an application transaction to update a smart contract, instead of
method_update: itxn_begin int appl itxn_field TypeEnum int UpdateApplication itxn_field OnCompletion txn ApplicationArgs 1 btoi txnas Applications itxn_field ApplicationID txn ApplicationArgs 2 dup len int 2 swap substring3 itxn_field ApprovalProgram txn ApplicationArgs 3 dup len int 2 swap substring3 itxn_field ClearStateProgram int 0 itxn_field Fee itxn_submit byte 0x151f7c7580 log int 1 return
When passing an application ID as an ABI argument, the SDKs and goal automatically put this application ID in the applications array. This array limits what additional contracts can be examined or called within the current application. For more information on smart contract arrays, see the developer documentation. This code reads the application array from the call to the parent contract and sets the
ApplicationID inner transaction property.
txn ApplicationArgs 1 btoi txnas Applications itxn_field ApplicationID
This property determines what contract to call in the inner transaction.
This code uses the same return code as in the deploy method but instead of concatenating the new child application ID, it adds
0x80 to the return bytes. This effectively signals the ABI a return type of true for boolean return values.
byte 0x151f7c7580 log int 1 return
The destroy method implementation
This method is called to delete a child smart contract. This method has many of the same fields as the previous two methods. The application sub-type is set to
DeletApplication, which is used to delete a smart contract. After deleting the child contract the parent contract also issues a payment transaction to return the original 10000 micro Algos deposited during the deploy method call. The
TypeEnum property of the inner transaction is set to
pay instead of
appl to indicate a payment transaction. The sender defaults to the parent contract address, the receiver is set to the sender of the destroy method caller and the amount is set to 10000 micro Algos.
method_destroy: itxn_begin int appl itxn_field TypeEnum int DeleteApplication itxn_field OnCompletion txn ApplicationArgs 1 btoi txnas Applications itxn_field ApplicationID int 0 itxn_field Fee Itxn_submit //return the original deposit itxn_begin int pay itxn_field TypeEnum txn Sender itxn_field Receiver int 100000 itxn_field Amount int 0 itxn_field Fee itxn_submit byte 0x151f7c7580 log int 1 return
After issuing the two transactions, if they complete successfully, a true value is returned similar to the update method. If either of these fail all transactions including the original parent call all fail.