Skip to content

Composable Transactions

Bernardo Ramos edited this page Aug 11, 2022 · 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","result1"],
  ["call","<address 2>","function","arg"],
  ["assert","%last_result%",">=","%result1%"]
]

Example 4:

[
  ["call","<address 1>","function1","arg"],
  ["store","result1"],
  ["call","<address 2>","function2","arg"],
  ["store","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","1.5 aergo","%contract%","do_something","%arg%"]

If the call fails, the entire transaction is reverted

pcall
["pcall",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 a list with 2 elements: [success, result]

We can check if the call succeeded by checking the first returned value

Examples:

["pcall","%contract%","transfer","%to%","10.5"]
["store","result"]
["get","%result%",1]
["if","%last_result%","=",true]
["get","%result%",2]
...
["end"]
pcall-send
["pcall-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 a list with 2 elements: [success, result]

We can check if the call succeeded by checking the first returned value

Examples:

["pcall-send","1.5 aergo","%contract%","do_something","%arg%"]
["store","result"]
["get","%result%",1]
["if","%last_result%","=",true]
["get","%result%",2]
...
["end"]

aergo balance and transfer

balance
["balance"]
["balance",address]

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

If no address is given, it returns the balance of the caller account.

send
["send",address,amount]

Transfer an amount of native AERGO tokens to the informed address

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.

⚠️ The decimal format must always contain a .

Here are some examples:

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

Store the last result in a variable with the given name

Example:

["store","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

get_keys
["get_keys",table]

Retrieve a list of keys from a table (dictionary)

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

sub
["sub",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

mul
["mul",value1,value2]

Multiplies 2 values and return the result

The values must be of the same type

They can be number or bignumber

div
["div",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

pow
["pow",value1,value2]

Elevates the first value by the power of the second and returns the result

The values must be of the same type

They can be number or bignumber

mod
["mod",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

sqrt
["sqrt",value]

Returns the square root of the given bignumber value

For numbers use:

["pow",value,0.5]

strings

concat
["concat",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"]
substr
["substr",string,start]
["substr",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

tobignum
["tobignum",value]
tonumber
["tonumber",value]
tostring
["tostring",value]
tojson
["tojson",list_or_object]
fromjson
["fromjson",json_string]

assertion

assert
["assert",<expression>]

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

Examples:

["assert","%variable_name%",">",10]
["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>]

Example expressions:

["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"]

⚠️ Important: if statements can NOT be nested!

elif
["elif",<expression>]

Example expressions:

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

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

The logic operators: and or

Example:

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

Example:

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

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"]
foreach
["foreach",item,list]

Iterate on items from a list

Example:

["let","list",[11,22,33]],
["foreach","item","%list%"],
...
[...,"%item%"],
...
["loop"]
forpair
["forpair",key,value,table]

Iterate on items from a dictionary

Example:

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

This command can be used in 4 ways:

  1. With a for command
  2. With a foreach command
  3. With a forpair command
  4. 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:

["foreach","item","%list%"],
...
["break","if",...],
...
["loop"]

Or if already using an if statement:

["foreach","item","%list%"],
...
["if",...],
["break"],
["end"],
...
["loop"]

return result

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

Example:

["return","%last_result%"]

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","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
["sub","%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