Skip to content

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
}

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
}

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()
}

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()
}

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()
}

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()
}

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()
}

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()
}

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]
}

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')
}

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
}