balancer / balancer-v2-monorepo

Balancer V2 Monorepo
https://balancer.fi/
GNU General Public License v3.0
310 stars 379 forks source link

[relayers] Pass arguments to future batched operations #934

Closed nventuro closed 2 years ago

nventuro commented 3 years ago

The relayer designs we've produced so far are quite limited and inefficient by the fact that inputs to future actions are typically dependent on the outputs of past actions, and therefore unknown by the caller. To work around this, we've relied on global state in the form of relayer balances: we first transfer tokens into the relayer, perform operations using the relayer's full balance, and finally send the last output to the user. This is inefficient, especially for joins, since we're moving tokens to and from the relayer, when we could use its relayer powers to move them from the user to the vault directly.

@TomAFrench and I discussed an alternative approach where we still use global state, except instead of it being the relayer's token balance, we store data in a temporary part of the relayer's storage. This allows for more flexibility, reduces token interactions (including approvals!), and should not result in extra gas since storage is only used temporarily (i.e. EIP2200 will kick-in).

Temporary storage

The high level idea is as follows. Each relayer action has multiple inputs (tokens in) and outputs (tokens out). For each input, we can either specify an immediate value (e.g. 50), or reference a previously written value in storage, which would be read and then cleared. For each output, we in turn optionally specify a storage slot in which to store it.

There are many ways we could implement this. A straightforward, if somewhat hacky approach, would be to rely on the fact that uint256s are unnecessarily large, and use int256 instead. Positive values would be treated as immediate values, and negative ones as storage references.

Simple example: 1) swap given in 50 DAI for USDC in Pool A, store output in -1 2) swap given in -1 USDC for BAL in Pool B, store output in -2 // clears slot -1 3) swap given in 30 USDT for WETH in Pool B, store output in -3 4) join Pool C with exact amounts [0, -2, -3], don't store output // clears slots -2, -3

This would result in the user joining Pool C with BAL and WETH, starting from DAI and USDT. All token transfers would happen to and from the user.

Learnings and tentative design

There's a few things to note about the example above:

With this in mind, we can improve the original idea a bit by adding more data to each high-level operation. I propose only worrying about given-in operations to begin with. I'm also not considering ETH <> WETH.

nventuro commented 3 years ago

On top of Vault operations, the relayer will also perform other miscellaneous actions, such as wrapping/unwrapping tokens in the Aave protocol. These could be defined as top-level actions (like swaps or joins), and use the same storage mechansim to pass values forward.

nventuro commented 3 years ago

As noted in this conversation, this feature would benefit greatly from the relayer being able to make Vault actions both from the sender and itself.

In other words, we would change require(sender == msg.sender) to require(sender == msg.sender || sender == address(this)). Also, we'd likely want to do token approvals when the sender is the relayer, as there's no guarantee of prior allowance.

nventuro commented 2 years ago

Implemented as of #965.