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

Avoid first epoch rebalance #169

Merged
merged 7 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
83 changes: 27 additions & 56 deletions src/Doppler.sol
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ contract Doppler is BaseHook {
bool _isToken0,
uint256 _numPDSlugs
) BaseHook(_poolManager) {
// Check that the current time is before the starting time
if (block.timestamp > _startingTime) revert InvalidTime();
/* Tick checks */
// Starting tick must be greater than ending tick if isToken0
// Ending tick must be greater than starting tick if isToken1
Expand Down Expand Up @@ -174,7 +176,7 @@ contract Doppler is BaseHook {
}

// Only check proceeds if we're after maturity and we haven't already triggered insufficient proceeds
if (block.timestamp > endingTime && !insufficientProceeds) {
if (block.timestamp >= endingTime && !insufficientProceeds) {
// If we haven't raised the minimum proceeds, we allow for all asset tokens to be sold back into
// the curve at the average clearing price
if (state.totalProceeds < minimumProceeds) {
Expand Down Expand Up @@ -877,84 +879,53 @@ contract Doppler is BaseHook {
function _unlockCallback(bytes calldata data) internal override returns (bytes memory) {
CallbackData memory callbackData = abi.decode(data, (CallbackData));
(PoolKey memory key,, int24 tick) = (callbackData.key, callbackData.sender, callbackData.tick);
int256 accumulatorDelta = _getMaxTickDeltaPerEpoch() * 1;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* 1 here is redundant

state.tickAccumulator = state.tickAccumulator + accumulatorDelta;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo it doesn't really make sense dutch auction before any epochs have actually passed. Instead, I think the tickAccumulator should stay as 0 at this point

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with you, but if you don't DA then it seems that the state at epoch 2 will be the same as the state in epoch 1. Since we set lastEpoch to 1 in order to avoid triggering the rebalance in the first epoch, we will then trigger a rebalance in epoch 2. In this case we multiply epochsPassed (in this case this value will be 1) by the max DA. So by leaving the state.tickAccumulator to 0, the DA in the first epoch will lead to the tickAccumulator going from 0 -> max DA, which is the same implied tickAccumulator as is in the unlockCallback.

state.lastEpoch = 1;

(, int24 tickUpper) = _getTicksBasedOnState(int24(0), key.tickSpacing);
int24 currentTick = _alignComputedTickWithTickSpacing(tick + int24(accumulatorDelta / 1e18), key.tickSpacing);
(, int24 tickUpper) = _getTicksBasedOnState(state.tickAccumulator, key.tickSpacing);
uint160 sqrtPriceNext = TickMath.getSqrtPriceAtTick(currentTick);
uint160 sqrtPriceCurrent = TickMath.getSqrtPriceAtTick(tick);

// Compute slugs to place
(SlugData memory upperSlug, uint256 assetRemaining) = _computeUpperSlugData(key, 0, tick, numTokensToSell);
// set the tickLower and tickUpper to the current tick as this is the default behavior when requiredProceeds and totalProceeds are 0
SlugData memory lowerSlug = SlugData({tickLower: currentTick, tickUpper: currentTick, liquidity: 0});
(SlugData memory upperSlug, uint256 assetRemaining) =
_computeUpperSlugData(key, 0, currentTick, numTokensToSell);
SlugData[] memory priceDiscoverySlugs =
_computePriceDiscoverySlugsData(key, upperSlug, tickUpper, assetRemaining);

BalanceDelta finalDelta;

// Place upper slug
if (upperSlug.liquidity != 0) {
(BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
key,
IPoolManager.ModifyLiquidityParams({
tickLower: isToken0 ? upperSlug.tickLower : upperSlug.tickUpper,
tickUpper: isToken0 ? upperSlug.tickUpper : upperSlug.tickLower,
liquidityDelta: int128(upperSlug.liquidity),
salt: UPPER_SLUG_SALT
}),
""
);
finalDelta = add(finalDelta, callerDelta);
}

// Place price discovery slug(s)
for (uint256 i; i < priceDiscoverySlugs.length; ++i) {
if (priceDiscoverySlugs[i].liquidity != 0) {
(BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
key,
IPoolManager.ModifyLiquidityParams({
tickLower: isToken0 ? priceDiscoverySlugs[i].tickLower : priceDiscoverySlugs[i].tickUpper,
tickUpper: isToken0 ? priceDiscoverySlugs[i].tickUpper : priceDiscoverySlugs[i].tickLower,
liquidityDelta: int128(priceDiscoverySlugs[i].liquidity),
salt: bytes32(uint256(3 + i))
}),
""
);
finalDelta = add(finalDelta, callerDelta);
}
}

// Provide tokens to the pool
if (isToken0) {
poolManager.sync(key.currency0);
key.currency0.transfer(address(poolManager), uint256(int256(-finalDelta.amount0())));
} else {
poolManager.sync(key.currency1);
key.currency1.transfer(address(poolManager), uint256(int256(-finalDelta.amount1())));
}

// Update position storage
Position[] memory newPositions = new Position[](2 + numPDSlugs);
newPositions[0] =
Position({tickLower: tick, tickUpper: tick, liquidity: 0, salt: uint8(uint256(LOWER_SLUG_SALT))});

newPositions[0] = Position({
tickLower: lowerSlug.tickLower,
tickUpper: lowerSlug.tickUpper,
liquidity: lowerSlug.liquidity,
salt: uint8(uint256(LOWER_SLUG_SALT))
});
newPositions[1] = Position({
tickLower: upperSlug.tickLower,
tickUpper: upperSlug.tickUpper,
liquidity: upperSlug.liquidity,
salt: uint8(uint256(UPPER_SLUG_SALT))
});

positions[LOWER_SLUG_SALT] = newPositions[0];
positions[UPPER_SLUG_SALT] = newPositions[1];

for (uint256 i; i < priceDiscoverySlugs.length; ++i) {
newPositions[2 + i] = Position({
tickLower: priceDiscoverySlugs[i].tickLower,
tickUpper: priceDiscoverySlugs[i].tickUpper,
liquidity: priceDiscoverySlugs[i].liquidity,
salt: uint8(3 + i)
});
}

_update(newPositions, sqrtPriceCurrent, sqrtPriceNext, key);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this reuse 🔥


positions[LOWER_SLUG_SALT] = newPositions[0];
positions[UPPER_SLUG_SALT] = newPositions[1];
for (uint256 i; i < numPDSlugs; ++i) {
positions[bytes32(uint256(3 + i))] = newPositions[2 + i];
}

poolManager.settle();

return new bytes(0);
}

Expand Down
1 change: 0 additions & 1 deletion test/integration/Rebalance.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,6 @@ contract RebalanceTest is BaseTest {
upperSlug.liquidity
) * 9 / 10;


uint256 amount0ToSwap = LiquidityAmounts.getAmount0ForLiquidity(
TickMath.getSqrtPriceAtTick(upperSlug.tickLower),
TickMath.getSqrtPriceAtTick(upperSlug.tickUpper),
Expand Down
36 changes: 18 additions & 18 deletions test/invariant/DopplerInvariants.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,27 @@ import {BaseTest} from "test/shared/BaseTest.sol";
import {DopplerHandler} from "test/invariant/DopplerHandler.sol";

contract DopplerInvariantsTest is BaseTest {
// DopplerHandler public handler;
// DopplerHandler public handler;

// function setUp() public override {
// super.setUp();
// handler = new DopplerHandler(key, hook, router, isToken0, usingEth);
// function setUp() public override {
// super.setUp();
// handler = new DopplerHandler(key, hook, router, isToken0, usingEth);

// bytes4[] memory selectors = new bytes4[](1);
// selectors[0] = handler.buyExactAmountIn.selector;
// bytes4[] memory selectors = new bytes4[](1);
// selectors[0] = handler.buyExactAmountIn.selector;

// targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
// targetContract(address(handler));
// }
// targetSelector(FuzzSelector({addr: address(handler), selectors: selectors}));
// targetContract(address(handler));
// }

// function afterInvariant() public view {
// console.log("Handler address", address(handler));
// console.log("Calls: ", handler.totalCalls());
// console.log("buyExactAmountIn: ", handler.calls(handler.buyExactAmountIn.selector));
// }
// function afterInvariant() public view {
// console.log("Handler address", address(handler));
// console.log("Calls: ", handler.totalCalls());
// console.log("buyExactAmountIn: ", handler.calls(handler.buyExactAmountIn.selector));
// }

// /// forge-config: default.invariant.fail-on-revert = true
// function invariant_works() public {
// assertTrue(true);
// }
// /// forge-config: default.invariant.fail-on-revert = true
// function invariant_works() public {
// assertTrue(true);
// }
}
11 changes: 4 additions & 7 deletions test/shared/BaseTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -366,17 +366,15 @@ contract BaseTest is Test, Deployers {
}
uint256 approveAmount = uint256(-amount);
TestERC20(asset).approve(address(swapRouter), approveAmount);
vm.expectRevert(abi.encodeWithSelector(
Hooks.Wrap__FailedHookCall.selector, hook, abi.encodeWithSelector(selector)
)
vm.expectRevert(
abi.encodeWithSelector(Hooks.Wrap__FailedHookCall.selector, hook, abi.encodeWithSelector(selector))
);
swapRouter.swap(
key,
IPoolManager.SwapParams(isToken0, amount, isToken0 ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT),
PoolSwapTest.TestSettings(true, false),
""
);

}

function buyExpectRevert(int256 amount, bytes4 selector) public {
Expand All @@ -393,9 +391,8 @@ contract BaseTest is Test, Deployers {
TestERC20(numeraire).approve(address(swapRouter), uint256(mintAmount));
}

vm.expectRevert(abi.encodeWithSelector(
Hooks.Wrap__FailedHookCall.selector, hook, abi.encodeWithSelector(selector)
)
vm.expectRevert(
abi.encodeWithSelector(Hooks.Wrap__FailedHookCall.selector, hook, abi.encodeWithSelector(selector))
);
swapRouter.swap{value: usingEth ? mintAmount : 0}(
key,
Expand Down
7 changes: 4 additions & 3 deletions test/unit/AfterInitialize.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,10 @@ contract AfterInitializeTest is BaseTest {
// Assert that upper and price discovery slugs have liquidity
assertNotEq(upperSlug.liquidity, 0);

// Assert that lower slug has both ticks as the startingTick
assertEq(lowerSlug.tickLower, hook.getStartingTick());
assertEq(lowerSlug.tickUpper, hook.getStartingTick());
// Assert that lower slug has both ticks as the startingTick offset by one max DA
int24 maxTickDelta = int24(hook.getMaxTickDeltaPerEpoch() / 1e18);
assertEq(lowerSlug.tickLower, hook.getStartingTick() + maxTickDelta);
assertEq(lowerSlug.tickUpper, hook.getStartingTick() + maxTickDelta);

// Assert that lower slug has no liquidity
assertEq(lowerSlug.liquidity, 0);
Expand Down
14 changes: 13 additions & 1 deletion test/unit/Swap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ contract SwapTest is BaseTest {
// NOTE: when testing conditions where we expect a revert using buy/sellExpectRevert,
// we need to pass in a negative amount to specify an exactIn swap.
// otherwise, the quoter will attempt to calculate an exactOut amount, which will fail.

function test_swap_RevertsBeforeStartTime() public {
vm.warp(hook.getStartingTime() - 1); // 1 second before the start time

buyExpectRevert(-1 ether, InvalidTime.selector);
}

Expand Down Expand Up @@ -178,4 +179,15 @@ contract SwapTest is BaseTest {

sellExpectRevert(-0.6 ether, SwapBelowRange.selector);
}

function test_swap_DoesNotRebalanceInTheFirstEpoch() public {
(, int256 tickAccumulator,,,,) = hook.state();

vm.warp(hook.getStartingTime());

buy(1 ether);
(, int256 tickAccumulator2,,,,) = hook.state();

assertEq(tickAccumulator, tickAccumulator2);
}
}
Loading