Create Publication

We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Article Thumbnail

Contract to Contract calls and an ABI come to Algorand

Article Quick Glance

Algorand Virtual Machine (AVM) 1.1

  • The AVM now supports Contract to Contract calling and grouped inner transactions, with nesting up to 8 levels deep!
  • Algorand now has an Application Binary Interface (ABI) and full support in the SDKS to invoke methods without having to know the internals of the contract being called. The ABI now allows additional standards to be created on top of the ABI.
  • A new Atomic Composer is now available in the SDKs that allow building atomic groups in a simplified way and fully supports invoking ABI compliant contracts.
  • New AVM 1.1 opcodes.

In this release, developers will be outfitted with powerful tools to expand the functionality of their application models. With contract-to-contract support, applications can create or invoke other contracts on the network. Deployed smart contracts can now be discovered and invoked using the Application Binary Interface(ABI). Additionally, groups of transactions can be created in a simplified manner, allowing for a much more versatile way of invoking contracts that require additional transactions.

Check out the key features releasing in 1.1 below.

Contract to Contract Calls

In the last release, Algorand added the ability for smart contracts to issue “inner transactions”, where a smart contract can issue additional payment and asset transactions. This not only gave smart contracts the ability to own (as an escrow) and transfer Algos and ASAs, but also the ability to create ASAs that were wholly managed by the smart contract. In this release the capability has been expanded to include the ability of a smart contract to call or create another smart contract using inner transactions. This should allow many unique scenarios for application developers including creating dynamic and short lived contracts programmatically on layer 1 of the protocol. This will also allow one smart contract to selectively choose which secondary contract to call based on the initial contracts logic. Additionally, deeply nested (up to 8 levels) smart contract calls are allowed.

Calling A Smart Contract Using Inner Transaction

EditorImages/2022/02/23 18:39/c2c.png

With the release of Contract to Contract calls comes an improved model for transaction limits. In the prior release, you could issue 16 inner transactions per application transaction. This app transaction could be grouped atomically with additional app transactions to extend this limit. With this release, one atomic group of transactions (even if the group consists of only one transaction) can issue up to 256 inner transactions in total. This effectively offers the ability of one application transaction to not only issue 256 inner transactions but also use the cumulative pooled opcode budget of all 256 transactions. Where one application call has a 700 opcode budget, if all transactions are used, this amounts to an opcode budget in total of almost 180k. This allows developers to do some very sophisticated applications that link with many additional smart contracts that offload complex functions to utility-style contracts.

        InnerTxnBuilder.Begin(),
        InnerTxnBuilder.SetFields({
            TxnField.type_enum: TxnType.ApplicationCall,
            # specific app id to call
            TxnField.application_id: Int(1234),
            # Pass application arguments
            TxnField.application_args: [my first arg],
        }),
        InnerTxnBuilder.Submit(),

When using inner transactions, reentrant calls are not allowed. For example, in the above diagram (figure 1), if Contract A calls Contract B, Contract B can not call Contract A. Contract A can call Contract B multiple times, however.

All smart contract calls, whether they are called by an application transaction or an inner transaction, have a limited view of the ledger. This is done to optimize the network for speed. What can be viewed in the ledger (using various opcodes) is restricted by a set of arrays that are passed with each application transaction, whether it is a top-level application transaction or an inner transaction. These arrays are discussed in the documentation. These arrays consist of additional accounts, applications (other smart contracts), and assets the smart contract can interrogate. In addition to these arrays, the contract also has visibility to the sender of the transaction’s address, the application that called the current smart contract(if this is an inner transaction), and the asset id or application of any created asset or application in the current transaction group as long as the asset or application has already been created prior to executing the specific opcode. Each application also has a set of parameters as well, so when using inner transactions these values may also be passed to subsequent application transaction calls using these parameters.

Grouped Inner Transactions

Another new feature of inner transactions is the ability to create and submit atomically grouped transactions within a smart contract. These transactions like standard atomically grouped transactions either all succeed or all fail. This allows smart contracts to properly compose transaction groups required by other contracts before calling them. As with all inner transactions, any failure will result in the application transaction being rejected and any state changes within the contract will not be committed to the blockchain. To create a grouped inner transaction, developers should use the new itxn_next opcode. This opcode is used after first creating an inner transaction and signifies that another inner transaction is going to be grouped with previous inner transaction.

    itxn_begin
    int pay
    itxn_field TypeEnum
    txn Accounts 1
    itxn_field Receiver
    int 1
    itxn_field Amount

    itxn_next
    int pay
    itxn_field TypeEnum
    txn Accounts 1
    itxn_field Receiver
    int 2
    itxn_field Amount

    itxn_submit

The above TEAL code simply groups two payment transactions together. To do grouped inner transactions in PyTeal use the InnerTxnBuilder.Next() function as shown below.

        # Start building group
        InnerTxnBuilder.Begin(),
        # Set fields on first group
        InnerTxnBuilder.SetFields({
            TxnField.type_enum: TxnType.Payment,
            TxnField.receiver: Txn.accounts[acct_ref1],
            TxnField.amount: amt1,
            TxnField.fee: Int(0) # make caller cover fees 
        }),
        # Start building next txn in group
        InnerTxnBuilder.Next(),
        # Set fields on second group
        InnerTxnBuilder.SetFields({
            TxnField.type_enum: TxnType.Payment,
            TxnField.receiver: Txn.accounts[acct_ref2],
            TxnField.amount: amt2,
            TxnField.fee: Int(0) # make caller cover fees 
        }),
        # Send  group
        InnerTxnBuilder.Submit(),

For more information on inner transactions see the developer documentation.

Application Binary Interface (ABI)

Algorand smart contracts are very flexible on how methods in the contract can be called and how the results can be handled. While this model offers many advantages, one drawback is that any application that may want to make use of your contract has to know how your contract handles parameters and processes the results. This also means that many existing applications may have to be altered to support your specific smart contract.

To address this issue, Algorand has implemented a specific standard that defines calling a method and how to process both the input arguments and return values. This standard describes an application binary interface (ABI) that can be used to standardize smart contracts. The ABI is described in the ARC4 standard. This allows applications to define specific smart contract interactions that their application will support. For example, a wallet provider may implement a standard way of listing contract state values stored in a contract. If you want your application to work with this wallet, you can write your contract to implement the standard the wallet provides. If there is an ABI compliant existing smart contract that is already on the blockchain, new application developers can implement the standard implemented in the published contract to support it as well.

These interactions with smart contracts are written to JSON files containing the associated metadata. Currently, this includes three distinct JSON types. These three types are Method, Interface and Class. A Method interface defines one method with the expected arguments and return types.

{   
    "name":"add",
    "desc":"method description",
    "args":[{"type":"uint32","desc":"first description"},
            {"type":"uint16","desc":"second description"}],
    "returns":{"type":"uint32","desc":"return description"}
}

In the above example, the Method interface defines a method named add that takes two arguments of unsigned integers of 32 and 16 bytes respectively. The method returns an 32 byte unsigned integer.

An Interface description defines a collection of methods that must be implemented by a contract in order to adhere to some standard. This allows for the ABI compliant projects to implement new standards on top of ARC4 that further extend the developer ecosystem.

{
    "name":"interface",
    "methods":[
        {
            "name":"add",
            "args":[{"type":"uint32"},{"type":"uint32"}],
            "returns":{"type":"uint32"}
        },        
        {
            "name":"sub",
            "args":[{"type":"uint32"},{"type":"uint32"}],
            "returns":{"type":"uint32"}
        },

    ]
}

This Interface provides an example of two methods (add and sub) that need to be implemented in order to support this interface.

The Contract description defines a specific contract that currently exists on the blockchain, and specifies all the methods a contract implements. This can be thought of as a concrete implementation of an interface. In the previous Interface example, smart contracts can implement additional methods in addition to the ones defined and still considered compliant with that standard.

{
    "name":"demo-abi",
    "networks": {
        "wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=": { "appID": 1234 },
        "SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=": { "appID": 5678 },
    },

    "methods":[
        {
            "name":"add",
            "description":"Add 2 integers",
            "args":[ { "type":"uint64" }, { "type":"uint64" } ],
            "returns": {"type":"uint64"}
        },
        {
            "name":"sub",
            "description":"Subtract 2 integers",
            "args":[ { "type":"uint64" }, { "type":"uint64" } ],
            "returns": {"type":"uint64"}
        },
        {
            "name":"mul",
            "description":"Multiply 2 integers",
            "args":[ { "type":"uint64" }, { "type":"uint64" } ],
            "returns": {"type":"uint64"}
        }
    ]
}

In the above example, the specific network genesis hash and application id are specified in the JSON file. In addition, this Contract defines three methods that the specific contract provides, along with parameter and return value expectations. This contract is considered compliant with the previous example Interface.

When developing a new smart contract, developers can create these JSON files before deploying their contracts to the blockchain.

EditorImages/2022/02/23 18:52/abi1.png

In production, other applications can read these JSON files and then properly invoke your methods and process the return values.

EditorImages/2022/02/23 18:55/abi2.png

All of the Algorand SDKs have been upgraded to support consuming these JSON files and provide methods for invoking specific contract calls. For example the following JavaScript prints out all the methods in a contract.

    const buffC = fs.readFileSync("contract.json");
    const c1 = new algosdk.ABIContract( JSON.parse(buffC.toString()));
    console.log("Contract Name: " + c1.name);
    console.log("APP ID: " + c1.appId);
    for (let mc of c1.methods) {
        console.log("Method Name " + mc.name);
    } 

In order to easily invoke ABI compliant contract methods a new feature has also been added to all the SDKs. This is referred to as the Atomic Transaction Composer, which is described in the next section.

Atomic Transaction Composer

With this release, Algorand provides a new way to construct transactions and transaction groups. This feature is called the Atomic Transaction Composer and it also fully supports the ABI standard explained in the previous section. For example, to invoke the previous section’s add method the following JavaScript can be used.

    const buff = fs.readFileSync("../contract.json")
    const contract = new algosdk.ABIContract( JSON.parse(buff.toString()))

    function getMethodByName(name: string): algosdk.ABIMethod  {
        const m = contract.methods.find((mt: algosdk.ABIMethod)=>{ return mt.name==name })
        if(m === undefined)
            throw Error("Method undefined")
        return m
    }

    const sum       = getMethodByName("add")

    const sp = await client.getTransactionParams().do()
    const commonParams = {
        appId:contract.appId,
        sender:acct.addr,
        suggestedParams:sp,
        signer: algosdk.makeBasicAccountTransactionSigner(myAccount)
    }

    const comp = new algosdk.AtomicTransactionComposer()
    comp.addMethodCall({
        method: sum, methodArgs: [1,1], ...commonParams
    })
    comp.buildGroup()
    const result = await comp.execute(client, 2)

This example simply reads the Contract JSON file, finds the method add, and uses the Atomic Transaction Composer to invoke it. The composer processes the method arguments, builds a specific method signature to call the contract with, and processes the contract’s return value. The composer’s addMethodCall is designed to support invoking smart contracts. Additionally, standard transactions can use the composer as well. For example, to create a simple payment transaction with the composer the following code can be used.

    const comp = new algosdk.AtomicTransactionComposer()
    const receiver = "HZ57J3K46JIJXILONBBZOHX6BKPXEM2VVXNRFSUED6DKFD5ZD24PMJ3MVA";
    let amount = 1000000;
    let sender = acct.addr;
    let txn = algosdk.makePaymentTxnWithSuggestedParams(sender, 
receiver, amount, undefined, undefined, sp);
    let transactionWithSigner = {
        txn: txn,
        signer: algosdk.makeBasicAccountTransactionSigner(acct)
      };

    comp.addTransaction(transactionWithSigner)
    const group = comp.buildGroup()
    const result = await comp.execute(client, 2)
    const r = result
    console.log(r)

The composer’s addTransaction method allows any standard transaction to be added to the group. The composer supports one or many transactions and is now the default way to build atomic groups of transactions. For example, to build a group of transactions, developers can just call the addMethod or addTransaction composer method for each subsequent transaction.

    comp.addMethodCall({
        method: sum, methodArgs: [1,1], ...commonParams
    })
    comp.addMethodCall({
        method: sub, methodArgs: [3,1], ...commonParams
    })
    comp.addMethodCall({
        method: div, methodArgs: [4,2], ...commonParams
    })

The Atomic Composer also contains a state machine that allows the status of the atomic group to be checked at any point. The group can be in a state of building, built, signed, submitted, and committed. These represent the various phases that an atomic group goes through before being committed to the blockchain. In addition to the state machine, the composer is designed to allow addMethod calls that expect additional transactions to be available in a group of transactions, before calling a specific contracts method. Suppose we have a method called myCall that takes two integer parameters but requires that this call is grouped with a payment transaction. This would be implemented as follows with the new composer.

let txn = algosdk.makePaymentTxnWithSuggestedParams(
acct.addr, acct.addr, 10000, undefined, undefined, sp);
comp.addMethodCall({
       method: myCall,
       methodArgs: [
           {
               txn: txn,
               signer: algosdk.makeBasicAccountTransactionSigner(acct)
           },
           1,
           2
       ],
       ...commonParams
   })

Notice the first argument to the call is actually a payment transaction. This will result in the composer building a two transaction group with the first being the payment transaction and the second the application call and passing the two integer parameters.

For more information on the Atomic Composer, see the developer documentation.

AVM 1.1

With this release, Algorand has updated the AVM to version 1.1 (TEAL 6), with support for more opcodes. Several of these opcodes are used to better support contract to contract calling and grouped inner transactions, but additional utility opcodes have also been added.

Using the new global OpcodeBudget allows a contract to determine how many ops are still available for execution budget. This limit will be based on the number of application calls that are used in a specific group of transactions. As a contract may take multiple paths through the logic of any contract, this allows the code to determine if the code has enough budget left before executing some code that may take the contract over the limit, resulting in a failure. Using this in conjunction with logging, developers should be able to see where in their code, they may be close to the limits.

When an inner transaction is used to call another smart contract, the inner contract can use the two new globals CallerApplicationID and CalllerApplicationAddress to get the ID and address of the contract that invoked them.

The acct_params_get opcode is now available to allow developers to retrieve a specific account’s balance and minimum balance, which is based on the assets opted into and the number and storage of the contracts the account has opted into or created. For example, a contract may want to see if a specific account has at least 10 Algos free to spend. This can be done with the following TEAL.

    // Get the current Algo balance of the sender.
    int 0
    acct_params_get AcctBalance
    assert

    // Get the minimum balance of the sender.
    int 0
    acct_params_get AcctMinBalance
    assert

    // Subtract the sender minimum balance from their full balance, then check
    // it's greater than 10.000000 Algo.
    -
    int 10000000
    >
    assert

This same opcode can be used to retrieve the current accounts auth address. This address will be set to the ZeroAddress for accounts that have not been rekeyed. If the auth address is not the ZeroAddress, the account has been rekeyed to the auth address. To check this in code the following code can be used.

    // Get the current sender's auth address. If it's not set then a
    // ZeroAddress is returned.
    int 0
    acct_params_get AcctAuthAddr
    assert

    // Compare the returned address with the ZeroAddress.
    global ZeroAddress
    ==
    assert

When creating inner transaction groups, often a contract may need to look at specific transaction properties for transactions in this inner group. Two new opcodes gitxn and gitxna support getting the properties of the last inner transaction group.

For example if we have two payment transactions grouped similar to the first example in this article these two amounts could be added together using the following TEAL code.

    itxn_begin
    int pay
    itxn_field TypeEnum
    txn Accounts 1
    itxn_field Receiver
    int 1
    itxn_field Amount

    itxn_next
    int pay
    itxn_field TypeEnum
    txn Accounts 1
    itxn_field Receiver
    int 2
    itxn_field Amount

    itxn_submit
    gitxn 0 Amount
    +
    gitxn 1 Amount

Additional TEAL opcodes are available, so be sure to check out the opcodes documentation.

Conclusion

Many examples of doing contract-to-contract calls, grouped inner transactions, atomic composer transactions, and the new TEAL opcodes can be seen at our developer relations GitHub repository.