diff --git a/contracts/strategies/NormalStrategyLib.sol b/contracts/strategies/NormalStrategyLib.sol index 3f5bb482..c9531796 100644 --- a/contracts/strategies/NormalStrategyLib.sol +++ b/contracts/strategies/NormalStrategyLib.sol @@ -62,6 +62,10 @@ error NormalStrategyLib_InvalidDuration(); error NormalStrategyLib_InvalidStrategyArgs(); error NormalStrategyLib_InvalidStrikePrice(); error NormalStrategyLib_InvalidVolatility(); +error NormalStrategyLib_UpperReserveYBoundNotReached(); +error NormalStrategyLib_LowerReserveYBoundNotReached(); +error NormalStrategyLib_UpperReserveXBoundNotReached(); +error NormalStrategyLib_LowerReserveXBoundNotReached(); /** * @notice @@ -164,12 +168,12 @@ function computeStdDevSqrtTau(NormalCurve memory self) pure returns (uint256) { * As x approaches its lower bound of 0, Φ⁻¹(1-x) approaches +∞. * * Theoretically, if both x and y are at opposite bounds, they cancel eachother out. - * Pragmatically, it's a lot messier because infinity does not exist. + * Pragmatically, it's complicated because infinity does not exist. * * To handle this special property, the x and y reserves are checked for exceeding their bounds. * If they do exceed a bound, they are overwritten to be at the bound minus 1. - * This prevents reverts that would happen if inputting values outside the theoretically and - * practial domain of the `Gaussian.ppf` function, i.e. inputs of 0 or 1. + * This prevents reverts that would happen if inputting values outside the theoretical + * domain of the `Gaussian.ppf` function, i.e. inputs of 0 or 1E18. * * By setting either or both the x and y to their bounds minus 1, each term will * return the `ppf`'s effective minimum and maximum output. @@ -180,54 +184,17 @@ function computeStdDevSqrtTau(NormalCurve memory self) pure returns (uint256) { * It's likely that the x and y reserves will reach opposite bounds, * in which case the invariant terms completely cancel out, returning `k = σ√τ`. * - * In the case only one bound is reached, the invariant term for the bound will - * be either the min or max output of the ppf. This will require the other term - * to be very close to the min or max output of the ppf in order to cancel out. - * - * note Assumes maximum values are from valid pools created via this strategy in Portfolio. - * - * ## Example - * The maximum value of σ√τ, where σ_max = 25_000 and τ_max = 94670859, is 4330127017500000000 [4.33e18]. - * The minimum value of σ√τ is 0. - * - * If all y tokens are removed, Φ⁻¹(0/K) -> Φ⁻¹(1e-18) = `invariantTermY` = -8710427241990476442. - * For the swap to pass, the invariant needs to grow by at least 1. - * Assuming we started at k = 0, we need to find the `invariantTermX` that solves this: - * -> 1 = -8710427241990476442 - invariantTermX + σ√τ. - * We need the σ√τ term to help us, so we can set it to its maximum value. Now we have: - * -> 1 = -8710427241990476442 - invariantTermX + 4330127017500000000. - * -> invariantTermX <= -8710427241990476442 + 4330127017500000000 - 1. - * -> invariantTermX <= -4380300224490476443 [~-4.38e18]. - * -> Φ⁻¹(1-x) <= -4.380300224490476443. (not in wad anymore...) - * -> 1 - x <= Φ(-4.380300224490476443). - * -> x >= 1 - Φ(-4.380300224490476443). - * -> x >= ~0.999999218479338350565045237958. - * In conclusion, to take all y reserves, the x reserves must be __very close__ to its upper bound. - * - * - * If all x tokens are removed, `invariantTermX` = 8710427241990476442. - * For the swap to pass, the invariant needs to grow by at least 1. - * -> 1 = invariantTermY - 8710427241990476442 + σ√τ. - * We can use the σ√τ to help us, so we can set it to its maximum value. Now we have: - * -> 1 = invariantTermY - 8710427241990476442 + 4330127017500000000. - * -> invariantTermY >= 8710427241990476442 - 4330127017500000000 + 1. - * -> invariantTermY >= 4380300224490476443 [~4.38e18]. - * -> Φ⁻¹(y/K) >= 4.380300224490476443. (not in wad anymore...) - * -> y/K >= Φ(4.380300224490476443). - * -> y >= KΦ(4.380300224490476443). - * -> y >= K * ~0.999994074204725119575460221025. - * However, since the `invariantTermX` will be subtracted from `invariantTermY` first, it will - * revert the transaction with an arithemtic underflow unless the `invariantTermX` is equal - * to it's maximum value of 8710427241990476442, which is when the y reserves are at their - * upper bound of y = k. - * In conclusion, to take all x reserves, the y reserves __must__ be at its upper bound. + * In the case only one bound is reached, the other reserve must be at the opposite bound or else the transaction will revert + * with the "ReserveX/YMustBeAtBounds" error. + * + * To take all y reserves (lower bound), the x reserves __must__ be at its upper bound. + * To take all x reserves (lower bound), the y reserves __must__ be at its upper bound. * * This overwrite of "infinity" effectively makes the trading function return an actual value * at the bounds, instead of reverting. This is important for pools which might have accrued * enough fees to push one of it's reserves over the bounds. Additionally, a maximum and minimum * price are effectively enforced at these boundaries. * - * * note Adjusted trading function's invariant != original trading function's invariant. * * @return invariant k; Signed invariant of the pool. @@ -243,27 +210,49 @@ function tradingFunction(NormalCurve memory self) (uint256 upperBoundX, uint256 lowerBoundX) = self.getReserveXBounds(); // Overwrite the reserves to be as close to its bounds as possible, to avoid reverting. - uint256 invariantTermXInput; // x - 1 - if (self.reserveXPerWad >= upperBoundX) { - // As x -> 1E18, 1E18 - x -> 0, Φ⁻¹(0) = -∞, therefore bound invariantTermXInput to 1E18 - 1E18 + 1. - invariantTermXInput = 1; - } else if (self.reserveXPerWad <= lowerBoundX) { - // As x -> 0, 1E18 - 0 -> 1E18, Φ⁻¹(1E18) = +∞, therefore bound invariantTermXInput to 1E18 - 0 - 1. - invariantTermXInput = WAD - 1; + uint256 invariantTermXInput; // 1E18 - x + uint256 invariantTermYInput = + self.reserveYPerWad.divWadDown(self.strikePriceWad); // y/K + + if (self.reserveXPerWad >= upperBoundX || invariantTermYInput <= 0) { + // If x is at upper bound or y is at lower bound. + // Trading function is only valid y is at its lower bound when x is at its upper bound. + // If y is not at its lower bound, the invariant will be negative infinity, which will revert. + if (invariantTermYInput > 0) { + revert NormalStrategyLib_LowerReserveYBoundNotReached(); + } + // If x is not at its upper bound, the invariant will be negative infinity, which will revert. + if (self.reserveXPerWad < upperBoundX) { + revert NormalStrategyLib_UpperReserveXBoundNotReached(); + } + + // As x -> 1E18, 1E18 - x -> 0, Φ⁻¹(0) = -∞ + // As y -> 0, y/K -> 0, Φ⁻¹(0) = -∞ + // These terms cancel eachother out, therefore leaving the invariant to be equal to `stdDevSqrtTau`. + return int256(stdDevSqrtTau); + } else if (self.reserveXPerWad <= lowerBoundX || invariantTermYInput >= WAD) + { + // If x is at lower bound or y is at upper bound. + // Trading function is only valid if y is at its upper bound when x is at its lower bound. + // If y is not at its upper bound, the invariant will be positive infinity, which will revert. + if (invariantTermYInput < WAD) { + revert NormalStrategyLib_UpperReserveYBoundNotReached(); + } + + // If x is not at its lower bound, the invariant will be positive infinity, which will revert. + if (self.reserveXPerWad > lowerBoundX) { + revert NormalStrategyLib_LowerReserveXBoundNotReached(); + } + + // As x -> 0, 1E18 - 0 -> 1E18, Φ⁻¹(1E18) = +∞ + // As y -> K, y/K -> 1E18, Φ⁻¹(1E18) = +∞ + // These terms cancel eachother out, therefore leaving the invariant to be equal to `stdDevSqrtTau`. + return int256(stdDevSqrtTau); } else { + // Else no bounds have been reached and the invariant can be computed with both terms. invariantTermXInput = WAD - self.reserveXPerWad; } - uint256 invariantTermYInput = - self.reserveYPerWad.divWadDown(self.strikePriceWad); // y/K -> [0,1] - if (invariantTermYInput >= WAD) { - // As y -> K, y/K -> 1E18, Φ⁻¹(1E18) = +∞, therefore bound invariantTermYInput to 1E18 - 1. - invariantTermYInput = WAD - 1; - } else if (invariantTermYInput <= 0) { - // As y -> 0, y/K -> 0, Φ⁻¹(0) = -∞, therefore bound invariantTermYInput to 0 + 1. - invariantTermYInput = 1; - } - // Φ⁻¹(1-x) int256 invariantTermX = Gaussian.ppf(int256(invariantTermXInput)); // Φ⁻¹(y/K) @@ -290,9 +279,9 @@ function approximateXGivenY(NormalCurve memory self) { (uint256 upperBoundX, uint256 lowerBoundX) = self.getReserveXBounds(); (uint256 upperBoundY, uint256 lowerBoundY) = self.getReserveYBounds(); - // If y reserves has reached upper bound, x reserves is zero. + // If y reserves has reached upper bound, x reserves must be zero (lower bound). if (self.reserveYPerWad >= upperBoundY) return lowerBoundX; - // If y reserves has reached lower bound, x reserves is one. + // If y reserves has reached lower bound, x reserves must be one wad (upper bound). if (self.reserveYPerWad <= lowerBoundY) return upperBoundX; // σ√τ uint256 stdDevSqrtTau = self.computeStdDevSqrtTau(); @@ -323,9 +312,9 @@ function approximateYGivenX(NormalCurve memory self) { (uint256 upperBoundX, uint256 lowerBoundX) = self.getReserveXBounds(); (uint256 upperBoundY, uint256 lowerBoundY) = self.getReserveYBounds(); - // If x reserves has reached upper bound, y reserves is zero. + // If x reserves has reached upper bound, y reserves must be zero (lower bound). if (self.reserveXPerWad >= upperBoundX) return lowerBoundY; - // If x reserves has reached lower bound, y reserves is equal to the strike price. + // If x reserves has reached lower bound, y reserves must be equal to the strike price (upper bound). if (self.reserveXPerWad <= lowerBoundX) return upperBoundY; // σ√τ uint256 stdDevSqrtTau = self.computeStdDevSqrtTau(); diff --git a/test/TestPortfolioInvariant.t.sol b/test/TestPortfolioInvariant.t.sol index e3948510..806b8165 100644 --- a/test/TestPortfolioInvariant.t.sol +++ b/test/TestPortfolioInvariant.t.sol @@ -51,6 +51,9 @@ contract TestPortfolioInvariant is Setup { uint256 volSqrtYearsWad = volatilityWad.mulWadDown(sqrtTauWad); // y / K uint256 quotientWad = reserveYPerWad.divWadUp(strikePriceWad); // todo: should this round up?? + if (quotientWad >= 1e18) { + quotientWad = reserveYPerWad.divWadDown(strikePriceWad); + } console2.log(reserveYPerWad, strikePriceWad); console2.log("quotientWad", quotientWad); // Φ⁻¹(y/K) @@ -258,6 +261,8 @@ contract TestPortfolioInvariant is Setup { deltaX = bound(deltaX, MINIMUM_DELTA, reserveXPerWad - MINIMUM_RESERVE_X); + console.log(reserveYPerWad); + int256 previousResult = tradingFunction( NormalCurve( reserveXPerWad, @@ -418,9 +423,10 @@ contract TestPortfolioInvariant is Setup { bound(deltaY, MINIMUM_DELTA, reserveYPerWad - MINIMUM_RESERVE_Y); uint256 prevQuotient = reserveYPerWad.divWadUp(strikePriceWad); - uint256 quotient = (reserveYPerWad - deltaY).divWadUp(strikePriceWad); + uint256 quotient = (reserveYPerWad - deltaY).divWadDown(strikePriceWad); console.log("prevQuotient: ", prevQuotient); console.log("quotient: ", quotient); + vm.assume(quotient > 0 && quotient < 1 ether); /// Make sure quotient is within bounds and it decreases by at least 2 vm.assume(prevQuotient >= MINIMUM_QUOTIENT_DELTA); @@ -502,6 +508,7 @@ contract TestPortfolioInvariant is Setup { // The Y reserve is bounded between the min delta and the strike price less min delta. // min y <= y <= strike - min delta + // Y reserve must be large enough to be divided by the strike price and not be zero. reserveYPerWad = bound( reserveYPerWad, MINIMUM_RESERVE_Y + MINIMUM_DELTA, @@ -518,7 +525,7 @@ contract TestPortfolioInvariant is Setup { timeRemainingSec = bound(timeRemainingSec, MIN_DURATION, MAX_DURATION); // Need to make sure the computations in the function are valid. - uint256 quotient = reserveYPerWad.divWadUp(strikePriceWad); + uint256 quotient = reserveYPerWad.divWadDown(strikePriceWad); uint256 difference = 1 ether - reserveXPerWad; vm.assume(quotient > 0 && quotient < 1 ether); vm.assume(difference > 0 && difference < 1 ether); @@ -569,7 +576,7 @@ contract TestPortfolioInvariant is Setup { : postReserve.divWadUp(strikePriceWad) ); - console.log("PPFs"); + /* console.log("PPFs"); console.logInt( Gaussian.ppf( int256( @@ -587,7 +594,7 @@ contract TestPortfolioInvariant is Setup { : 1 ether - reserveXPerWad ) ) - ); + ); */ console.log( "stdDevSqrtTau", volatilityWad.mulWadDown( diff --git a/test/TestPortfolioSwap.t.sol b/test/TestPortfolioSwap.t.sol index 41fd4e82..068e15d5 100644 --- a/test/TestPortfolioSwap.t.sol +++ b/test/TestPortfolioSwap.t.sol @@ -555,19 +555,10 @@ contract TestPortfolioSwap is Setup { // This swap takes all of the quote token "y" out of the pool. // Therefore, we will hit the "lower bound" case for the "y" reserves. // This will make the invariant y term `Gaussian.ppf(1)`, which is -8710427241990476442. - // Then it will subtract the invariantTermX and add the stdDevSqrtTau term. - // The minimum value for the invariantTermX is the same, `Gaussian.ppf(1)`, which is -8710427241990476442. - // Therefore, the x reserve needs to be close to its upper bound, since the input to the ppf is 1 - x. - // If it is close or at the boundary, it will cancel out the invariant y term. - // If its not close, the stdDevSqrtTau term will need to be large enough to close the difference, - // which might be too large than that term could be. - vm.expectRevert( - abi.encodeWithSelector( - Portfolio_InvalidInvariant.selector, - int256(938), - int256(-8707810991428151127) - ) - ); + // This requires the asset token "x" reserves to be at the upper bound of 1 WAD. + // If both reserves are not at opposite bounds, it will revert. + // In this case, it will revert with the `NormalStrategyLib_UpperReserveXBoundNotReached` error. + vm.expectRevert(NormalStrategyLib_UpperReserveXBoundNotReached.selector); subject_.swap(order); } diff --git a/test/tradingFunction.t.sol b/test/tradingFunction.t.sol new file mode 100644 index 00000000..e30a65b7 --- /dev/null +++ b/test/tradingFunction.t.sol @@ -0,0 +1,400 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.4; + +import "./Setup.sol"; + +/** + * @dev Please, solidity, add support for fixed sized arrays in storage! + * + * [NormalCurve({ + * reserveXPerWad: 0, + * reserveYPerWad: 0, + * strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + * standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS * 1e14, // 10% + * timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, + * invariant: 0 + * }), + * NormalCurve({ + * reserveXPerWad: 0, + * reserveYPerWad: 0, + * strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + * standardDeviationWad: MIN_VOLATILITY * 1e14, // .01% + * timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, // 1 days in seconds + * invariant: 0 + * }), + * NormalCurve({ + * reserveXPerWad: 0, + * reserveYPerWad: 0, + * strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + * standardDeviationWad: MAX_VOLATILITY * 1e14, // 250%, + * timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, + * invariant: 0 + * }), + * NormalCurve({ + * reserveXPerWad: 0, + * reserveYPerWad: 0, + * strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + * standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS * 1e14, // 10%, + * timeRemainingSeconds: MIN_DURATION, + * invariant: 0 + * }), + * NormalCurve({ + * reserveXPerWad: 0, + * reserveYPerWad: 0, + * strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + * standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS * 1e14, // 10% + * timeRemainingSeconds: MAX_DURATION, + * invariant: 0 + * }) + * ]; + * @author alexangelj + * @notice Uses the branching tree technique (BTT), defined in tradingFunction.tree. + */ +contract TestTradingFunction is Setup { + NormalCurve[] public profiles; + + function setUp() public override { + super.setUp(); + + profiles.push( + NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS + * 1e14, // 10% + timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, + invariant: 0 + }) + ); + + profiles.push( + NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + standardDeviationWad: MIN_VOLATILITY * 1e14, // .01% + timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, // 1 days in seconds + invariant: 0 + }) + ); + + profiles.push( + NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + standardDeviationWad: MAX_VOLATILITY * 1e14, // 250%, + timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, + invariant: 0 + }) + ); + + profiles.push( + NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS + * 1e14, // 10%, + timeRemainingSeconds: MIN_DURATION, + invariant: 0 + }) + ); + + profiles.push( + NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, + standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS + * 1e14, // 10% + timeRemainingSeconds: MAX_DURATION, + invariant: 0 + }) + ); + } + + uint256 public UPPER_BOUND = WAD; + uint256 public LOWER_BOUND = 0; + + /// @dev Trading function is monotonic when reserves change by at least more than 1 wei. + /// This is because a 1 wei change is a rounding error for the trading function. + uint256 public MONOTONICITY_DELTA = 2; + + NormalCurve defaultCurve = NormalCurve({ + reserveXPerWad: 0, + reserveYPerWad: 0, + strikePriceWad: NormalConfiguration_DEFAULT_STRIKE_WAD, // $1 + standardDeviationWad: NormalConfiguration_DEFAULT_VOLATILITY_BPS * 1e14, // 100% + timeRemainingSeconds: NormalConfiguration_DEFAULT_DURATION_SEC, // 1 days in seconds + invariant: 0 + }); + + NormalCurve normalCurve = defaultCurve; + + modifier whenReserveXPerWadIsAtTheUpperBound() { + normalCurve.reserveXPerWad = UPPER_BOUND; + _; + delete normalCurve; + } + + modifier whenReserveYPerWadIsNotAtTheLowerBound() { + normalCurve.reserveYPerWad++; + _; + delete normalCurve; + } + + function test_WhenReserveYPerWadIsNotAtTheLowerBound() + external + whenReserveXPerWadIsAtTheUpperBound + whenReserveYPerWadIsNotAtTheLowerBound + { + // It should revert with `NormalStrategyLib_LowerReserveYBoundNotReached` + vm.expectRevert(NormalStrategyLib_LowerReserveYBoundNotReached.selector); + normalCurve.tradingFunction(); + } + + modifier whenReserveYPerWadIsAtTheLowerBound() { + normalCurve.reserveYPerWad = LOWER_BOUND; + _; + } + + function test_WhenReserveYPerWadIsAtTheLowerBound() + external + whenReserveXPerWadIsAtTheUpperBound + whenReserveYPerWadIsAtTheLowerBound + { + // It should return σ√τ + int256 expected = int256(normalCurve.computeStdDevSqrtTau()); + int256 actual = normalCurve.tradingFunction(); + assertEq( + actual, expected, "tradingFunction should return stdDevSqrtTau" + ); + } + + modifier whenReserveXPerWadIsNotAtTheUpperBound() { + normalCurve.reserveXPerWad++; + _; + } + + function test_WhenReserveXPerWadIsNotAtTheUpperBound() + external + whenReserveYPerWadIsAtTheLowerBound + whenReserveXPerWadIsNotAtTheUpperBound + { + // It should revert with `NormalStrategyLib_UpperReserveXBoundNotReached` + vm.expectRevert(NormalStrategyLib_UpperReserveXBoundNotReached.selector); + normalCurve.tradingFunction(); + } + + function test_WhenReserveXPerWadIsAtTheUpperBound() + external + whenReserveYPerWadIsAtTheLowerBound + whenReserveXPerWadIsAtTheUpperBound + { + // It should return σ√τ + int256 expected = int256(normalCurve.computeStdDevSqrtTau()); + int256 actual = normalCurve.tradingFunction(); + assertEq( + actual, expected, "tradingFunction should return stdDevSqrtTau" + ); + } + + modifier whenReserveXPerWadIsAtTheLowerBound() { + normalCurve.reserveXPerWad = LOWER_BOUND; + _; + } + + modifier whenReserveYPerWadIsNotAtTheUpperBound() { + if (normalCurve.reserveYPerWad >= UPPER_BOUND) { + normalCurve.reserveYPerWad--; + } + + if (normalCurve.reserveYPerWad == LOWER_BOUND) { + normalCurve.reserveYPerWad++; + } + _; + } + + function test_WhenReserveYPerWadIsNotAtTheUpperBound() + external + whenReserveXPerWadIsAtTheLowerBound + whenReserveYPerWadIsNotAtTheUpperBound + { + // It should revert with `NormalStrategyLib_UpperReserveYBoundNotReached` + vm.expectRevert(NormalStrategyLib_UpperReserveYBoundNotReached.selector); + normalCurve.tradingFunction(); + } + + modifier whenReserveYPerWadIsAtTheUpperBound() { + normalCurve.reserveYPerWad = UPPER_BOUND; + _; + } + + function test_WhenReserveYPerWadIsAtTheUpperBound() + external + whenReserveXPerWadIsAtTheLowerBound + whenReserveYPerWadIsAtTheUpperBound + { + // It should return σ√τ if reserveYPerWad + int256 expected = int256(normalCurve.computeStdDevSqrtTau()); + int256 actual = normalCurve.tradingFunction(); + assertEq( + actual, expected, "tradingFunction should return stdDevSqrtTau" + ); + } + + modifier whenReserveXPerWadIsNotAtTheLowerBound() { + if (normalCurve.reserveXPerWad <= LOWER_BOUND) { + normalCurve.reserveXPerWad++; + } + _; + } + + function test_WhenReserveXPerWadIsNotAtTheLowerBound() + external + whenReserveYPerWadIsAtTheUpperBound + whenReserveXPerWadIsNotAtTheLowerBound + { + // It should revert with `NormalStrategyLib_LowerReserveXBoundNotReached` + vm.expectRevert(NormalStrategyLib_LowerReserveXBoundNotReached.selector); + normalCurve.tradingFunction(); + } + + function test_WhenReserveXPerWadIsAtTheLowerBound() + external + whenReserveYPerWadIsAtTheUpperBound + whenReserveXPerWadIsAtTheLowerBound + { + // It should return σ√τ + int256 expected = int256(normalCurve.computeStdDevSqrtTau()); + int256 actual = normalCurve.tradingFunction(); + assertEq( + actual, expected, "tradingFunction should return stdDevSqrtTau" + ); + } + + modifier whenReserveXPerWadAndReserveYPerWadAreBothNotAtAnyBound() { + if (normalCurve.reserveXPerWad <= LOWER_BOUND) { + normalCurve.reserveXPerWad++; + } + + if (normalCurve.reserveXPerWad >= UPPER_BOUND) { + normalCurve.reserveXPerWad--; + } + + if (normalCurve.reserveYPerWad <= LOWER_BOUND) { + normalCurve.reserveYPerWad++; + } + + if (normalCurve.reserveYPerWad >= UPPER_BOUND) { + normalCurve.reserveYPerWad--; + } + _; + } + + function test_WhenReserveXPerWadAndReserveYPerWadAreBothNotAtAnyBound() + external + whenReserveXPerWadAndReserveYPerWadAreBothNotAtAnyBound + { + // It should not revert + // It should return `Φ⁻¹(y/K) - Φ⁻¹(1-x) + σ√τ` + uint256 stdDevSqrtTau = normalCurve.computeStdDevSqrtTau(); + int256 invariant = normalCurve.tradingFunction(); + assertTrue( + invariant != int256(stdDevSqrtTau), + "tradingFunction should not return only stdDevSqrtTau" + ); + } + + function test_fuzz_WhenReserveXPerWadAndReserveYPerWadAreBothNotAtAnyBound( + uint256 seed + ) external whenReserveXPerWadAndReserveYPerWadAreBothNotAtAnyBound { + normalCurve.reserveXPerWad = + bound(seed, LOWER_BOUND + 1, UPPER_BOUND - 1); + normalCurve.reserveYPerWad = + bound(seed, LOWER_BOUND + 1, normalCurve.strikePriceWad - 1); + + // It should not revert + // It should return `Φ⁻¹(y/K) - Φ⁻¹(1-x) + σ√τ` + uint256 stdDevSqrtTau = normalCurve.computeStdDevSqrtTau(); + int256 invariant = normalCurve.tradingFunction(); + assertTrue( + invariant != int256(stdDevSqrtTau), + "tradingFunction should not return only stdDevSqrtTau" + ); + } + + modifier whenUsingAllParameterProfiles(uint256 rand) { + uint256 index = rand % profiles.length; + normalCurve = profiles[index]; + (normalCurve.reserveXPerWad, normalCurve.reserveYPerWad) = normalCurve + .approximateReservesGivenPrice(normalCurve.strikePriceWad); + _; + } + + modifier whenIncreasingReserveXPerWad() { + int256 prev = normalCurve.tradingFunction(); + normalCurve.reserveXPerWad += MONOTONICITY_DELTA; + _; + int256 post = normalCurve.tradingFunction(); + assertTrue(post > prev, "Invariant should increase"); + } + + function test_fuzz_WhenIncreasingReserveXPerWad(uint256 rand) + external + whenUsingAllParameterProfiles(rand) + whenIncreasingReserveXPerWad + { + // It should increase the invariant + } + + modifier whenIncreasingReserveYPerWad() { + int256 prev = normalCurve.tradingFunction(); + normalCurve.reserveYPerWad += MONOTONICITY_DELTA; + _; + int256 post = normalCurve.tradingFunction(); + assertTrue(post > prev, "Invariant should increase"); + } + + function test_fuzz_WhenIncreasingReserveYPerWad(uint256 rand) + external + whenUsingAllParameterProfiles(rand) + whenIncreasingReserveYPerWad + { + // It should increase the invariant + } + + modifier whenDecreasingReserveXPerWad() { + int256 prev = normalCurve.tradingFunction(); + normalCurve.reserveXPerWad -= MONOTONICITY_DELTA; + _; + int256 post = normalCurve.tradingFunction(); + assertTrue(post < prev, "Invariant should decrease"); + } + + function test_fuzz_WhenDecreasingReserveXPerWad(uint256 rand) + external + whenUsingAllParameterProfiles(rand) + whenDecreasingReserveXPerWad + { + // It should decrease the invariant + } + + modifier whenDecreasingReserveYPerWad() { + int256 prev = normalCurve.tradingFunction(); + normalCurve.reserveYPerWad -= MONOTONICITY_DELTA; + _; + int256 post = normalCurve.tradingFunction(); + assertTrue(post < prev, "Invariant should decrease"); + } + + function test_fuzz_WhenDecreasingReserveYPerWad(uint256 rand) + external + whenUsingAllParameterProfiles(rand) + whenDecreasingReserveYPerWad + { + // It should decrease the invariant + } +} diff --git a/test/tradingFunction.tree b/test/tradingFunction.tree new file mode 100644 index 00000000..4436f851 --- /dev/null +++ b/test/tradingFunction.tree @@ -0,0 +1,33 @@ +TestTradingFunction +├── When reserveXPerWad is at the upper bound +│ └── When reserveYPerWad is not at the lower bound + │ └── It should revert with `NormalStrategyLib_LowerReserveYBoundNotReached` +│ └── When reserveYPerWad is at the lower bound + │ └── It should return σ√τ +├── When reserveYPerWad is at the lower bound +│ └── When reserveXPerWad is not at the upper bound + │ └── It should revert with `NormalStrategyLib_UpperReserveXBoundNotReached` +│ └── When reserveXPerWad is at the upper bound + │ └── It should return σ√τ +├── When reserveXPerWad is at the lower bound +│ └── When reserveYPerWad is not at the upper bound + │ └── It should revert with `NormalStrategyLib_UpperReserveYBoundNotReached` +│ └── When reserveYPerWad is at the upper bound + │ └── It should return σ√τ if reserveYPerWad +├── When reserveYPerWad is at the upper bound +│ └── When reserveXPerWad is not at the lower bound + │ └── It should revert with `NormalStrategyLib_LowerReserveXBoundNotReached` +│ └── When reserveXPerWad is at the lower bound + │ └── It should return σ√τ +├── When reserveXPerWad and reserveYPerWad are both not at any bound +│ └── It should not revert +│ └── It should return `Φ⁻¹(y/K) - Φ⁻¹(1-x) + σ√τ` +├── When using all parameter profiles +│ └── When increasing reserveXPerWad + │ └── It should increase the invariant +│ └── When increasing reserveYPerWad + │ └── It should increase the invariant +│ └── When decreasing reserveXPerWad + │ └── It should decrease the invariant +│ └── When decreasing reserveYPerWad + │ └── It should decrease the invariant \ No newline at end of file