Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(#439): updates trading function bound case v1.4.0-beta #441

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 55 additions & 66 deletions contracts/strategies/NormalStrategyLib.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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)
Expand All @@ -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();
Expand Down Expand Up @@ -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();
Expand Down
15 changes: 11 additions & 4 deletions test/TestPortfolioInvariant.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -569,7 +576,7 @@ contract TestPortfolioInvariant is Setup {
: postReserve.divWadUp(strikePriceWad)
);

console.log("PPFs");
/* console.log("PPFs");
console.logInt(
Gaussian.ppf(
int256(
Expand All @@ -587,7 +594,7 @@ contract TestPortfolioInvariant is Setup {
: 1 ether - reserveXPerWad
)
)
);
); */
console.log(
"stdDevSqrtTau",
volatilityWad.mulWadDown(
Expand Down
17 changes: 4 additions & 13 deletions test/TestPortfolioSwap.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading