aergoio / aergo

aergo blockchain kernel
http://docs.aergo.io
MIT License
214 stars 45 forks source link

Proposal: Composable Transactions #149

Open kroggen opened 2 years ago

kroggen commented 2 years ago

COMPOSABLE TRANSACTIONS

It is a method to call many contracts in a single transaction.

All the process is atomic. If some of the calls fail, all the transaction is rolled back.

It is used in cases where the user desires to process many things at the same time atomically.

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

It may be possible to implement it with backwards compatibility with existing transactions, by adding a new transaction type:

type: COMPOSABLE_CALL
recipient: <empty>
amount: 0

And then the call information is stored in the payload in JSON format.

It would be a list of calls processed in order

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

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

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

Example uses:

Buy 2 products or services on a single transaction:

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

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

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

Add liquidity to a swap pair:

[
  ["<token 1>","transfer","<amount 1>","<swap_pair>","store_token"],
  ["<token 2>","transfer","<amount 2>","<swap_pair>","add_liquidity"],
]
kroggen commented 2 years ago

Commands

A more advanced approach would contain different commands, and the first element would be the command to be executed:

The call is a normal smart contract call

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

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

Example 1:

[
  ["call","<address>","function","arg"],
  ["assert","return_value",">=","250"],
]

Example 2:

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

Example 3:

[
  ["call","<address 1>","function","arg"],
  ["store_result","result1"],
  ["call","<address 2>","function","arg"],
  ["assert","return_value",">=","result1"],
]
kroggen commented 2 years ago

Variables

Another possible feature is the use of variables in the arguments, using some form of identification like %variable%

One hard-coded variable can be the result of the last call.

Example usage:

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

When we need to store the result to use later, we use the store_result command:

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

Implementation

To implement this is easy: iterate over the items in the array and check if they start and end with %, then replace the content with the variable value

For security reasons, block variable substitution in the first item (command)

The variable substitution also makes the assert command simpler

kroggen commented 2 years ago

Finished Concept

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

List of Commands

-- contract call
call
call.send
call.gas

-- aergo balance and transfer
balance
send

-- variables
let
store

-- tables
get
set
insert    (inserts at the end if no position informed)
remove

-- math
add
sub
mul
div
pow
mod
sqrt

-- strings
format    (can also be used for concatenation)
substr
find
replace

-- conversions
tobignum
tonumber
tostring
tojson
fromjson

-- assertion
assert

-- conditional execution
if
elif
else
end

-- return result
return

Using

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

And many other use cases that are possible with this new feature

Security

Execution of these scripts is only enabled on transactions with type MULTICALL and signed by the user. This means that there is no way an user can execute an script in the context of another user, as the engine uses the account from the transaction as the context.