Closed nventuro closed 2 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.
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.
Implemented as of #965.
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
uint256
s are unnecessarily large, and useint256
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.
balanceOf
asjoinPool
returns nothing).balanceOf
.