Linking Algorand Stateful and Stateless Smart Contracts
Article Quick Glance
Algorand has two layer-1 smart contract types
- Stateless Smart Contracts are used to approve spending or asset transfer transactions and often govern all transactions from a specific account.
- Stateful Smart Contracts implement application logic and can store and update on chain values.
Use both contract types when your dApp combines application logic and user state with the handling of funds or assets. Examples include
- When a global value is required before approving escrow payments. For example, a tracking application that stores location on the blockchain.
- When dates or specific rounds are specified to restrict transaction times, for example a voting application.
- When ssset or balance information from the ledger is required to approve transactions, for example a trading application that requires a specific token to participate.
- When filling orders fully or partially is required, for example a digital exchange.
- and many more…
- Create and deploy a Stateful Smart Contract that stores a global variable placeholder for the Stateless Contract Address.
- Add logic to a Stateless Smart Contract that requires two transactions be submitted at the same time (Atomic Transfer) and verify the first is an application call to the Stateful Smart Contract deployed in the previous step.
- Compile and retrieve the address for the Stateless Contract.
- Update the Stateful Smart Contract to store the Stateless Contract’s address as a global variable.
Algorand currently has two types of layer-1 smart contracts, stateful and stateless. When building applications on the Algorand Blockchain, these two contracts perform very different functions. While each of these functions working stand-alone allow developers to build some amazing applications, often it is required that these two types of contracts work in conjunction to solve some type of problem. This type of problem generally occurs when you want to store some persistent data about a user or some global data in conjunction with performing some type of spending transaction. For example, with a crowdfunding application, when users donate (spending transaction) to a fund, you also want to increment a fund total. You may also want to store how much an individual gave; in case you need to return the funds to the user at a later date. This article will cover the basic concept of linking the two types of smart contracts within your applications to make this possible.
Before getting into combining the two types of contracts, a brief overview of each type of contract is in order or if you are already familiar with Algorand contract types, skip ahead to an example of linking contracts.
Stateless Smart Contracts
Stateless smart contracts are described extensively in the developer documentation. Their primary function is to approve spending transactions with logic. Algorand transactions are signed with either a user’s signature, a multi-signature or with a stateless smart contract’s logic.
The type of required signature depends on the “from” attribute of the transaction. In the case of a normal Algorand account the “from” will be a simple address. In this case a normal signature is required. If the “from” attribute is a multi-signature account, the transaction needs to be signed by one or more individual Algorand accounts that make up the multi-signature.
Stateless smart contracts provide two additional ways transactions can be signed with logic. The first is often referred to as an escrow or contract account where a stateless smart contract is written and compiled. The compiled contract generates an Algorand address. This address can be funded with Algos or Assets with simple transactions where the escrow address is the “to” attribute of the transaction. Once funded, anyone can submit a transaction with the “from” attribute of the transaction set to the address of the escrow and sign with the stateless smart contract. The transaction will initiate the Algorand blockchain to run the logic in the escrow account and based on the results of running that logic, will approve or deny the transaction.
The second way stateless smart contracts can be used to sign a transaction is by using them in a delegated manner. This simply means a stateless smart contract is written and an individual or multi-signature account signs the logic . This signed logic can then be used at a later date to submit a transaction on behalf of the account or multi-signature account to retrieve either Algos or Assets from the original signing account(s). The logic is checked once the transaction is submitted. For example, you may sign logic that says your mortgage company can take X Algos from your account once a month. The mortgage company could then submit a transaction once a month, where your account is the “from” attribute of the transaction and sign that transaction with the previously signed logic. This signed logic, is referred to as an Algorand LogicSig and may be persisted or kept in memory of an application. It is often wise to expire the LogicSig by putting a maximum round in which they can be used in the smart contract code. Many additional checks should be used here as well, including transaction fee, the recipient’s address, verify no Rekeying is occurring, etc. See the developer documentation for more details on critical checks within the smart contract logic.
Stateful Smart Contracts
Stateful smart contracts primarily function as state holding contracts. They store on chain values for the contract globally or for individual accounts that have opted into the smart contract. In this sense, these contracts live on the blockchain and can be initiated by anyone. These contracts are called by individuals with stateful smart contract transactions. These transactions initiate the logic stored in the stateful smart contract. The contract processes the logic and either succeeds or fails depending on the values passed to the contract and the logic within the contract. Using the crowdfunding example, the total amount raised may be stored globally for the contract and an individual account’s giving may be stored (locally) in the user’s ledger balance. If the logic passes, all the state whether local or globally that was changed by the logic will be finalized in the ledger. If the logic fails, no state changes will be recorded to the ledger. For example, suppose your crowdfunding application has a minimum giving amount. The logic may check how much an account is trying to give. If the amount is to low the logic will reject the stateful smart contract transaction. If it is above the minimum it will pass the transaction and finalize state changes.
Linking Stateful Smart Contracts with Payment Transactions
Stateful smart contracts do not in and of themselves approve spending transactions, just stateful smart contract transactions. They can be linked with spending transactions though and effectively approve or reject a spending transaction. This is done by using Algorand’s Atomic transfer capability. This feature allows up to 16 transactions to be submitted simultaneously and all transactions must succeed or fail. If any one of the transactions fail, they all fail. This powerful feature allows stateful smart contracts to effectively reject a spending transaction. In fact, when using the Atomic feature, either stateless or stateful smart contracts can interrogate all the transaction properties of any transaction in the group. Using the crowdfunding example, assume an address has been set up to receive all the donations. To donate a user simply sends a payment transaction to this address. If used in conjunction with a stateful smart contract, the contract can interrogate the payment transaction values. The contract could check the amount of the spending transaction and verify it is above the minimum giving amount. If the logic fails, the payment transaction will also fail because of the atomic transfer.
The TEAL code to implement this type of check may look something like the following.
gtxn 1 Amount int 10000 >= return
Linking Stateful Smart Contracts with Stateless Smart Contracts
As seen in the previous section, linking stateful smart contracts with payment transactions can be quite powerful. Using Atomic transfers, many different types of transactions can be grouped including both stateful and stateless smart contracts. For example, suppose in the crowdfunding example, the fund account shown in the previous example was an escrow stateless smart contract. Donations are held in this escrow account until the fund-raising period is over. The end date for the fund is stored in a stateful contract’s global state. This means that you don’t want funds to leave the escrow until after the fund end date has passed. Ideally there will be additional checks you want to make, but in the interest of simplicity, lets assume this is the only check. The escrow does not have access to the end date that is stored in the global state of the stateful smart contract. This can be circumvented by adding logic to the stateless escrow to verify that the stateful smart contract is called at the same time someone is trying to transfer funds out of the escrow. The stateful smart contract could then check the date.
To do this, the stateful smart contract needs to know about the escrow stateless contract and the escrow needs to know about the stateful smart contract. There are several ways this can be done. When you deploy a stateful smart contract, it generates a unique id for the stateful smart contract. This application id can be checked in the escrow stateless contract with logic similar to the following.
// Portion of escrow stateless contract logic // This contract only spends out // if two transactions are grouped global GroupSize int 2 == // The first transaction must be // an ApplicationCall (ie call stateful smart contract) gtxn 0 TypeEnum int appl == && // The specific App ID must be called // This should be changed after creation gtxn 0 ApplicationID int 12345 //App id of the stateful smart contract == && // The second transaction must be a payment transaction gtxn 1 TypeEnum int pay == &&
From the above code you can see that the logic will fail unless any transaction from the escrow is grouped with a stateful smart contract call and the application id must be the specific stateful smart contract we are expecting. Additionally, the second transaction must be a payment transaction. This means the stateful smart contract must be deployed before the escrow code can be complete.
To link the escrow to the stateful smart contract, similar code needs to be added to the stateful contract code. As discussed earlier, stateless escrow accounts can be identified by their address after compiling the contract. The address can be checked in the stateful smart contract. This presents a problem because the escrow contract address will not be known until after the stateful smart contract is deployed. One way around this is to compile the escrow and pass the address to the stateful smart contract to store in global state. Then the logic in the stateful smart contract could check that value as shown below.
// This stateful contract will only approve stateful transaction // if two transactions are grouped global GroupSize int 2 == // The first transaction must be // an ApplicationCall (ie call stateful smart contract) gtxn 0 TypeEnum int appl == && // The second transaction must be a payment transaction gtxn 1 TypeEnum int pay == && // verify the sender // of the payment transaction // is the escrow account gtxn 1 Sender byte "Escrow" app_global_get == && . . // check that we are past fund close date // assume the fund close date was stored earlier as well global LatestTimestamp byte "FundCloseDate" app_global_get >= &&
To get the escrow address into the global state of the stateful smart contract, you could add logic to the stateful smart contract to store the escrow address. In fact you could just use the update stateful smart contract transaction. The escrow would be passed as an argument to the transaction.
// check if this is update --- int UpdateApplication txn OnCompletion == bz not_update // the call should pass the escrow // address txn NumAppArgs int 1 == bz failed // store the address in global state // this parameter should be addr: byte "Escrow" txna ApplicationArgs 0 app_global_put int 1 return
This will effectively link both contracts and fail either if the other is not present when the transactions are submitted.