From 6bec234205f90232f39937ccc8d02614d24ec1f2 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Thu, 10 Oct 2024 16:30:20 -0400 Subject: [PATCH 1/8] feat: allow equal starting end ending ticks --- src/Doppler.sol | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index 0d980ae6..ac444a34 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -96,8 +96,15 @@ contract Doppler is BaseHook { /* Tick checks */ // Starting tick must be greater than ending tick if isToken0 // Ending tick must be greater than starting tick if isToken1 - if (_isToken0 && _startingTick <= _endingTick) revert InvalidTickRange(); - if (!_isToken0 && _startingTick >= _endingTick) revert InvalidTickRange(); + if (_startingTick != endingTick) { + if (_isToken0 && _startingTick <= _endingTick) revert InvalidTickRange(); + if (!_isToken0 && _startingTick >= _endingTick) revert InvalidTickRange(); + + int24 totalTickDelta = _isToken0 ? _startingTick - _endingTick : _endingTick - _startingTick; + int256 totalEpochs = int256((_endingTime - _startingTime) / _epochLength); + // DA worst case is starting tick - ending tick + if (_gamma * totalEpochs != totalTickDelta) revert InvalidGamma(); + } // Enforce maximum tick spacing if (_poolKey.tickSpacing > MAX_TICK_SPACING) revert InvalidTickSpacing(); @@ -116,10 +123,6 @@ contract Doppler is BaseHook { /* Gamma checks */ // Enforce that the total tick delta is divisible by the total number of epochs - int24 totalTickDelta = _isToken0 ? _startingTick - _endingTick : _endingTick - _startingTick; - int256 totalEpochs = int256((_endingTime - _startingTime) / _epochLength); - // DA worst case is starting tick - ending tick - if (_gamma * totalEpochs != totalTickDelta) revert InvalidGamma(); // Enforce that gamma is divisible by tick spacing if (_gamma % _poolKey.tickSpacing != 0) revert InvalidGamma(); From 687c5dafd00e38c9eb4b6d788691879d49a64830 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Fri, 18 Oct 2024 16:14:02 -0400 Subject: [PATCH 2/8] startTick env var --- test/shared/BaseTest.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/shared/BaseTest.sol b/test/shared/BaseTest.sol index 8a1c574c..1e5af22e 100644 --- a/test/shared/BaseTest.sol +++ b/test/shared/BaseTest.sol @@ -152,8 +152,8 @@ contract BaseTest is Test, Deployers { // isToken0 ? startTick > endTick : endTick > startTick // In both cases, price(startTick) > price(endTick) - startTick = isToken0 ? DEFAULT_START_TICK : -DEFAULT_START_TICK; - endTick = isToken0 ? -DEFAULT_END_TICK : DEFAULT_END_TICK; + startTick = isToken0 ? int24(vm.envOr("START_TICK", DEFAULT_START_TICK)) : -int24(vm.envOr("START_TICK", DEFAULT_START_TICK)); + endTick = isToken0 ? -int24(vm.envOr("END_TICK", DEFAULT_END_TICK)) : int24(vm.envOr("END_TICK", DEFAULT_END_TICK)); // Default to feeless case because it's easier to reason about config.fee = uint24(vm.envOr("FEE", uint24(0))); From cb2443ae24f11c19685662e5688d65e2c1b934e1 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Fri, 18 Oct 2024 16:45:16 -0400 Subject: [PATCH 3/8] fix typo --- src/Doppler.sol | 2 +- test/shared/BaseTest.sol | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index ac444a34..5292475d 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -96,7 +96,7 @@ contract Doppler is BaseHook { /* Tick checks */ // Starting tick must be greater than ending tick if isToken0 // Ending tick must be greater than starting tick if isToken1 - if (_startingTick != endingTick) { + if (_startingTick != _endingTick) { if (_isToken0 && _startingTick <= _endingTick) revert InvalidTickRange(); if (!_isToken0 && _startingTick >= _endingTick) revert InvalidTickRange(); diff --git a/test/shared/BaseTest.sol b/test/shared/BaseTest.sol index 1e5af22e..c3464984 100644 --- a/test/shared/BaseTest.sol +++ b/test/shared/BaseTest.sol @@ -152,8 +152,8 @@ contract BaseTest is Test, Deployers { // isToken0 ? startTick > endTick : endTick > startTick // In both cases, price(startTick) > price(endTick) - startTick = isToken0 ? int24(vm.envOr("START_TICK", DEFAULT_START_TICK)) : -int24(vm.envOr("START_TICK", DEFAULT_START_TICK)); - endTick = isToken0 ? -int24(vm.envOr("END_TICK", DEFAULT_END_TICK)) : int24(vm.envOr("END_TICK", DEFAULT_END_TICK)); + startTick = isToken0 ? int24(vm.envOr("START_TICK", DEFAULT_START_TICK)) : int24(vm.envOr("START_TICK", -DEFAULT_START_TICK)); + endTick = isToken0 ? int24(vm.envOr("END_TICK", -DEFAULT_END_TICK)) : int24(vm.envOr("END_TICK", DEFAULT_END_TICK)); // Default to feeless case because it's easier to reason about config.fee = uint24(vm.envOr("FEE", uint24(0))); From a9cad4ae70555dfad83d63e05875244dfd17da77 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Mon, 21 Oct 2024 14:46:17 -0400 Subject: [PATCH 4/8] fix: constructor tests --- test/unit/Constructor.t.sol | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/test/unit/Constructor.t.sol b/test/unit/Constructor.t.sol index 754e23c1..7dc53f70 100644 --- a/test/unit/Constructor.t.sol +++ b/test/unit/Constructor.t.sol @@ -54,6 +54,9 @@ contract ConstructorTest is BaseTest { if (selector != 0) { vm.expectRevert(selector); } + + int24 startTick = _startTick != 0 ? _startTick : isToken0 ? DEFAULT_START_TICK : -DEFAULT_START_TICK; + int24 endTick = _endTick != 0 ? _endTick : isToken0 ? -DEFAULT_END_TICK : DEFAULT_END_TICK; deployCodeTo( "DopplerImplementation.sol:DopplerImplementation", abi.encode( @@ -64,8 +67,8 @@ contract ConstructorTest is BaseTest { config.maximumProceeds, config.startingTime, config.endingTime, - _startTick, - _endTick, + startTick, + endTick, config.epochLength, config.gamma, isToken0, @@ -124,7 +127,7 @@ contract ConstructorTest is BaseTest { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; config.tickSpacing = int24(maxTickSpacing + 1); - deployDoppler(InvalidTickSpacing.selector, config, 200, 100, true); + deployDoppler(InvalidTickSpacing.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidTimeRange_WhenStartingTimeGreaterThanOrEqualToEndingTime() public { @@ -132,7 +135,7 @@ contract ConstructorTest is BaseTest { config.startingTime = 1000; config.endingTime = 1000; - deployDoppler(InvalidTimeRange.selector, config, 200, 100, true); + deployDoppler(InvalidTimeRange.selector, config, DEFAULT_START_TICK, DEFAULT_START_TICK, true); } function testConstructor_RevertsInvalidGamma_WhenGammaCalculationZero() public { @@ -142,23 +145,21 @@ contract ConstructorTest is BaseTest { config.epochLength = 1; config.gamma = 0; - deployDoppler(InvalidGamma.selector, config, 200, 100, true); + deployDoppler(InvalidGamma.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidEpochLength_WhenTimeDeltaNotDivisibleByEpochLength() public { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; - config.startingTime = 1000; - config.endingTime = 5000; config.epochLength = 3000; - deployDoppler(InvalidEpochLength.selector, config, 200, 100, true); + deployDoppler(InvalidEpochLength.selector, config, DEFAULT_START_TICK, DEFAULT_START_TICK, true); } function testConstructor_RevertsInvalidGamma_WhenGammaNotDivisibleByTickSpacing() public { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; config.gamma += 1; - deployDoppler(InvalidGamma.selector, config, 200, 100, true); + deployDoppler(InvalidGamma.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidGamma_WhenGammaTimesTotalEpochsNotDivisibleByTotalTickDelta() public { @@ -168,28 +169,28 @@ contract ConstructorTest is BaseTest { config.endingTime = 5000; config.epochLength = 1000; - deployDoppler(InvalidGamma.selector, config, 200, 100, true); + deployDoppler(InvalidGamma.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidGamma_WhenGammaIsNegative() public { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; config.gamma = -1; - deployDoppler(InvalidGamma.selector, config, 200, 100, true); + deployDoppler(InvalidGamma.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidNumPDSlugs_WithZeroSlugs() public { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; config.numPDSlugs = 0; - deployDoppler(InvalidNumPDSlugs.selector, config, 0, -172_800, true); + deployDoppler(InvalidNumPDSlugs.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidNumPDSlugs_GreaterThanMax() public { DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; config.numPDSlugs = MAX_PRICE_DISCOVERY_SLUGS + 1; - deployDoppler(InvalidNumPDSlugs.selector, config, 0, -172_800, true); + deployDoppler(InvalidNumPDSlugs.selector, config, 0, 0, true); } function testConstructor_RevertsInvalidProceedLimits_WhenMinimumProceedsGreaterThanMaximumProceeds() public { @@ -197,16 +198,14 @@ contract ConstructorTest is BaseTest { config.minimumProceeds = 100; config.maximumProceeds = 0; - deployDoppler(InvalidProceedLimits.selector, config, 0, -172_800, true); + deployDoppler(InvalidProceedLimits.selector, config, 0, 0, true); } function testConstructor_Succeeds_WithValidParameters() public { bool _isToken0 = true; - int24 _startTick = 0; - int24 _endTick = -172_800; DopplerConfig memory config = DEFAULT_DOPPLER_CONFIG; - deployDoppler(0, config, _startTick, _endTick, _isToken0); + deployDoppler(0, config, 0, 0, _isToken0); } } From e1d303ff2abaefe22c14ba3a1942938195c76cab Mon Sep 17 00:00:00 2001 From: kinrezc Date: Mon, 21 Oct 2024 16:26:01 -0400 Subject: [PATCH 5/8] fix: assetAvailable bug --- src/Doppler.sol | 12 ++++++------ test/shared/DopplerImplementation.sol | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index 5292475d..85cc94db 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -346,7 +346,6 @@ contract Doppler is BaseHook { * int256(1e18 - FullMath.mulDiv(totalTokensSold_, 1e18, expectedAmountSold)) / 1e18; } else { int24 tauTick = startingTick + int24(state.tickAccumulator / 1e18); - Position memory pdSlug = positions[DISCOVERY_SLUG_SALT]; // Safe from overflow since the result is <= gamma which is an int24 already int24 computedRange = int24(_getGammaShare() * gamma / 1e18); @@ -428,9 +427,10 @@ contract Doppler is BaseHook { SlugData memory lowerSlug = _computeLowerSlugData(key, requiredProceeds, numeraireAvailable, totalTokensSold_, tickLower, currentTick); - SlugData memory upperSlug = _computeUpperSlugData(key, totalTokensSold_, currentTick, assetAvailable); + (SlugData memory upperSlug, uint256 assetRemaining) = _computeUpperSlugData(key, totalTokensSold_, currentTick, assetAvailable); SlugData[] memory priceDiscoverySlugs = - _computePriceDiscoverySlugsData(key, upperSlug, tickUpper, assetAvailable); + _computePriceDiscoverySlugsData(key, upperSlug, tickUpper, assetRemaining); + // TODO: If we're not actually modifying liquidity, skip below logic // TODO: Consider whether we need slippage protection @@ -611,7 +611,7 @@ contract Doppler is BaseHook { uint256 totalTokensSold_, int24 currentTick, uint256 assetAvailable - ) internal view returns (SlugData memory slug) { + ) internal view returns (SlugData memory slug, uint256 assetRemaining) { int256 tokensSoldDelta = int256(_getExpectedAmountSoldWithEpochOffset(1)) - int256(totalTokensSold_); // compute if we've sold more or less tokens than expected by next epoch uint256 tokensToLp; @@ -636,7 +636,7 @@ contract Doppler is BaseHook { TickMath.getSqrtPriceAtTick(slug.tickUpper), tokensToLp ); - assetAvailable -= tokensToLp; + assetRemaining = assetAvailable -tokensToLp; } else { slug.liquidity = 0; } @@ -814,7 +814,7 @@ contract Doppler is BaseHook { (, int24 tickUpper) = _getTicksBasedOnState(int24(0), key.tickSpacing); - SlugData memory upperSlug = _computeUpperSlugData(key, 0, tick, numTokensToSell); + (SlugData memory upperSlug, ) = _computeUpperSlugData(key, 0, tick, numTokensToSell); SlugData[] memory priceDiscoverySlugs = _computePriceDiscoverySlugsData(key, upperSlug, tickUpper, numTokensToSell); diff --git a/test/shared/DopplerImplementation.sol b/test/shared/DopplerImplementation.sol index a1f637b0..3cf81f03 100644 --- a/test/shared/DopplerImplementation.sol +++ b/test/shared/DopplerImplementation.sol @@ -143,7 +143,7 @@ contract DopplerImplementation is Doppler { uint256 totalTokensSold, int24 currentTick, uint256 assetAvailable - ) public view returns (SlugData memory) { + ) public view returns (SlugData memory, uint256 assetRemaining) { return _computeUpperSlugData(poolKey, totalTokensSold, currentTick, assetAvailable); } From 95dcd0258cce11b345f9fd89e6bdf35167f285f1 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Mon, 21 Oct 2024 20:16:26 -0400 Subject: [PATCH 6/8] fix: maxDutchAuction test --- src/Doppler.sol | 4 +++- test/integration/Rebalance.t.sol | 20 +++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index 85cc94db..712220a4 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -17,6 +17,7 @@ import {FixedPoint96} from "v4-periphery/lib/v4-core/src/libraries/FixedPoint96. import {TransientStateLibrary} from "v4-periphery/lib/v4-core/src/libraries/TransientStateLibrary.sol"; import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {ProtocolFeeLibrary} from "v4-periphery/lib/v4-core/src/libraries/ProtocolFeeLibrary.sol"; +import "forge-std/console.sol"; struct SlugData { int24 tickLower; @@ -424,6 +425,7 @@ contract Doppler is BaseHook { numeraireAvailable = uint256(uint128(tokensRemoved.amount0())); assetAvailable = uint256(uint128(tokensRemoved.amount1())) + key.currency1.balanceOfSelf(); } + console.log("numAvail", numeraireAvailable); SlugData memory lowerSlug = _computeLowerSlugData(key, requiredProceeds, numeraireAvailable, totalTokensSold_, tickLower, currentTick); @@ -636,10 +638,10 @@ contract Doppler is BaseHook { TickMath.getSqrtPriceAtTick(slug.tickUpper), tokensToLp ); - assetRemaining = assetAvailable -tokensToLp; } else { slug.liquidity = 0; } + assetRemaining = assetAvailable - tokensToLp; } function _computePriceDiscoverySlugsData( diff --git a/test/integration/Rebalance.t.sol b/test/integration/Rebalance.t.sol index b835c49c..06f6b2a9 100644 --- a/test/integration/Rebalance.t.sol +++ b/test/integration/Rebalance.t.sol @@ -23,6 +23,8 @@ import {InvalidTime, SwapBelowRange} from "src/Doppler.sol"; import {BaseTest} from "test/shared/BaseTest.sol"; import {Position} from "../../src/Doppler.sol"; +import "forge-std/console.sol"; + contract RebalanceTest is BaseTest { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -107,6 +109,9 @@ contract RebalanceTest is BaseTest { } // Validate that each price discovery slug has liquidity + // console.log("slug liq", priceDiscoverySlugs[i].liquidity); + // console.log("slug upper", priceDiscoverySlugs[i].tickUpper); + // console.log("slug lower", priceDiscoverySlugs[i].tickLower); assertGt(priceDiscoverySlugs[i].liquidity, 0); } @@ -555,7 +560,6 @@ contract RebalanceTest is BaseTest { vm.warp(hook.getStartingTime()); PoolKey memory poolKey = key; - swapRouter.swap( // Swap numeraire to asset // If zeroForOne, we use max price limit (else vice versa) @@ -565,7 +569,7 @@ contract RebalanceTest is BaseTest { "" ); - (uint40 lastEpoch, int256 tickAccumulator, uint256 totalTokensSold,, uint256 totalTokensSoldLastEpoch,) = + (uint40 lastEpoch,, uint256 totalTokensSold,, uint256 totalTokensSoldLastEpoch,) = hook.state(); assertEq(lastEpoch, 1); @@ -596,6 +600,7 @@ contract RebalanceTest is BaseTest { assertEq(totalTokensSoldLastEpoch2, 1 ether); vm.warp(hook.getStartingTime() + hook.getEpochLength() * 2); // Next epoch + SlugVis.visualizeSlugs(hook, poolKey.toId(), "test", block.timestamp); // We swap again just to trigger the rebalancing logic in the new epoch swapRouter.swap( @@ -631,10 +636,6 @@ contract RebalanceTest is BaseTest { // Get global lower and upper ticks (, int24 tickUpper) = hook.getTicksBasedOnState(tickAccumulator3, key.tickSpacing); - // Get current tick - PoolId poolId = key.toId(); - int24 currentTick = hook.getCurrentTick(poolId); - // Slugs must be inline and continuous assertEq(lowerSlug.tickUpper, upperSlug.tickLower, "lowerSlug.tickUpper != upperSlug.tickLower"); @@ -662,13 +663,6 @@ contract RebalanceTest is BaseTest { assertGt(priceDiscoverySlugs[i].liquidity, 0); } - (,, uint24 protocolFee, uint24 lpFee) = manager.getSlot0(key.toId()); - // Lower slug should be unset with ticks at the current price if the fee is 0 - if (protocolFee == 0 && lpFee == 0) { - assertEq(lowerSlug.tickLower, lowerSlug.tickUpper, "lowerSlug.tickLower != lowerSlug.tickUpper"); - assertEq(lowerSlug.liquidity, 0, "lowerSlug.liquidity != 0"); - assertEq(lowerSlug.tickUpper, currentTick, "lowerSlug.tickUpper != currentTick"); - } // Upper and price discovery slugs must be set assertNotEq(upperSlug.liquidity, 0); } From dfbd14d022dd87e8a861a82667bad124c38561e4 Mon Sep 17 00:00:00 2001 From: kinrezc Date: Mon, 21 Oct 2024 20:24:36 -0400 Subject: [PATCH 7/8] rm console logs --- src/Doppler.sol | 2 -- test/integration/Rebalance.t.sol | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index 712220a4..a5c7e07d 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -17,7 +17,6 @@ import {FixedPoint96} from "v4-periphery/lib/v4-core/src/libraries/FixedPoint96. import {TransientStateLibrary} from "v4-periphery/lib/v4-core/src/libraries/TransientStateLibrary.sol"; import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol"; import {ProtocolFeeLibrary} from "v4-periphery/lib/v4-core/src/libraries/ProtocolFeeLibrary.sol"; -import "forge-std/console.sol"; struct SlugData { int24 tickLower; @@ -425,7 +424,6 @@ contract Doppler is BaseHook { numeraireAvailable = uint256(uint128(tokensRemoved.amount0())); assetAvailable = uint256(uint128(tokensRemoved.amount1())) + key.currency1.balanceOfSelf(); } - console.log("numAvail", numeraireAvailable); SlugData memory lowerSlug = _computeLowerSlugData(key, requiredProceeds, numeraireAvailable, totalTokensSold_, tickLower, currentTick); diff --git a/test/integration/Rebalance.t.sol b/test/integration/Rebalance.t.sol index 06f6b2a9..cd17df68 100644 --- a/test/integration/Rebalance.t.sol +++ b/test/integration/Rebalance.t.sol @@ -23,7 +23,6 @@ import {InvalidTime, SwapBelowRange} from "src/Doppler.sol"; import {BaseTest} from "test/shared/BaseTest.sol"; import {Position} from "../../src/Doppler.sol"; -import "forge-std/console.sol"; contract RebalanceTest is BaseTest { using PoolIdLibrary for PoolKey; @@ -109,9 +108,6 @@ contract RebalanceTest is BaseTest { } // Validate that each price discovery slug has liquidity - // console.log("slug liq", priceDiscoverySlugs[i].liquidity); - // console.log("slug upper", priceDiscoverySlugs[i].tickUpper); - // console.log("slug lower", priceDiscoverySlugs[i].tickLower); assertGt(priceDiscoverySlugs[i].liquidity, 0); } From 494f409d98119867485ef196a18520a78daa4d8e Mon Sep 17 00:00:00 2001 From: kinrezc Date: Tue, 22 Oct 2024 13:39:22 -0400 Subject: [PATCH 8/8] use asset remaining in unlock callback --- src/Doppler.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Doppler.sol b/src/Doppler.sol index a5c7e07d..dd299b83 100644 --- a/src/Doppler.sol +++ b/src/Doppler.sol @@ -814,9 +814,9 @@ contract Doppler is BaseHook { (, int24 tickUpper) = _getTicksBasedOnState(int24(0), key.tickSpacing); - (SlugData memory upperSlug, ) = _computeUpperSlugData(key, 0, tick, numTokensToSell); + (SlugData memory upperSlug, uint256 assetRemaining) = _computeUpperSlugData(key, 0, tick, numTokensToSell); SlugData[] memory priceDiscoverySlugs = - _computePriceDiscoverySlugsData(key, upperSlug, tickUpper, numTokensToSell); + _computePriceDiscoverySlugsData(key, upperSlug, tickUpper, assetRemaining); BalanceDelta finalDelta;