Closed code423n4 closed 1 year ago
141345 marked the issue as duplicate of #278
141345 marked the issue as duplicate of #136
alcueca marked the issue as partial-50
No mention of sync()
alcueca marked the issue as full credit
alcueca marked the issue as satisfactory
Lines of code
https://github.com/code-423n4/2023-07-basin/blob/c1b72d4e372a6246e0efbd57b47fb4cbb5d77062/src/Well.sol#L352
Vulnerability details
Description
The TWAP mechanism relies on measurements sent to the oracle at various points in time. Before reserve counts change, the TWAP is sent the last reserve counts, which are multiplied by the time passed and added to the accumulator. In MultiFlowPump, it happens here:
lastReserves[i]
is capped and updated with the parameterreserves[i].price
. Then it is used to determinecumulativeReserves[i]
. This behavior is correct, as discussed in the Uniswap v3 book.The safety of the TWAP relies on calling the observation function (
update()
) with the current reserve, before any reserve changes take place. Updating happens in the_updatePumps
call:It is called in the following functions which change reserve counts:
_swapFrom()
swapTo()
_addLiquidity()
removeLiquidity()
removeLiquidityOneToken()
removeLiquidityImbalanced()
However, it is not called in the
shift()
function, which is very similar toswapFrom()
and updates reserves counts.At the end of the function, it will update the reserves from the current balances:
This means an attacker can easily manipulate the TWAP price. They will transfer tokens to the contract and use
shift()
to change the reserve count to an extreme ratio, then swap back the tokens using a normalswapFrom()
call, which will set update the oracle with the post-shift reserve counts. It will appear as if the entire duration until the shift was with the extreme ratio. Both the EMA oracle and the Cumulative oracle will be skewed.Impact
The TWAP oracle can be arbitrarily manipulated by an attacker.
POC
Assume a Well holding 100 ETH : 100000 USDC, Fair ratio is 1:1000. Last trade happens at t = T Time is now t = T + 1000 sec. Cumulative reserves in oracle contract are
[X, Y]
. To remind, they are calculated asshift()
, new reserves are 10100 : 990. Receive100000-990
USDC to walletswapFrom()
, passing received USDC fromshift()
. Pool returns to 100 : 100,000, user receives 10,000 ETH backswapFrom()
, cumulative reserves were updated:For example, a victim now queries the TWAP price in the last 1000 sec, using
readTwaReserves(Well, [x,y], T)
: It will calculate:Calculation shows the TWAP price for the past 1000 seconds is 1 ETH = 0.098 USDC. Attacker can now exploit any user of the TWAP through uncollateralized loans, swaps at wrong ratio, or any other target-specific way.
Tools Used
Manual audit
Recommended Mitigation Steps
In the
shift()
function, call the_updatePumps()
function, even though the current reserve count is not necessary.Assessed type
Oracle