Skip to content

Composable Transactions

Bernardo Ramos edited this page Jul 21, 2024 · 15 revisions

Normal transactions can make only a single call to a smart contract.

This means that when we need to call a function many times, or interact with different smart contracts, we need a separate transaction for each smart contract interaction.

Composable Transactions bring the ability to make many calls in a single transaction, either to the same smart contract or to different ones.

Characteristics

  1. Multiple calls
  2. Atomicity
  3. Sequential execution
  4. Act on current state
  5. Scripting
  6. No blind signing

Atomicity

If any of the calls fail, the entire transaction is aborted.

This means that either all calls succeed or none of them.

Sequential Execution

Execute a series of contract calls with the guarantee that they happen in sequence order with no external action occurring between them.

Act on current state

Do actions using the current values from contract and account states (by making queries to contracts and using the returned value on the same transaction) instead of a previously hardcoded value acquired at transaction building time.

This is useful when another transaction arrives first and changes the contract state.

Scripting

Instead of just a list of calls, it is possible to process the returned value and act according to them.

The language supports data conversion and manipulation, assertion, conditional execution, loops, and more.

No Blind Signing

The transaction can be reviewed even on hardware wallets before approval or rejection

Use Cases

1) Purchase airplane tickets and hotel reservation from different contracts, only if both succeed. If some of the purchases fail, the other is reverted (the user does not want one of them if the other is not acquired).

2) Swap token and buy something with it. If the purchase fails, the user does not want that token.

3) Swap tokens using a split route. If one of the routes fail, revert the other(s). Also check the minimum output in the transaction itself (by adding individual outputs) and revert if not satisfied.

4) Swap to an exact amount of the output token when using a multi-hop route, by either: a. doing a reverse swap of the surplus amount, or b. querying a contract for the right amount to send (between approved range) in the same transaction.

5) Add liquidity to a pool (2 tokens) in a single transaction

6) Trustless swap or purchase - check if the purchased token was received, otherwise revert the transaction

7) Transferring tokens (fungible and/or non-fungible) to multiple recipients at the same time

8) Transferring different tokens (fungible and/or non-fungible) to one recipient in a single transaction

9) Mint many non-fungible tokens (NFTs) in a single transaction

10) Approve contract B to use contract A (token) and then call a function on contract B that handles the resources on contract A (eg: approve and swap, approve and add liquidity) on a single transaction

11) Approve, use resource, and remove approval on a single transaction, with the guarantee that no other operation would happen in between while the resource/token is approved to contract B

Implementation

The call information is stored in the transaction payload in JSON format.

You can view it as a list of calls processed in order:

[
  <call 1>
  <call 2>
  <call 3>
]

Each call containing the contract address, function to call, and arguments:

[
  [call, contract, function, arg1, arg2, arg3...],
  [call, contract, function, arg1, arg2, arg3...],
  [call, contract, function, arg1, arg2, arg3...]
]

Some Examples

Buy 2 products or services on a single transaction:

[
  ["call","<token 1>","transfer","<amount 1>","<shop 1>","buy","product 1"],
  ["call","<token 2>","transfer","<amount 2>","<shop 2>","buy","product 2"],
]

Acquire token2 via swap and buy a product/service with it:

[
  ["call","<token 1>","transfer","<amount 1>","<swap pair>","swap",{"exact_output":"<amount 2>"}],
  ["call","<token 2>","transfer","<amount 2>","<shop>","buy","product"],
]

Add liquidity to a swap pair:

[
  ["call","<token 1>","transfer","<amount 1>","<swap_pair>","store_token"],
  ["call","<token 2>","transfer","<amount 2>","<swap_pair>","add_liquidity"],
]

Variables

Another feature is the use of variables, in the format %name%

When a string in the above format is found, it is replaced by the corresponding value.

One hard-coded variable is the returned value from the last operation: %last result%

Example usage:

[
  ["call","<address 1>","function1","arg"],
  ["call","<address 2>","function2","%last result%"]
]

Scripting

This feature uses a new simple scripting language based on JSON.

The first element of each operation is a command. Here are some of them:

  • call
  • store (result)
  • assert

The call is a normal smart contract call

The store command is used to store the returned value in a temporary variable

The assert is used to ensure that a value is within expected bounds, otherwise the whole transaction is reverted

Example 1:

[
  ["call","<address>","function","arg"],
  ["assert","%last result%",">=","250"]
]

Example 2:

[
  ["call","<address 1>","function1","arg"],
  ["call","<address 2>","function2","arg"],
  ["assert","%last result%",">=","250"]
]

Example 3:

[
  ["call","<address 1>","function","arg"],
  ["store result as","result1"],
  ["call","<address 2>","function","arg"],
  ["assert","%last result%",">=","%result1%"]
]

Example 4:

[
  ["call","<address 1>","function1","arg"],
  ["store result as","result1"],
  ["call","<address 2>","function2","arg"],
  ["store result as","result2"],
  ["call","<address 3>","function3","%result1%","%result2%"]
]

Commands

To support a variety of use cases, composable transactions contain many different commands:

  • Smart contract call
  • Sending Aergo tokens
  • Checking Aergo balance
  • Mathematical operations
  • String operations
  • Table operations (lists and dictionaries)
  • Variables
  • Conversions
  • Assertion
  • Conditional execution
  • Loops
  • Returning a result

List of Commands

(click on a command to expand)

contract call

call
["call",address,function,args...]

Calls a function on a smart contract

Examples:

["call","%contract%","transfer","%to%","10.5"]
["call","Ammlaklkfld","transfer","Amafpoajf","10.5"]

If the call fails, the entire transaction is reverted

call + send
["call + send",amount,address,function,args...]

Calls a function on a smart contract, also sending an amount of native AERGO tokens

Examples:

["call + send","1.5 aergo","%contract%","buy_something","%arg%"]

If the call fails, the entire transaction is reverted

try call
["try call",address,function,args...]

Makes a protected call to a function on a smart contract.

A "protected call" means that if the call fails, it continues execution on the next command.

It returns 2 values: %call succeeded% and %last result%

You can use that information to conditionally execute commands

Examples:

["try call","%contract%","transfer","%to%","10.5"]
["if","%call succeeded%"]
["...","%last result%"]
...
["end if"]
try call + send
["try call + send",amount,address,function,args...]

Makes a protected call to a function on a smart contract, also sending an amount of native AERGO tokens.

A "protected call" means that if the call fails, it continues execution on the next command.

It returns 2 values: %call succeeded% and %last result%

You can use that information to conditionally execute commands

Examples:

["try call + send","1.5 aergo","%contract%","buy_something","%arg%"]
["if","%call succeeded%"]
["...","%last result%"]
...
...
["end if"]

aergo balance and transfer

get aergo balance
["get aergo balance",address]

Returns the current balance in AERGO tokens from the given account address.

It is also possible to direclty use the %my aergo balance% variable

send
["send",address,amount]

Transfer an amount of native AERGO tokens to the informed address

The amount can be a string or a bignumber

variables

let
["let",variable_name,value]
["let",variable_name,value,token_address]

Assign the value to the variable

The value can be a number, bignumber, string, boolean, list or object

It is also possible to convert an amount in decimal format to bignumber, by supplying the address of the token.

Here are some examples:

["let","min_amount","0.05","%token2%"]
["let","min_amount","1.5","%token2%"]
["let","min_amount","100","Am..."]
store result as
["store result as",variable_name]

Store the result of the last operation in a variable with the given name

Example:

["store result as","amount"]

tables

get
["get",table,key_or_pos]

Retrieve an element from a table (list or object)

For lists we inform the position:

["get","%list%",3]

For dictionaries we inform the key in string format:

["get","%result%","price"]
set
["set",table,key_or_pos,value]

Set a value in a table (list or object)

For lists we inform the position:

["set","%list%",2,"%value%"]

For dictionaries we inform the key in string format:

["set","%obj%","price","%last result%"]
insert
["insert",table,item]
["insert",table,pos,item]

Inserts an element on a list

Other elements are moved up

If no position is informed, the element is inserted at the end

remove
["remove",table,position]

Removes an element from a list, by position

It returns the removed item

get size
["get size",table_or_string]

Retrieve the number of elements on a table (array) or the length of a string

math

add
["add",value1,value2]

Adds 2 values and return the result

The values must be of the same type

They can be number or bignumber

subtract
["subtract",value1,value2]

Subtract one value from the other and return the result

The values must be of the same type

They can be number or bignumber

multiply
["multiply",value1,value2]

Multiplies 2 values and return the result

The values must be of the same type

They can be number or bignumber

divide
["divide",value1,value2]

Divides the first value by the second and returns the result

The values must be of the same type

They can be number or bignumber

remainder
["remainder",value1,value2]

Returns the modulo of the division of the first value by the second

The values must be of the same type

They can be number or bignumber

strings

combine
["combine",str1,str2,...]

Concatenates all the given strings into one

format
["format",formatstring,...]

The same as string.format in Lua

It can also be used for concatenation:

["format","%s %s","hello","world"]
extract
["extract",string,start]
["extract",string,start,end]

The same as string.sub in Lua

find
["find",string,pattern]
["find",string,pattern,init]

The same as string.match in Lua

replace
["replace",string,pattern,replace]
["replace",string,pattern,replace,n]

The same as string.gsub in Lua

conversions

to big number
["to big number"]
["to big number",value]

Converts a value to a bignumber

If no argument is given, it converts the last result

to number
["to number"]
["to number",value]

Converts a value to a number

If no argument is given, it converts the last result

to string
["to string"]
["to string",value]

Converts a value to a string

If no argument is given, it converts the last result

to json
["to json"]
["to json",list_or_object]

Converts a value to a json string

If no argument is given, it converts the last result

from json
["from json"]
["from json",json_string]

Converts a json string to a table

If no argument is given, it converts the last result

assertion

assert
["assert",<expression>]

If the assertion fails, the whole transaction is reverted and marked as failed

If the first value is a big number, the second value can be a string representation of an amount

The expression can be a single boolean value

Examples:

["assert","%call succeeded%"]
["assert","%variable_name%",">","10.5 aergo"]
["assert","%var1%","<=","%var2%"]
["assert","%var1%","<","%var2%","and","%var1%",">=",1500]
["assert","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]

The operators can be: = != > >= < <= match

The logic operators: and or

conditional execution

if
["if",<expression>]

Executes the following commands only if the expression is true

The expression can be a single boolean value

Example expressions:

["if","%call succeeded%"]
["if","%variable_name%",">",10]
["if","%var1%","<=","%var2%"]
["if","%var1%","<","%var2%","and","%var1%",">=",1500]
["if","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]

The operators can be: = != > >= < <= match

The logic operators: and or

Example:

["if","%amount%",">",20],
...
["end if"]

⚠️ Important: if statements can NOT be nested!

else if
["else if",<expression>]

Executes the following commands only if the expression is true

Example expressions:

["else if","%variable_name%",">",10]
["else if","%var1%","<=","%var2%"]
["else if","%var1%","<","%var2%","and","%var1%",">=",1500]
["else if","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]

The operators can be: = != > >= < <= match

The logic operators: and or

Example:

["if","%amount%",">",20],
...
["else if","%amount%",">",10],
...
["end if"]
else
["else"]

Example:

["if","%amount%",">",20],
...
["else"],
...
["end if"]
end if
["end if"]

Used to close an if statement. Check examples above

loops

for
["for",variable,start,end,increment]

Example:

["for","n",1,10],
...
[...,"%n%"],
...
["loop"]

With a decrement:

["for","n",50,10,-10],
...
[...,"%n%"],
...
["loop"]
for each

Iterate on the items from a list or a dictionary

To iterate on a list:

["for each",item,"in",list]

Example:

["let","list",[11,22,33]],
["for each","item","in","%list%"],
...
[...,"%item%"],
...
["loop"]

To iterate on a dictionary:

["for each",key,value,"in",table]

Example:

["let","payments",{"Am..":"10.5","Am..":"12.3"}],
["for each","address","amount","in","%payments%"],
["send","%address%","%amount%"],
["loop"]
loop
["loop"]

This command can be used in 3 ways:

  1. With a for command
  2. With a for each command
  3. Alone as the last command

When used alone, it will loop to the first command on the list.

We can exit the loop using the break command or a return inside of an if statement.

break
["break"]
["break","if",<expression>]

Used to exit from a loop

Example:

["for each","item","in","%list%"],
...
["break","if",...],
...
["loop"]

Or if already using an if statement:

["for each","item","in","%list%"],
...
["if",...],
["break"],
["end if"],
...
["loop"]

return result

return
["return"]
["return",value]

Example:

["return","%last result%"]

variables

There is also these predefined variables:

%my account address%

%my aergo balance%

Example:

["assert","%my aergo balance%",">","10 aergo"],
["call","%token address%","balanceOf","%my account address%"],

Example Scripts

Here is an example script (payload) that would be used to check the returned amount of tokens on a token swap:

["call","<tokenB>","balanceOf"],                         <-- get the previous balance of token B
["store result as","balance_before"],                    <-- store it in a variable
["call","<tokenA>","transfer","<to>","<amount>","swap"], <-- swap token A for token B
["call","<tokenB>","balanceOf"],                         <-- get the new balance of token B
["subtract","%last result%","%balance_before%"],         <-- subtract one balance by the other
["assert","%last result%",">=","<minimum_amount>"]       <-- assert that we got the minimum amount

This feature allows real TRUSTLESS TRANSACTIONS, where the user does not trust even the swap contract!

It can allow for direct token swaps between users A and B, in a complete trustless way

Using Composable Transactions

To build a transaction with composable transactions we:

  • Set the type to MULTICALL (7)
  • Set the recipient to null/none
  • Set the amount to 0
  • Put the JSON script in the payload