diff --git a/src/Doppler.sol b/src/Doppler.sol index 0d980ae6..dd299b83 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(); @@ -343,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); @@ -425,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 @@ -608,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; @@ -633,10 +636,10 @@ contract Doppler is BaseHook { TickMath.getSqrtPriceAtTick(slug.tickUpper), tokensToLp ); - assetAvailable -= tokensToLp; } else { slug.liquidity = 0; } + assetRemaining = assetAvailable - tokensToLp; } function _computePriceDiscoverySlugsData( @@ -811,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; diff --git a/test/integration/Rebalance.t.sol b/test/integration/Rebalance.t.sol index b835c49c..cd17df68 100644 --- a/test/integration/Rebalance.t.sol +++ b/test/integration/Rebalance.t.sol @@ -23,6 +23,7 @@ import {InvalidTime, SwapBelowRange} from "src/Doppler.sol"; import {BaseTest} from "test/shared/BaseTest.sol"; import {Position} from "../../src/Doppler.sol"; + contract RebalanceTest is BaseTest { using PoolIdLibrary for PoolKey; using StateLibrary for IPoolManager; @@ -555,7 +556,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 +565,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 +596,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 +632,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 +659,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); } diff --git a/test/shared/BaseTest.sol b/test/shared/BaseTest.sol index 8a1c574c..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 ? 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))); 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); } 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); } }