From 0406f466e829d62eea35812814104646d915a354 Mon Sep 17 00:00:00 2001 From: Eidolon <92181746+imrtlfarm@users.noreply.github.com> Date: Thu, 11 Jan 2024 04:33:18 -0800 Subject: [PATCH 1/2] format more --- foundry.toml | 2 +- src/OptionsToken.sol | 4 +--- src/interfaces/IBalancerVault.sol | 5 +---- src/interfaces/IThenaPair.sol | 10 ++------- src/oracles/BalancerOracle.sol | 15 ++----------- src/oracles/ThenaOracle.sol | 3 +-- src/oracles/UniswapV3Oracle.sol | 12 +++------- test/BalancerOracle.t.sol | 13 ++++------- test/OptionsToken.t.sol | 32 +++++++++------------------ test/ThenaOracle.t.sol | 3 +-- test/mocks/MockBalancerTwapOracle.sol | 6 +---- test/mocks/MockUniswapPool.sol | 14 ++---------- 12 files changed, 29 insertions(+), 90 deletions(-) diff --git a/foundry.toml b/foundry.toml index 15f6ce7..2362c36 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,7 +4,7 @@ verbosity = 1 via_ir = true [fmt] -line_length = 130 +line_length = 150 # Extreme Fuzzing CI Profile :P [profile.ci] diff --git a/src/OptionsToken.sol b/src/OptionsToken.sol index 3048eea..e4df922 100644 --- a/src/OptionsToken.sol +++ b/src/OptionsToken.sol @@ -26,9 +26,7 @@ contract OptionsToken is IOptionsToken, ERC20Upgradeable, OwnableUpgradeable, UU /// Events /// ----------------------------------------------------------------------- - event Exercise( - address indexed sender, address indexed recipient, uint256 amount, address data0, uint256 data1, uint256 data2 - ); + event Exercise(address indexed sender, address indexed recipient, uint256 amount, address data0, uint256 data1, uint256 data2); event SetOracle(IOracle indexed newOracle); event SetExerciseContract(address indexed _address, bool _isExercise); diff --git a/src/interfaces/IBalancerVault.sol b/src/interfaces/IBalancerVault.sol index a67cf92..eb4630c 100644 --- a/src/interfaces/IBalancerVault.sol +++ b/src/interfaces/IBalancerVault.sol @@ -141,10 +141,7 @@ interface IVault { * * Emits a `Swap` event. */ - function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline) - external - payable - returns (uint256); + function swap(SingleSwap memory singleSwap, FundManagement memory funds, uint256 limit, uint256 deadline) external payable returns (uint256); /** * @dev Data for a single swap executed by `swap`. `amount` is either `amountIn` or `amountOut` depending on diff --git a/src/interfaces/IThenaPair.sol b/src/interfaces/IThenaPair.sol index 9ab4697..76db264 100644 --- a/src/interfaces/IThenaPair.sol +++ b/src/interfaces/IThenaPair.sol @@ -7,19 +7,13 @@ interface IThenaPair { function reserve1CumulativeLast() external view returns (uint256); - function currentCumulativePrices() - external - view - returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); + function currentCumulativePrices() external view returns (uint256 reserve0Cumulative, uint256 reserve1Cumulative, uint256 blockTimestamp); function stable() external view returns (bool); function observationLength() external view returns (uint256); - function observations(uint256) - external - view - returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative); + function observations(uint256) external view returns (uint256 timestamp, uint256 reserve0Cumulative, uint256 reserve1Cumulative); function token0() external view returns (address); diff --git a/src/oracles/BalancerOracle.sol b/src/oracles/BalancerOracle.sol index e1c818f..633515e 100644 --- a/src/oracles/BalancerOracle.sol +++ b/src/oracles/BalancerOracle.sol @@ -66,14 +66,7 @@ contract BalancerOracle is IOracle, Owned { /// Constructor /// ----------------------------------------------------------------------- - constructor( - IBalancerTwapOracle balancerTwapOracle_, - address token, - address owner_, - uint56 secs_, - uint56 ago_, - uint128 minPrice_ - ) Owned(owner_) { + constructor(IBalancerTwapOracle balancerTwapOracle_, address token, address owner_, uint56 secs_, uint56 ago_, uint128 minPrice_) Owned(owner_) { balancerTwapOracle = balancerTwapOracle_; IVault vault = balancerTwapOracle.getVault(); @@ -118,11 +111,7 @@ contract BalancerOracle is IOracle, Owned { // query Balancer oracle to get TWAP value { IBalancerTwapOracle.OracleAverageQuery[] memory queries = new IBalancerTwapOracle.OracleAverageQuery[](1); - queries[0] = IBalancerTwapOracle.OracleAverageQuery({ - variable: IBalancerTwapOracle.Variable.PAIR_PRICE, - secs: secs_, - ago: ago_ - }); + queries[0] = IBalancerTwapOracle.OracleAverageQuery({variable: IBalancerTwapOracle.Variable.PAIR_PRICE, secs: secs_, ago: ago_}); price = balancerTwapOracle.getTimeWeightedAverage(queries)[0]; } diff --git a/src/oracles/ThenaOracle.sol b/src/oracles/ThenaOracle.sol index 91e33a6..1916774 100644 --- a/src/oracles/ThenaOracle.sol +++ b/src/oracles/ThenaOracle.sol @@ -97,8 +97,7 @@ contract ThenaOracle is IOracle, Owned { thenaPair.observations(observationLength - 1); uint32 T = uint32(blockTimestampCurrent - blockTimestampLast); if (T < secs_) { - (blockTimestampLast, reserve0CumulativeLast, reserve1CumulativeLast) = - thenaPair.observations(observationLength - 2); + (blockTimestampLast, reserve0CumulativeLast, reserve1CumulativeLast) = thenaPair.observations(observationLength - 2); T = uint32(blockTimestampCurrent - blockTimestampLast); } uint112 reserve0 = safe112((reserve0CumulativeCurrent - reserve0CumulativeLast) / T); diff --git a/src/oracles/UniswapV3Oracle.sol b/src/oracles/UniswapV3Oracle.sol index 2bb3588..de15ef6 100644 --- a/src/oracles/UniswapV3Oracle.sol +++ b/src/oracles/UniswapV3Oracle.sol @@ -66,9 +66,7 @@ contract UniswapV3Oracle is IOracle, Owned { /// Constructor /// ----------------------------------------------------------------------- - constructor(IUniswapV3Pool uniswapPool_, address token, address owner_, uint32 secs_, uint32 ago_, uint128 minPrice_) - Owned(owner_) - { + constructor(IUniswapV3Pool uniswapPool_, address token, address owner_, uint32 secs_, uint32 ago_, uint128 minPrice_) Owned(owner_) { uniswapPool = uniswapPool_; isToken0 = token == uniswapPool_.token0(); secs = secs_; @@ -113,14 +111,10 @@ contract UniswapV3Oracle is IOracle, Owned { // Calculate quoteAmount with better precision if it doesn't overflow when multiplied by itself if (sqrtRatioX96 <= type(uint128).max) { uint256 ratioX192 = uint256(sqrtRatioX96) * sqrtRatioX96; - price = isToken0 - ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) - : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); + price = isToken0 ? FullMath.mulDiv(ratioX192, decimalPrecision, 1 << 192) : FullMath.mulDiv(1 << 192, decimalPrecision, ratioX192); } else { uint256 ratioX128 = FullMath.mulDiv(sqrtRatioX96, sqrtRatioX96, 1 << 64); - price = isToken0 - ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) - : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); + price = isToken0 ? FullMath.mulDiv(ratioX128, decimalPrecision, 1 << 128) : FullMath.mulDiv(1 << 128, decimalPrecision, ratioX128); } } diff --git a/test/BalancerOracle.t.sol b/test/BalancerOracle.t.sol index e91c8f6..05cc0c6 100644 --- a/test/BalancerOracle.t.sol +++ b/test/BalancerOracle.t.sol @@ -166,9 +166,8 @@ contract BalancerOracleTest is Test { // weighted average of the first recorded oracle price and the current spot price // weighted by the time since the last update - uint256 spotAverage = ( - (price_1 * (_default.secs - skipTime)) + (getSpotPrice(address(_default.pair), _default.token) * skipTime) - ) / _default.secs; + uint256 spotAverage = + ((price_1 * (_default.secs - skipTime)) + (getSpotPrice(address(_default.pair), _default.token) * skipTime)) / _default.secs; assertApproxEqRel(spotAverage, oracle.getPrice(), 0.01 ether, "price variance too large"); } @@ -187,13 +186,9 @@ contract BalancerOracleTest is Test { : (balances[0] * weights[1]).divWadDown(balances[1] * weights[0]); } - function swap(address pool, address tokenIn, address tokenOut, uint256 amountIn, address sender) - internal - returns (uint256 amountOut) - { + function swap(address pool, address tokenIn, address tokenOut, uint256 amountIn, address sender) internal returns (uint256 amountOut) { bytes32 poolId = IBalancerTwapOracle(pool).getPoolId(); - IVault.SingleSwap memory singleSwap = - IVault.SingleSwap(poolId, IVault.SwapKind.GIVEN_IN, IAsset(tokenIn), IAsset(tokenOut), amountIn, ""); + IVault.SingleSwap memory singleSwap = IVault.SingleSwap(poolId, IVault.SwapKind.GIVEN_IN, IAsset(tokenIn), IAsset(tokenOut), amountIn, ""); IVault.FundManagement memory funds = IVault.FundManagement(sender, false, payable(sender), false); diff --git a/test/OptionsToken.t.sol b/test/OptionsToken.t.sol index e88d2d7..8302af2 100644 --- a/test/OptionsToken.t.sol +++ b/test/OptionsToken.t.sol @@ -111,8 +111,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); (uint256 paymentAmount,,,) = optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); // verify options tokens were transferred @@ -123,12 +122,8 @@ contract OptionsTokenTest is Test { assertEqDecimal(paymentToken.balanceOf(address(this)), 0, 18, "user still has payment tokens"); uint256 paymentFee1 = expectedPaymentAmount.mulDivDown(feeBPS_[0], 10000); uint256 paymentFee2 = expectedPaymentAmount - paymentFee1; - assertEqDecimal( - paymentToken.balanceOf(feeRecipients_[0]), paymentFee1, 18, "fee recipient 1 didn't receive payment tokens" - ); - assertEqDecimal( - paymentToken.balanceOf(feeRecipients_[1]), paymentFee2, 18, "fee recipient 2 didn't receive payment tokens" - ); + assertEqDecimal(paymentToken.balanceOf(feeRecipients_[0]), paymentFee1, 18, "fee recipient 1 didn't receive payment tokens"); + assertEqDecimal(paymentToken.balanceOf(feeRecipients_[1]), paymentFee2, 18, "fee recipient 2 didn't receive payment tokens"); assertEqDecimal(paymentAmount, expectedPaymentAmount, 18, "exercise returned wrong value"); } @@ -147,8 +142,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); vm.expectRevert(bytes4(keccak256("BalancerOracle__BelowMinPrice()"))); optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); } @@ -168,8 +162,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); (uint256 paidAmount,,,) = optionsToken.exercise(amount, address(this), address(exerciser), abi.encode(params)); // update multiplier @@ -201,8 +194,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens which should fail - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount - 1, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount - 1, deadline: type(uint256).max}); vm.expectRevert(DiscountExercise.Exercise__SlippageTooHigh.selector); optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); } @@ -225,8 +217,7 @@ contract OptionsTokenTest is Test { oracle.setParams(ORACLE_SECS, ORACLE_LARGEST_SAFETY_WINDOW, ORACLE_MIN_PRICE); // exercise options tokens which should fail - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); vm.expectRevert(BalancerOracle.BalancerOracle__TWAPOracleNotReady.selector); optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); } @@ -244,8 +235,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: deadline}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: deadline}); if (amount != 0) { vm.expectRevert(DiscountExercise.Exercise__PastDeadline.selector); } @@ -263,8 +253,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens which should fail - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); vm.expectRevert(BaseExercise.Exercise__NotOToken.selector); exerciser.exercise(address(this), amount, recipient, abi.encode(params)); } @@ -285,8 +274,7 @@ contract OptionsTokenTest is Test { paymentToken.mint(address(this), expectedPaymentAmount); // exercise options tokens which should fail - DiscountExerciseParams memory params = - DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); + DiscountExerciseParams memory params = DiscountExerciseParams({maxPaymentAmount: expectedPaymentAmount, deadline: type(uint256).max}); vm.expectRevert(OptionsToken.OptionsToken__NotExerciseContract.selector); optionsToken.exercise(amount, recipient, address(exerciser), abi.encode(params)); } diff --git a/test/ThenaOracle.t.sol b/test/ThenaOracle.t.sol index 26e5219..0b592be 100644 --- a/test/ThenaOracle.t.sol +++ b/test/ThenaOracle.t.sol @@ -188,8 +188,7 @@ contract ThenaOracleTest is Test { // wait skip(skipTime); - uint256 expectedMinPrice = - (price_1 * (_default.secs - skipTime) + getSpotPrice(_default.pair, _default.token) * skipTime) / _default.secs; + uint256 expectedMinPrice = (price_1 * (_default.secs - skipTime) + getSpotPrice(_default.pair, _default.token) * skipTime) / _default.secs; assertGeDecimal(oracle.getPrice(), expectedMinPrice, 18, "price variation too large"); } diff --git a/test/mocks/MockBalancerTwapOracle.sol b/test/mocks/MockBalancerTwapOracle.sol index f14e56d..3d0eb2c 100644 --- a/test/mocks/MockBalancerTwapOracle.sol +++ b/test/mocks/MockBalancerTwapOracle.sol @@ -11,11 +11,7 @@ contract MockVault is IVault { tokens = _tokens; } - function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) - external - payable - override - {} + function joinPool(bytes32 poolId, address sender, address recipient, JoinPoolRequest memory request) external payable override {} function getPool(bytes32 poolId) external view override returns (address, PoolSpecialization) {} diff --git a/test/mocks/MockUniswapPool.sol b/test/mocks/MockUniswapPool.sol index c47066d..42cd0ba 100644 --- a/test/mocks/MockUniswapPool.sol +++ b/test/mocks/MockUniswapPool.sol @@ -92,13 +92,7 @@ contract MockUniswapPool is IUniswapV3Pool { external view override - returns ( - uint128 _liquidity, - uint256 feeGrowthInside0LastX128, - uint256 feeGrowthInside1LastX128, - uint128 tokensOwed0, - uint128 tokensOwed1 - ) + returns (uint128 _liquidity, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128, uint128 tokensOwed0, uint128 tokensOwed1) {} function observations(uint256 index) @@ -122,11 +116,7 @@ contract MockUniswapPool is IUniswapV3Pool { returns (uint128 amount0, uint128 amount1) {} - function burn(int24 tickLower, int24 tickUpper, uint128 amount) - external - override - returns (uint256 amount0, uint256 amount1) - {} + function burn(int24 tickLower, int24 tickUpper, uint128 amount) external override returns (uint256 amount0, uint256 amount1) {} function swap(address recipient, bool zeroForOne, int256 amountSpecified, uint160 sqrtPriceLimitX96, bytes calldata data) external From 1075484cd6cae3b1cdfec618763825c04a058b60 Mon Sep 17 00:00:00 2001 From: Eidolon <92181746+imrtlfarm@users.noreply.github.com> Date: Thu, 11 Jan 2024 04:40:18 -0800 Subject: [PATCH 2/2] Update README.md --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ba1fdbf..9548b78 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,22 @@ # OptionsToken -An options token representing the right to purchase the underlying token at an oracle-specified rate. It's similar to a call option but with a variable strike price that's always at a certain discount to the market price. -It also has no expiry date. +An options token representing the right to exercise any one of the whitelisted exercise contracts, allowing the user to receive different forms of discounted assets in return for the appropriate payment. The option does not expire. The options token receives user input and a specified exercise contract address, passing through to the exercise contract to execute the option. We fork https://github.com/timeless-fi/options-token, which is a simple implementation of an option for discounted tokens at an adjusted oracle rate. Here, we divorce the exercise functionality from the token contract, and allow an admin to whitelist and fund exercise contracts as the desired. We also implement more potential oracle types, and make several other minor changes. + +We want to ensure there are no attacks on pricing in DiscountExercise, atomically or otherwise, in each oracle implementation. We want to ensure users will never pay more than maxPaymentAmount. When properly specified, this should ensure users experience no more deviation in price than they specify. + +Given the nature of this token, it is fine for the admin to have some centralized permissions (admin can mint tokens, admin is the one who funds exercise contracts, etc). The team is responsible for refilling the exercise contracts. We limit the amount of funds we leave in an exercise contract at any given time to limit risk. + +# Flow of an Option Token Exercise (Ex. Discount Exercise) + +The user will always interact with the OptionsToken itself, and never with any exercise contract directly. + +1. The user approves OptionsToken the amount of WETH they wish to spend +2. User calls exercise on the OptionsToken, specifying their desired exercise contract and encoding exercise parameters +3. OptionsToken validates the exercise contract, decodes the parameters for the exercise function on the exercise contract of choice, and calls said function. In the case of DiscountExercise, the params are maxPaymentAmount and deadline. +4. oTokens are burnt, WETH is sent to the treasury, and underlyingTokens, discounted by the multiplier, are sent to the user exercising + a. Can be priced using balancer, thena, univ3 twap oracles + b. Reverts above maxPaymentAmount or past deadline + ## Installation