Closed c4-bot-3 closed 6 months ago
Picodes marked the issue as primary issue
othernet-global (sponsor) disputed
In the POC the amount of SALT swapped is 5x the amount of SALT in the pool reserves. Yes, this will shift the reserves and is acceptable behavior as a single trade being 5x the reserves is not realistic.
This is a duplicate of #222 with a detailed example.
Picodes marked the issue as duplicate of #222
Picodes changed the severity to 2 (Med Risk)
Picodes marked the issue as satisfactory
Lines of code
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/pools/Pools.sol#L382-L391 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L95-L111 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/arbitrage/ArbitrageSearch.sol#L39-L53
Vulnerability details
Impact
De-pegging of USDS and bad debts
Proof of Concept
Lets take a look at the following scenario 1 SALT -> 2.5\$ 1 ETH -> 2500\$ -> 1000 SALT 1 BTC -> 40000\$ -> 16 ETH WETH : SALT pool has -> 1 WETH : 1000 SALT WBTC : SALT pool has -> 1 WBTC : 16000 SALT WETH : WBTC pool has -> 0.01 WBTC : 0.16 WETH (For simpler calculations) all the pools are in the correct ratio as you can see from the prices. Bob flashloans 5000 SALT(or simply has them), 10 WBTC, 40 WETH = 512500$
He calls pools.depositSwapWithdraw() with tokenIn as SALT and tokenOut as WETH.
depositSwapWithdraw calls _adjustReservesForSwapAndAttemptArbitrage which in turn calls _adjustReservesForSwap which returns swapAmountOut as 0.833 WETH because -> (1 e18 * 5000 e18)/ (5000 e18 + 1000 e18) = 0.833 WETH Next in the call stack _attemptArbitrage is called and so on (Run the POC with stacktraces for more details).
Essentially after bob is done with swapping out his 5000 SALT to 0.833 WETH the pool reserves will Look like this
------------ ReserveAfterSaltSwap ------------- reserve_Wbtc_wbtc_weth_pool: 0.05021715 reserve_Weth_wbtc_weth_pool: 0.031861624962786618 reserve_Weth_weth_salt_pool: 0.187632666693793401 reserve_Salt_weth_salt_pool: 5329.562371097923848262 reserve_Salt_salt_wbtc_pool: 16670.437628902076151738 reserve_Wbtc_salt_wbtc_pool: 0.95978285 ------------ ReserveAfterSaltSwap -------------
Where reserve_Wbtc_wbtc_weth_pool represents WBTC reserve in WBTC : WETH pool , reserve_Weth_wbtc_weth_pool represents WETH reserve in WBTC : WETH and so on.
These can be verified by both running the coded POC and doing manual calculations of _arbitrage function using the 'LogData' event in the above plaintext blob, which i added to know to state of contracts before _arbitrage is called.
So you might notice that the wbtc & weth ratio are skewed almost making 5 WBTC = 3 WETH. Now bob calls depositCollateralAndIncreaseShare function with 10 WBTC and 10 WETH passed in as input amounts(flashloaned) . So the protocol adds 10 WBTC and 6.334 WETH as he did not dual zap. Next he takes maxBorrowableLoan 207931$ of USDS
At this point bob has 207931$ of USDS & 33.666 WETH. & wbtc-weth pool reserves are- WbtcReservesNow: 10.05021715 WethReservesNow: 6.376631282895707542
In the next step bob rebalances the reserves close to their original ration of 1 WBTC = 16 WETH and in the process swaps out most of his deposited WBTC for cheaper prices. In the poc he gives 30 WETH(~ approx 1.9 wbtc) to recieve back btcOut: 8.28846717.
Now bob's attack is complete and his final balances are finalWethBalanceBob: 4.488563675400412409 -> 11,222 \$ finalWbtcBalanceBob: 8.28846717 -> 331,536 \$ finalUsdsBalanceBob: 207930.961872416151153750 -> 207,931 \$
making his final dollar value ~= 550,690, yielding a net profit = 550,690 - 512,500(flashloan value) ~= 38,190 \$.
So in a nutshell the attack vector can be summarized as -
It is also important to note that arbitrage path while swapping SALT to WETH & swapping WETH to WBTC is the same i.e WETH->SALT->WBTC->WETH Which was crucial for this attack vector to work.
Now here is the coded POC, add it to src/pools/tests/Pools.t.sol and run using
If you want to manually verify u can also add the LogData event in Pools.sol's _attempArbitrage
Tools Used
Manual review
Recommended Mitigation Steps
Assessed type
Error