Resolves #883
Reviewers @hensha256 @snreynolds
I believe a bad tradeoff was made in response to TOB-UNI4-3.
The manager contract does not need to be fool-proof.
Similar to TOB-UNI4-6, it can remain safely unresolved.
Not every issue raised by an audit must be resolved.
Trail Of Bits raised TOB-UNI4-6 but it was ignored.
Mistakes can be made between sync and settle.
The main issue raised by TOB-UNI4-3 is that mistakes can be made between sync and settle.
A particular kind of mistake is hypothesized.
This mistake is not the only kind of mistake that can be made, because the interface is not fool-proof.
Here is an example of another mistake that is possible between sync and settle: take.
take, like collectProtocolFees, can be invoked between sync and settle.
This can cause a deposit not to be fully credited; an amount of balance equal to the amount taken is burned, just as in TOB-UNI4-3.
However, the manager does not prevent this with an additional TLOAD.
Instead, the users are supposed to not call take on that currency between sync and settle.
Similarly, the users should not call collectProtocolFees between sync and settle.
collectProtocolFees can be safely called without restricting sync
It isn't necessary to call collectProtocolFees (or take) between sync and settle.
Proper usage can prevent loss of funds.
settle should be called as soon after sync as possible.
Warnings can be added to the source code.
There are valid reasons to sync before unlock
The callback may not be the originator of the funds.
It is sometimes better to transfer-in the funds before calling unlock.
One such flow looks like this:
sync()
transfer()
unlock()
3a. settle
3b. swap
3c. take
Preventing this flow requires the unlock callback contract to have access to the originating funds, which isn't always good or necessary.
The originating funds could be coming from a separate system entirely.
Impact: Additional transfers
This section describes example of the aforementioned scenario, where being able to sync before unlock saves thousands of gas. For my project the gas impact is between 8371 and 25292 gas when the settled token is DAI or WETH.
Custodian Contract (CC)
This contract custodies funds and is the ultimate counterparty performing the swap. Perhaps it is an ERC-4337 smart account. Perhaps it is a multisig. But for whatever reason it doesn't have support for the UniswapV4 callback.
Swap Callback (SC)
This contract has support for the UniswapV4 callback. Its logic might be shared between many CC or it may be specific to one CC.
The additional approval/transferFrom or transfer/transfer in comparison to a single transfer amounts to thousands to tens of thousands of gas.
Any account that wants to swap but doesn't have uniswap v4 callbacks benefits from sync outside of unlock.
Changes
Succinctly, the trade-off being made is whether collectProtocolFees should be fool-proofed against use between sync and settle, at the cost of custodial smart contracts without a uniswapv4 hook needing additional ERC20 operations to perform a swap. However, it is unclear that it is more important to fool-proof collectProtocolFees than other methods, such as take, that are still not fool-proofed against the same mistake. Therefore #856 is reverted and TOB-UNI4-3 is accepted as permissible.
How to solve the issue without requiring sync() to be unlocked.
If collectProtocolFees must be fool-proof, it can add a check that the collected token is not set in CURRENCY_SLOT. This solution is discussed more in its pull request: #886
Resolves #883 Reviewers @hensha256 @snreynolds I believe a bad tradeoff was made in response to
TOB-UNI4-3
. The manager contract does not need to be fool-proof. Similar toTOB-UNI4-6
, it can remain safely unresolved.Not every issue raised by an audit must be resolved.
Trail Of Bits raised
TOB-UNI4-6
but it was ignored.Mistakes can be made between sync and settle.
The main issue raised by
TOB-UNI4-3
is that mistakes can be made betweensync
andsettle
. A particular kind of mistake is hypothesized. This mistake is not the only kind of mistake that can be made, because the interface is not fool-proof. Here is an example of another mistake that is possible betweensync
andsettle
:take
.take
, likecollectProtocolFees
, can be invoked betweensync
andsettle
. This can cause a deposit not to be fully credited; an amount of balance equal to the amount taken is burned, just as inTOB-UNI4-3
. However, the manager does not prevent this with an additionalTLOAD
. Instead, the users are supposed to not calltake
on that currency betweensync
andsettle
. Similarly, the users should not callcollectProtocolFees
betweensync
andsettle
.collectProtocolFees can be safely called without restricting sync
It isn't necessary to call
collectProtocolFees
(ortake
) betweensync
andsettle
. Proper usage can prevent loss of funds.settle
should be called as soon aftersync
as possible. Warnings can be added to the source code.There are valid reasons to sync before unlock
The callback may not be the originator of the funds. It is sometimes better to transfer-in the funds before calling unlock. One such flow looks like this:
sync()
transfer()
unlock()
3a.settle
3b.swap
3c.take
Preventing this flow requires the unlock callback contract to have access to the originating funds, which isn't always good or necessary. The originating funds could be coming from a separate system entirely.
Impact: Additional transfers
This section describes example of the aforementioned scenario, where being able to
sync
beforeunlock
saves thousands of gas. For my project the gas impact is between 8371 and 25292 gas when the settled token is DAI or WETH.Custodian Contract (CC)
This contract custodies funds and is the ultimate counterparty performing the swap. Perhaps it is an ERC-4337 smart account. Perhaps it is a multisig. But for whatever reason it doesn't have support for the UniswapV4 callback.
Swap Callback (SC)
This contract has support for the UniswapV4 callback. Its logic might be shared between many CC or it may be specific to one CC.
The swap with versatile
sync
: transferCC: PoolManager.sync(TokenA) CC: TokenA.transfer(PoolManager, AmountIn) CC: SC.performSwap() SC: PoolManager.unlock() PoolManager: SC.unlockCallback() SC: PoolManager.settle(TokenA) SC: PoolManager.swap() SC: PoolManager.take(TokenB, CC)
The swap without versatile
sync
: approve/transferFromCC: TokenA.approve(SC, AmountIn) CC: SC.performSwap() SC: PoolManager.unlock() PoolManager: SC.unlockCallback() SC: PoolManager.sync(TokenA) SC: TokenA.transferFrom(CC, PoolManager, AmountIn) SC: PoolManager.settle(TokenA) SC: PoolManager.swap() SC: PoolManager.take(TokenB, CC)
The swap without versatile
sync
: transfer/transferCC: TokenA.transfer(SC, AmountIn) CC: SC.performSwap() SC: PoolManager.unlock() PoolManager: SC.unlockCallback() SC: PoolManager.sync(TokenA) SC: TokenA.transfer(PoolManager, AmountIn) SC: PoolManager.settle(TokenA) SC: PoolManager.swap() SC: PoolManager.take(TokenB, CC)
Analysis
The additional approval/transferFrom or transfer/transfer in comparison to a single transfer amounts to thousands to tens of thousands of gas. Any account that wants to swap but doesn't have uniswap v4 callbacks benefits from
sync
outside ofunlock
.Changes
Succinctly, the trade-off being made is whether
collectProtocolFees
should be fool-proofed against use betweensync
andsettle
, at the cost of custodial smart contracts without a uniswapv4 hook needing additional ERC20 operations to perform a swap. However, it is unclear that it is more important to fool-proofcollectProtocolFees
than other methods, such astake
, that are still not fool-proofed against the same mistake. Therefore #856 is reverted andTOB-UNI4-3
is accepted as permissible.How to solve the issue without requiring
sync()
to be unlocked.If
collectProtocolFees
must be fool-proof, it can add a check that the collected token is not set inCURRENCY_SLOT
. This solution is discussed more in its pull request: #886