Inner Transactions
What are Inner Transactions?
When a smart contract is deployed to the Algorand blockchain, it is assigned a unique identifier called the App ID. Additionally, every smart contract has an associated unique Algorand account.
We call these accounts application accounts, and their unique identifier is a 58-character long public key known as the application address. The account allows the smart contract to function as an escrow account, which can hold and manage Algorand Standard Assets (ASA) and send transactions just like any other Algorand account.
The transactions sent by the smart contract (application) account are called Inner Transactions.
Inner Transaction Details
Since application accounts are Algorand accounts, they need Algo to cover transaction fees when sending inner transactions. To fund the application account, any account in the Algorand network can send Algo to the specified account. For funds to leave the application account, the following conditions must be met:
- The logic within the smart contract must submit an inner transaction.
- The smart contract’s logic must return true.
A smart contract can issue up to 256 inner transactions with one application call. If any of these transactions fail, the smart contract call will also fail.
Inner transactions support all the same transaction types that a regular account can make, including:
- Payment
- Key Registration
- Asset Configuration
- Asset Freeze
- Asset Transfer
- Application Call
- State Proof
You can also group multiple inner transactions and atomically execute them. Refer to the code example below for more details.
Inner transactions are evaluated during AVM execution, allowing changes to be visible within the contract. For example, if the balance
opcode is used before and after submitting a pay
transaction, the balance change would be visible to the executing contract.
Inner transactions also have access to the Sender
field. It is not required to set this field as all inner transactions default the sender to the contract address. If another account is rekeyed to the smart contract address, setting the sender to the address that has been rekeyed allows the contract to spend from that account. The recipient of an inner transaction must be in the accounts array. Additionally, if the sender of an inner transaction is not the contract, the sender must also be in the accounts array.
Clear state programs do not support creating inner transactions. However, clear state programs can be called by an inner transaction.
Paying Inner Transaction Fees
By default, fees for Inner Transactions are paid by the application account—NOT the smart contract method caller—and are set automatically to the minimum transaction fee.
However, for many smart contracts, this presents an attack vector in which the application account could be drained through repeated calls to send Inner Transactions that incur fee costs. The recommended pattern is to hard-code Inner Transaction fees to zero. This forces the app call sender to cover those fees through increased fees on the outer transaction through fee pooling. Fee pooling enables the application call to a smart contract method to cover the fees for inner transactions or any other transaction within an atomic transaction group.
Payment
Smart contracts can send Algo payments to other accounts using payment inner transactions. The following example demonstrates how to create a payment inner transaction while ensuring the app call sender covers the transaction fees through fee pooling.
/** * Demonstrates a simple payment inner transaction. * The fee is set to 0 by default. Manually set here for demonstration purposes. * The `Sender` for the payment is implied to be Global.currentApplicationAddress. * If a different sender is needed, it'd have to be an account that has been * rekeyed to the application address. * @returns The amount of the payment */ @abimethod() public payment(): uint64 { const result = itxn .payment({ amount: 5000, receiver: Txn.sender, fee: 0, }) .submit()
return result.amount }
@abimethod() def payment(self) -> UInt64: result = itxn.Payment(amount=5000, receiver=Txn.sender, fee=0).submit() return result.amount
""" fee is set to 0 by default. Manually set here for demonstration purposes. The `Sender` for the above is implied to be Global.current_application_address().
If a different sender is needed, it'd have to be an account that has been rekeyed to the application address. """
Asset Create
Assets can be created by a smart contract. Use the following contract code to create an asset with an inner transaction.
/** * Creates a fungible asset (token) * @returns The ID of the created asset */ @abimethod() public fungibleAssetCreate(): uint64 { const itxnResult = itxn .assetConfig({ total: 100_000_000_000, decimals: 2, unitName: 'RP', assetName: 'Royalty Points', }) .submit()
return itxnResult.createdAsset.id }
/** * Creates a non-fungible asset (NFT). * Following the ARC3 standard, the total supply must be 1 for a non-fungible asset. * If you want to create fractional NFTs, `total` * `decimals` point must be 1. * ex) total=100, decimals=2, 100 * 0.01 = 1 * The fee is set to 0 by default for inner transactions. * The Sender is implied to be Global.currentApplicationAddress. * @returns The ID of the created asset */ @abimethod() public nonFungibleAssetCreate(): uint64 { const itxnResult = itxn .assetConfig({ total: 100, decimals: 2, unitName: 'ML', assetName: 'Mona Lisa', url: 'https://link_to_ipfs/Mona_Lisa', manager: Global.currentApplicationAddress, reserve: Global.currentApplicationAddress, freeze: Global.currentApplicationAddress, clawback: Global.currentApplicationAddress, fee: 0, }) .submit()
return itxnResult.createdAsset.id }
@abimethod def fungible_asset_create(self) -> UInt64: itxn_result = itxn.AssetConfig( total=100_000_000_000, decimals=2, unit_name="RP", asset_name="Royalty Points", ).submit()
return itxn_result.created_asset.id
@abimethod def non_fungible_asset_create(self) -> UInt64: """ Following the ARC3 standard, the total supply must be 1 for a non-fungible asset. If you want to create fractional NFTs, `total` * `decimals` point must be 1. ex) total=100, decimals=2, 100 * 0.01 = 1 """ itxn_result = itxn.AssetConfig( total=100, decimals=2, unit_name="ML", asset_name="Mona Lisa", url="https://link_to_ipfs/Mona_Lisa", manager=Global.current_application_address, reserve=Global.current_application_address, freeze=Global.current_application_address, clawback=Global.current_application_address, ).submit()
return itxn_result.created_asset.id
Asset Opt In
If a smart contract wishes to transfer an asset it holds or needs to opt into an asset, this can be done with an asset transfer inner transaction. If the smart contract created the asset via an inner transaction, it does not need to opt into the asset.
/** * Opts the application into an asset. * A zero amount asset transfer to one's self is a special type of asset transfer * that is used to opt-in to an asset. * To send an asset transfer, the asset must be an available resource. * @param asset The asset to opt into */ @abimethod() public assetOptIn(asset: Asset): void { itxn .assetTransfer({ assetReceiver: Global.currentApplicationAddress, xferAsset: asset, assetAmount: 0, fee: 0, }) .submit() }
@abimethod def asset_opt_in(self, asset: Asset) -> None: itxn.AssetTransfer( asset_receiver=Global.current_application_address, xfer_asset=asset, asset_amount=0, fee=0, ).submit()
""" A zero amount asset transfer to one's self is a special type of asset transfer that is used to opt-in to an asset.
To send an asset transfer, the asset must be an available resource. Refer the Resource Availability section for more information. """
Asset Transfer
If a smart contract is opted into the asset, it can transfer the asset with an asset transfer transaction.
/** * Transfers an asset from the application to another account. * For a smart contract to transfer an asset, the app account must be opted into the asset * and be holding non zero amount of assets. * To send an asset transfer, the asset must be an available resource. * @param asset The asset to transfer * @param receiver The account to receive the asset * @param amount The amount to transfer */ @abimethod() public assetTransfer(asset: Asset, receiver: Account, amount: uint64): void { itxn .assetTransfer({ assetReceiver: receiver, xferAsset: asset, assetAmount: amount, fee: 0, }) .submit() }
@abimethod def asset_transfer(self, asset: Asset, receiver: Account, amount: UInt64) -> None: itxn.AssetTransfer( asset_receiver=receiver, xfer_asset=asset, asset_amount=amount, fee=0, ).submit()
""" For a smart contract to transfer an asset, the app account must be opted into the asset and be holding non zero amount of assets.
To send an asset transfer, the asset must be an available resource. Refer the Resource Availability section for more information. """
Asset Freeze
A smart contract can freeze any asset, where the smart contract is set as the freeze address.
/** * Freezes an asset for a specific account. * To freeze an asset, the asset must be a freezable asset * by having an account with freeze authority. * @param acctToBeFrozen The account to freeze the asset for * @param asset The asset to freeze */ @abimethod() public assetFreeze(acctToBeFrozen: Account, asset: Asset): void { itxn .assetFreeze({ freezeAccount: acctToBeFrozen, // account to be frozen freezeAsset: asset, frozen: true, fee: 0, }) .submit() }
@abimethod def asset_freeze(self, acct_to_be_frozen: Account, asset: Asset) -> None: itxn.AssetFreeze( freeze_account=acct_to_be_frozen, # account to be frozen freeze_asset=asset, frozen=True, fee=0, ).submit()
""" To freeze an asset, the asset must be a freezable asset by having an account with freeze authority. """
Asset Revoke
A smart contract can revoke or clawback any asset where the smart contract address is specified as the asset clawback address.
/** * Revokes (clawbacks) an asset from an account. * To revoke an asset, the asset must be a revocable asset * by having an account with clawback authority. * The Sender is implied to be current_application_address. * @param asset The asset to revoke * @param accountToBeRevoked The account to revoke the asset from * @param amount The amount to revoke */ @abimethod() public assetRevoke(asset: Asset, accountToBeRevoked: Account, amount: uint64): void { itxn .assetTransfer({ assetReceiver: Global.currentApplicationAddress, xferAsset: asset, assetSender: accountToBeRevoked, // AssetSender is only used in the case of clawback assetAmount: amount, fee: 0, }) .submit() }
@abimethod def asset_revoke( self, asset: Asset, account_to_be_revoked: Account, amount: UInt64 ) -> None: itxn.AssetTransfer( asset_receiver=Global.current_application_address, xfer_asset=asset, asset_sender=account_to_be_revoked, # AssetSender is only used in the case of clawback asset_amount=amount, fee=0, ).submit()
""" To revoke an asset, the asset must be a revocable asset by having an account with clawback authority.
Sender is implied to be current_application_address """
Asset Configuration
As with all assets, the mutable addresses can be changed using contract code similar to the code below. Note these these addresses cannot be changed once set to an empty value.
/** * Reconfigures an existing asset. * For a smart contract to transfer an asset, the app account must be opted into the asset * and be holding non zero amount of assets. * To send an asset transfer, the asset must be an available resource. * Refer the Resource Availability section for more information. * @param asset The asset to reconfigure */ @abimethod() public assetConfig(asset: Asset): void { itxn .assetConfig({ configAsset: asset, manager: Global.currentApplicationAddress, reserve: Global.currentApplicationAddress, freeze: Txn.sender, clawback: Txn.sender, fee: 0, }) .submit() }
@abimethod def asset_config(self, asset: Asset) -> None: itxn.AssetConfig( config_asset=asset, manager=Global.current_application_address, reserve=Global.current_application_address, freeze=Txn.sender, clawback=Txn.sender, fee=0, ).submit()
""" For a smart contract to transfer an asset, the app account must be opted into the asset and be holding non zero amount of assets.
To send an asset transfer, the asset must be an available resource. Refer the Resource Availability section for more information. """
Asset Delete
Assets managed by the contract can also be deleted. This can be done with the following contract code. Note that the entire supply of the asset must be returned to the contract account before deleting the asset.
/** * Deletes an asset. * To delete an asset, the asset must be a deleteable asset * by having an account with delete authority. * The Sender is implied to be current_application_address. * @param asset The asset to delete */ @abimethod() public assetDelete(asset: Asset): void { itxn .assetConfig({ configAsset: asset, fee: 0, }) .submit() }
@abimethod def asset_delete(self, asset: Asset) -> None: itxn.AssetConfig( config_asset=asset, fee=0, ).submit()
Grouped Inner Transactions
A smart contract can make inner transactions consisting of multiple transactions grouped together atomically. The following example groups a payment transaction with a call to another smart contract.
/** * Demonstrates grouped inner transactions * @param appId The application to call * @returns A tuple containing the payment amount and the result of the hello world call */ @abimethod() public multiInnerTxns(appId: Application): [uint64, string] { // First payment transaction const payTxn = itxn .payment({ amount: 5000, receiver: Txn.sender, fee: 0, }) .submit()
// Second application call transaction const appCallTxn = itxn .applicationCall({ appId: appId.id, appArgs: [arc4.methodSelector('sayHello(string,string)string'), new arc4.Str('Jane'), new arc4.Str('Doe')], fee: 0, }) .submit()
// Get result from the log of the app call const helloWorldResult = arc4.decodeArc4<string>(appCallTxn.lastLog, 'log') return [payTxn.amount, helloWorldResult] }
@abimethod def multi_inner_txns(self, app_id: Application) -> tuple[UInt64, arc4.String]: payment_params = itxn.Payment(amount=5000, receiver=Txn.sender, fee=0)
app_call_params = itxn.ApplicationCall( app_id=app_id, app_args=(arc4.arc4_signature("hello(string)string"), arc4.String("World")), fee=0, )
pay_txn, app_call_txn = itxn.submit_txns(payment_params, app_call_params)
hello_world_result = arc4.String.from_log(app_call_txn.last_log) return pay_txn.amount, hello_world_result
Contract to Contract Calls
A smart contract can also call another smart contract method with inner transactions. However there are some limitations when making contract to contract calls.
- An application may not call itself, even indirectly. This is referred to as re-entrancy and is explicitly forbidden.
- An application may only call into other applications up to a stack depth of 8. In other words, if app calls (->) look like
1->2->3->4->5->6->7->8
, App 8 may not call another application. This would violate the stack depth limit. - An application may issue up to 256 inner transactions to increase its budget (max budget of 179.2k even for a group size of 1), but the max call budget is shared for all applications in the group. This means you can’t have two app calls in the same group that both try to issue 256 inner app calls.
- An application of AVM version 6 or above may not call contracts with a AVM version 3 or below. This limitation protects an older application from unexpected behavior introduced in newer AVM versions.
A smart contract can call other smart contracts using any of the OnComplete types. This allows a smart contract to create, opt in, close out, clear state, delete, or just call (NoOp) other smart contracts. To call an existing smart contract the following contract code can be used.
NoOp Application call
A NoOp application call allows a smart contract to invoke another smart contract’s logic. This is the most common type of application call used for general-purpose interactions between contracts.
/** * Demonstrates calling methods on another application * @param appId The application to call * @returns A string result from the hello world call */ @abimethod() public noopAppCall(appId: Application): string { // First application call - invoke an ABI method const callTxn = itxn .applicationCall({ appId: appId.id, appArgs: [arc4.methodSelector('sayHello(string,string)string'), new arc4.Str('John'), new arc4.Str('Doe')], }) .submit()
// Extract result from the log return arc4.decodeArc4<string>(callTxn.lastLog, 'log') }
@abimethod def noop_app_call(self, app_id: Application) -> tuple[arc4.String, String]: # invoke an ABI method call_txn = itxn.ApplicationCall( app_id=app_id, app_args=(arc4.arc4_signature("hello(string)string"), arc4.String("World")), ).submit() # extract result first_hello_world_result = arc4.String.from_log(call_txn.last_log)
# OR, call it automatic ARC4 encoding, type validation and result handling second_hello_world_result, call_txn = arc4.abi_call( # declare return type HelloWorld.hello, # method signature to call "again", # abi method arguments app_id=app_id, )
return first_hello_world_result, second_hello_world_result
Deploy smart contract via inner transaction
Smart contracts can dynamically create and deploy other smart contracts using inner transactions. This powerful feature enables contracts to programmatically spawn new applications on the blockchain.
/** * HelloWorld class is a contract class defined in a different file. * It would be imported in a real implementation: * * import HelloWorld from '../HelloWorld/contract.algo' */
/** * Deploys a HelloWorld contract using direct application call * * This method uses the itxn.applicationCall to deploy the HelloWorld contract. * @returns The ID of the deployed application */ @abimethod() public deployApp(): uint64 { // In a real implementation, we would compile the HelloWorld contract // This is a placeholder implementation that mocks the bytecode const appTxn = itxn .applicationCall({ approvalProgram: Bytes('approval_program'), clearStateProgram: Bytes('clear_state_program'), fee: 0, }) .submit()
return appTxn.createdApp.id }
/** * Deploys a HelloWorld contract using arc4 * * This method uses arc4 to deploy the HelloWorld contract. * @returns The ID of the deployed application */ @abimethod() public arc4DeployApp(): uint64 { // In a real implementation, we would use the SDK to create the app // This is a mock implementation that returns a hardcoded ID
/** @TODO is this implemented in puya-ts? * app_txn = arc4.arc4_create(HelloWorld) */ return 1234 }
""" HelloWorld class is a contract class defined in a different file. It is imported in the beginning of this file.
from ..hello_world.contract import HelloWorld """
@abimethod def deploy_app(self) -> UInt64: """ This method uses the itxn.ApplicationCall to deploy the HelloWorld contract. """ compiled_contract = compile_contract(HelloWorld)
app_txn = itxn.ApplicationCall( approval_program=compiled_contract.approval_program, clear_state_program=compiled_contract.clear_state_program, fee=0, ).submit() app = app_txn.created_app
return app.id
@abimethod def arc4_deploy_app(self) -> UInt64: """ This method uses the arc4.arc4_create to deploy the HelloWorld contract. """ app_txn = arc4.arc4_create(HelloWorld)
return app_txn.created_app.id