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
Important
Edit the demo.sh script and set the appropriate location of your sandbox
executable.
SANDBOX="$HOME/sandbox/sandbox"
Run the script.
./demo.sh
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.
Note
This example is not production code and is for educational purposes only. Many security checks are not included.
Code Explanation
The parent smart contract supports three ABI methods (deploy
, update
, and 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
deploy(pay,byte[],byte[])uint64
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
update(application,byte[],byte[])bool
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
destroy(application)bool
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.
Setup Implementation
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 handle_setup
label.
Next 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 handle_update
label.
Routing Implementation
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 deploy
, update’, or
destroy`. 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 assert
.
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 NoOp
.
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.