Inner Transactions | Algorand Developer Portal
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.
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.
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
*/
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
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
*/
public fungibleAssetCreate(): uint64 {
const itxnResult = itxn
.assetConfig({
total: 100_000_000_000,
decimals: 2,
unitName: 'RP',
assetName: 'Royalty Points',
})
.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
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.
* @param asset The asset to transfer
* @param receiver The account to receive the asset
* @param amount The amount to transfer
*/
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()
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
*/
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()
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.
* @param asset The asset to revoke
* @param accountToBeRevoked The account to revoke the asset from
* @param amount The amount to revoke
*/
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()
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.
* @param asset The asset to reconfigure
*/
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()
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.
* @param asset The asset to delete
*/
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.
/**
* 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
*/
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.
- An application may issue up to 256 inner transactions to increase its budget.
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.
/**
* Demonstrates calling methods on another application
* @param appId The application to call
* @returns A string result from the hello world call
*/
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)
return first_hello_world_result
Deploy smart contract via inner transaction
Smart contracts can dynamically create and deploy other smart contracts using inner transactions.