From 2a58a2208e6a774d657e2d92042b5eb46ff0b340 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Tue, 19 Nov 2024 16:31:14 -0500 Subject: [PATCH 1/3] fix TWAMM bugs + add more tests --- .../TWAMM executTWAMMOrders 1 interval.snap | 1 + .../TWAMM executTWAMMOrders 2 intervals.snap | 1 + .../TWAMM executTWAMMOrders 3 intervals.snap | 1 + ...ecutTWAMMOrders singleSell 1 interval.snap | 1 + ...cutTWAMMOrders singleSell 2 intervals.snap | 1 + .forge-snapshots/TWAMMSubmitOrder.snap | 2 +- contracts/hooks/examples/TWAMM.sol | 6 +- test/TWAMM.t.sol | 412 ++++++++++++++++-- .../implementation/TWAMMImplementation.sol | 21 + 9 files changed, 403 insertions(+), 43 deletions(-) create mode 100644 .forge-snapshots/TWAMM executTWAMMOrders 1 interval.snap create mode 100644 .forge-snapshots/TWAMM executTWAMMOrders 2 intervals.snap create mode 100644 .forge-snapshots/TWAMM executTWAMMOrders 3 intervals.snap create mode 100644 .forge-snapshots/TWAMM executTWAMMOrders singleSell 1 interval.snap create mode 100644 .forge-snapshots/TWAMM executTWAMMOrders singleSell 2 intervals.snap diff --git a/.forge-snapshots/TWAMM executTWAMMOrders 1 interval.snap b/.forge-snapshots/TWAMM executTWAMMOrders 1 interval.snap new file mode 100644 index 00000000..101f9831 --- /dev/null +++ b/.forge-snapshots/TWAMM executTWAMMOrders 1 interval.snap @@ -0,0 +1 @@ +484776 \ No newline at end of file diff --git a/.forge-snapshots/TWAMM executTWAMMOrders 2 intervals.snap b/.forge-snapshots/TWAMM executTWAMMOrders 2 intervals.snap new file mode 100644 index 00000000..07d527f5 --- /dev/null +++ b/.forge-snapshots/TWAMM executTWAMMOrders 2 intervals.snap @@ -0,0 +1 @@ +590266 \ No newline at end of file diff --git a/.forge-snapshots/TWAMM executTWAMMOrders 3 intervals.snap b/.forge-snapshots/TWAMM executTWAMMOrders 3 intervals.snap new file mode 100644 index 00000000..44ba2ff9 --- /dev/null +++ b/.forge-snapshots/TWAMM executTWAMMOrders 3 intervals.snap @@ -0,0 +1 @@ +687728 \ No newline at end of file diff --git a/.forge-snapshots/TWAMM executTWAMMOrders singleSell 1 interval.snap b/.forge-snapshots/TWAMM executTWAMMOrders singleSell 1 interval.snap new file mode 100644 index 00000000..b645958e --- /dev/null +++ b/.forge-snapshots/TWAMM executTWAMMOrders singleSell 1 interval.snap @@ -0,0 +1 @@ +256690 \ No newline at end of file diff --git a/.forge-snapshots/TWAMM executTWAMMOrders singleSell 2 intervals.snap b/.forge-snapshots/TWAMM executTWAMMOrders singleSell 2 intervals.snap new file mode 100644 index 00000000..b03bae48 --- /dev/null +++ b/.forge-snapshots/TWAMM executTWAMMOrders singleSell 2 intervals.snap @@ -0,0 +1 @@ +288800 \ No newline at end of file diff --git a/.forge-snapshots/TWAMMSubmitOrder.snap b/.forge-snapshots/TWAMMSubmitOrder.snap index 3d61294d..668f357f 100644 --- a/.forge-snapshots/TWAMMSubmitOrder.snap +++ b/.forge-snapshots/TWAMMSubmitOrder.snap @@ -1 +1 @@ -122043 \ No newline at end of file +122174 \ No newline at end of file diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index dc1f3b00..dc2eb9bf 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -145,12 +145,16 @@ contract TWAMM is BaseHook, ITWAMM { PoolId poolId = key.toId(); (uint160 sqrtPriceX96,,,) = manager.getSlot0(poolId); State storage twamm = twammStates[poolId]; + if (twamm.lastVirtualOrderTimestamp == 0) revert NotInitialized(); (bool zeroForOne, uint160 sqrtPriceLimitX96) = _executeTWAMMOrders(twamm, manager, key, PoolParamsOnExecute(sqrtPriceX96, manager.getLiquidity(poolId))); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, type(int256).max, sqrtPriceLimitX96))); + // we trade to the sqrtPriceLimitX96, but v3 math inherently has small imprecision, must set swapAmountLimit + // to balance in case the trade needs more wei than is left in the contract + int256 swapAmountLimit = -int256(zeroForOne ? key.currency0.balanceOfSelf() : key.currency1.balanceOfSelf()); + manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, swapAmountLimit, sqrtPriceLimitX96))); } } diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 0f2f82e0..4b5fd2a2 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -43,8 +43,10 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 earningsFactorLast ); - TWAMM twamm = - TWAMM(address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG))); + + TWAMMImplementation twamm = TWAMMImplementation( + address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG)) + ); address hookAddress; MockERC20 token0; MockERC20 token1; @@ -69,6 +71,8 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { } } + // modifyLiquidityRouter = new PoolLiquidityPositionTest(IPoolManager(address(manager))); + // swapRouter = new PoolSwapTest(IPoolManager(address(manager))); (poolKey, poolId) = initPool(currency0, currency1, twamm, 3000, SQRT_PRICE_1_1, ZERO_BYTES); token0.approve(address(modifyLiquidityRouter), 100 ether); @@ -92,12 +96,11 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { (PoolKey memory initKey, PoolId initId) = newPoolKeyWithTWAMM(twamm); assertEq(twamm.lastVirtualOrderTimestamp(initId), 0); vm.warp(10000); - manager.initialize(initKey, SQRT_PRICE_1_1, ZERO_BYTES); assertEq(twamm.lastVirtualOrderTimestamp(initId), 10000); } - function testTWAMM_submitOrder_StoresOrderWithCorrectPoolAndOrderPoolInfo() public { + function testTWAMM_submitOrder_storesOrderWithCorrectPoolAndOrderPoolInfo() public { uint160 expiration = 30000; uint160 submitTimestamp = 10000; uint160 duration = expiration - submitTimestamp; @@ -126,27 +129,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(earningsFactorCurrent1For0, 0); } - function TWAMMSingleSell0For1SellRateAndEarningsFactorGetsUpdatedProperly() public { - // TODO: fails with a bug for single pool sell, swap amount 3 wei above balance. - - ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); - ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, true); - - token0.approve(address(twamm), 100e18); - token1.approve(address(twamm), 100e18); - vm.warp(10000); - twamm.submitOrder(poolKey, orderKey1, 1e18); - vm.warp(30000); - twamm.submitOrder(poolKey, orderKey2, 1e18); - vm.warp(40000); - - ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey2); - (, uint256 earningsFactorCurrent) = twamm.getOrderPool(poolKey, true); - assertEq(submittedOrder.sellRate, 1 ether / 10000); - assertEq(submittedOrder.earningsFactorLast, earningsFactorCurrent); - } - - function testTWAMM_submitOrder_StoresSellRatesEarningsFactorsProperly() public { + function testTWAMM_submitOrder_storesSellRatesEarningsFactorsProperly() public { uint160 expiration1 = 30000; uint160 expiration2 = 40000; uint256 submitTimestamp1 = 10000; @@ -189,7 +172,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(earningsFactor1For0, 1470157410324350030712806974476955); } - function testTWAMM_submitOrder_EmitsEvent() public { + function testTWAMM_submitOrder_emitsEvent() public { ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); token0.approve(address(twamm), 100e18); @@ -200,7 +183,72 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { twamm.submitOrder(poolKey, orderKey1, 1e18); } - function testTWAMM_updateOrder_EmitsEvent() public { + function testTWAMM_submitOrder_singleSell_zeroForOne_sellRateAndEarningsFactorGetsUpdatedProperly() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, true); + + token0.approve(address(twamm), 100e18); + vm.warp(10000); + twamm.submitOrder(poolKey, orderKey1, 1e18); + vm.warp(30000); + twamm.submitOrder(poolKey, orderKey2, 1e18); + vm.warp(40000); + + ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey2); + (, uint256 earningsFactorCurrent) = twamm.getOrderPool(poolKey, true); + assertEq(submittedOrder.sellRate, 1 ether / 10000); + assertEq(submittedOrder.earningsFactorLast, earningsFactorCurrent); + } + + function testTWAMM_submitOrder_singleSell_OneForZero_sellRateAndEarningsFactorGetsUpdatedProperly() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, false); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, false); + + token1.approve(address(twamm), 100e18); + vm.warp(10000); + twamm.submitOrder(poolKey, orderKey1, 1e18); + vm.warp(30000); + twamm.submitOrder(poolKey, orderKey2, 1e18); + vm.warp(40000); + + ITWAMM.Order memory submittedOrder = twamm.getOrder(poolKey, orderKey2); + (, uint256 earningsFactorCurrent) = twamm.getOrderPool(poolKey, false); + assertEq(submittedOrder.sellRate, 1 ether / 10000); + assertEq(submittedOrder.earningsFactorLast, earningsFactorCurrent); + } + + function testTWAMM_submitOrder_revertsIfExpiryNotOnInterval() public { + uint160 invalidTimestamp = 30001; + ITWAMM.OrderKey memory invalidKey = ITWAMM.OrderKey(address(this), invalidTimestamp, true); + token0.approve(address(twamm), 100e18); + + vm.expectRevert(abi.encodeWithSelector(ITWAMM.ExpirationNotOnInterval.selector, invalidTimestamp)); + twamm.submitOrder(poolKey, invalidKey, 1e18); + } + + function testTWAMM_submitOrder_revertsIfPoolNotInitialized() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + PoolKey memory invalidPoolKey = poolKey; + invalidPoolKey.fee = 1000; + + token0.approve(address(twamm), 100e18); + vm.warp(10000); + + vm.expectRevert(ITWAMM.NotInitialized.selector); + twamm.submitOrder(invalidPoolKey, orderKey1, 1e18); + } + + function testTWAMM_submitOrder_revertsIfExpiryInThePast() public { + uint160 prevTimestamp = 10000; + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), prevTimestamp, true); + token0.approve(address(twamm), 100e18); + vm.warp(20000); + + vm.expectRevert(abi.encodeWithSelector(ITWAMM.ExpirationLessThanBlocktime.selector, prevTimestamp)); + twamm.submitOrder(poolKey, orderKey1, 1e18); + } + + function testTWAMM_updateOrder_emitsEvent() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; uint256 orderAmount; @@ -216,7 +264,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { twamm.updateOrder(poolKey, orderKey1, amountDelta); } - function testTWAMM_updateOrder_ZeroForOne_DecreasesSellrateUpdatesSellTokensOwed() public { + function testTWAMM_updateOrder_zeroForOne_decreasesSellrateUpdatesSellTokensOwed() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; uint256 orderAmount; @@ -234,13 +282,13 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey1.owner); uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); - // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); + // takes 10% off the remaining half amount (so 80% of original sellrate) + assertEq(updatedSellRate, originalSellRate * 80 / 100); assertEq(token0Owed, uint256(-amountDelta)); assertEq(token1Owed, orderAmount / 2); } - function testTWAMM_updateOrder_OneForZero_DecreasesSellrateUpdatesSellTokensOwed() public { + function testTWAMM_updateOrder_oneForZero_decreasesSellrateUpdatesSellTokensOwed() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; uint256 orderAmount; @@ -260,12 +308,12 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey1.owner); // takes 10% off the remaining half (so 80% of original sellrate) - assertEq(updatedSellRate, (originalSellRate * 80) / 100); + assertEq(updatedSellRate, originalSellRate * 80 / 100); assertEq(token0Owed, orderAmount / 2); assertEq(token1Owed, uint256(-amountDelta)); } - function testTWAMM_updatedOrder_ZeroForOne_ClosesOrderIfEliminatingPosition() public { + function testTWAMM_updatedOrder_zeroForOne_closesOrderIfEliminatingPosition() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; uint256 orderAmount; @@ -285,7 +333,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(token1Owed, orderAmount / 2); } - function testTWAMM_updatedOrder_OneForZero_ClosesOrderIfEliminatingPosition() public { + function testTWAMM_updatedOrder_oneForZero_closesOrderIfEliminatingPosition() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; uint256 orderAmount; @@ -305,7 +353,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(token1Owed, orderAmount / 2); } - function testTWAMM_updatedOrder_ZeroForOne_IncreaseOrderAmount() public { + function testTWAMM_updatedOrder_zeroForOne_increaseOrderAmount() public { int256 amountDelta = 1 ether; ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; @@ -330,7 +378,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(token1Owed, orderAmount / 2); } - function testTWAMM_updatedOrder_OneForZero_IncreaseOrderAmount() public { + function testTWAMM_updatedOrder_oneForZero_increaseOrderAmount() public { int256 amountDelta = 1 ether; ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; @@ -355,6 +403,211 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(token1Owed, 0); } + function testTWAMM_updatedOrder_revertsIfDecreasingByAmoungGreaterThanOrder() public { + ITWAMM.OrderKey memory orderKey1; + ITWAMM.OrderKey memory orderKey2; + uint256 orderAmount; + (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); + // decrease entire order after some has already sold + int256 amountDelta = -int256(orderAmount); + + // set timestamp to halfway through the order + vm.warp(20000); + + vm.expectRevert( + abi.encodeWithSelector(ITWAMM.InvalidAmountDelta.selector, orderKey1, orderAmount / 2, amountDelta) + ); + twamm.updateOrder(poolKey, orderKey1, amountDelta); + } + + function testTWAMM_updatedOrder_doesNotRevertIfEliminatingExactAmount() public { + ITWAMM.OrderKey memory orderKey1; + ITWAMM.OrderKey memory orderKey2; + uint256 orderAmount; + (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); + + // decrease order amount by 10% + int256 restOfOrder = -int256(orderAmount) / 2; + + // set timestamp to halfway through the order + vm.warp(20000); + + twamm.updateOrder(poolKey, orderKey2, restOfOrder); + (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, false); + ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey2); + + uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); + uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); + + // sellRate is 0, tokens owed equal all of order + assertEq(updatedSellRate, 0); + assertEq(token0Owed, orderAmount / 2); + assertEq(token1Owed, orderAmount / 2); + assertEq(deletedOrder.sellRate, 0); + assertEq(deletedOrder.earningsFactorLast, 0); + } + + function testTWAMM_updateOrder_updatesTokensOwedIfCalledAfterExpirationWithNoDelta() public { + ITWAMM.OrderKey memory orderKey1; + ITWAMM.OrderKey memory orderKey2; + uint256 orderAmount; + (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); + + // set timestamp to halfway through the order + vm.warp(orderKey2.expiration + 10); + + twamm.updateOrder(poolKey, orderKey2, 0); + (uint256 updatedSellRate,) = twamm.getOrderPool(poolKey, false); + ITWAMM.Order memory deletedOrder = twamm.getOrder(poolKey, orderKey2); + + uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); + uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); + + // sellRate is 0, tokens owed equal all of order + assertEq(updatedSellRate, 0); + assertEq(token0Owed, orderAmount); + assertEq(token1Owed, 0); + assertEq(deletedOrder.sellRate, 0); + assertEq(deletedOrder.earningsFactorLast, 0); + } + + function testTWAMM_updateOrder_revertsIfIncreasingAmountAfterExpiration() public { + ITWAMM.OrderKey memory orderKey1; + ITWAMM.OrderKey memory orderKey2; + uint256 orderAmount; + (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); + + // set timestamp to after expiration + vm.warp(orderKey2.expiration + 10); + + vm.expectRevert(abi.encodeWithSelector(ITWAMM.CannotModifyCompletedOrder.selector, orderKey2)); + twamm.updateOrder(poolKey, orderKey2, 1000); + } + + function testTWAMM_executeTWAMMOrders_updatesAllTheNecessaryEarningsFactorIntervals() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, false); + ITWAMM.OrderKey memory orderKey3 = ITWAMM.OrderKey(address(this), 50000, true); + ITWAMM.OrderKey memory orderKey4 = ITWAMM.OrderKey(address(this), 50000, false); + + token0.approve(address(twamm), 100 ether); + token1.approve(address(twamm), 100 ether); + + vm.warp(10000); + + twamm.submitOrder(poolKey, orderKey1, 1 ether); + twamm.submitOrder(poolKey, orderKey2, 5 ether); + twamm.submitOrder(poolKey, orderKey3, 2 ether); + twamm.submitOrder(poolKey, orderKey4, 2 ether); + + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 20000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 20000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 30000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 30000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 40000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 40000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 50000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 50000), 0); + + vm.warp(50000); // go to exact interval to also test when block is exactly on an interval + snapStart("TWAMM executTWAMMOrders 3 intervals"); + twamm.executeTWAMMOrders(poolKey); + snapEnd(); + + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 20000), 0); + assertEq(twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 20000), 0); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 30000), 1903834450064690094904650934081653 + ); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 30000), 1332467160273236668937468324643833 + ); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 40000), 3151779959438527761611322345863307 + ); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 40000), 1837497928424750201261148602165737 + ); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), true, 50000), 4499127981259426598474740139623306 + ); + assertEq( + twamm.getOrderPoolEarningsFactorAtInterval(poolKey.toId(), false, 50000), 2303495623595701879842493741448458 + ); + } + + function testTWAMM_executeTWAMMOrders_OneIntervalGas() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 30000, false); + + token0.approve(address(twamm), 100 ether); + token1.approve(address(twamm), 100 ether); + + vm.warp(10000); + + twamm.submitOrder(poolKey, orderKey1, 1 ether); + twamm.submitOrder(poolKey, orderKey2, 5 ether); + + vm.warp(60000); + snapStart("TWAMM executTWAMMOrders 1 interval"); + twamm.executeTWAMMOrders(poolKey); + snapEnd(); + } + + function testTWAMM_executeTWAMMOrders_TwoIntervalsGas() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 30000, false); + ITWAMM.OrderKey memory orderKey3 = ITWAMM.OrderKey(address(this), 40000, true); + ITWAMM.OrderKey memory orderKey4 = ITWAMM.OrderKey(address(this), 40000, false); + + token0.approve(address(twamm), 100 ether); + token1.approve(address(twamm), 100 ether); + + vm.warp(10000); + + twamm.submitOrder(poolKey, orderKey1, 1 ether); + twamm.submitOrder(poolKey, orderKey2, 5 ether); + twamm.submitOrder(poolKey, orderKey3, 2 ether); + twamm.submitOrder(poolKey, orderKey4, 2 ether); + + vm.warp(60000); + snapStart("TWAMM executTWAMMOrders 2 intervals"); + twamm.executeTWAMMOrders(poolKey); + snapEnd(); + } + + function testTWAMM_executeTWAMMOrders_singlePoolSell_OneIntervalGas() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + + token0.approve(address(twamm), 100 ether); + + vm.warp(10000); + + twamm.submitOrder(poolKey, orderKey1, 1 ether); + + vm.warp(60000); + snapStart("TWAMM executTWAMMOrders singleSell 1 interval"); + twamm.executeTWAMMOrders(poolKey); + snapEnd(); + } + + function testTWAMM_executeTWAMMOrders_SinglePoolSell_twoIntervalsGas() public { + ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); + ITWAMM.OrderKey memory orderKey2 = ITWAMM.OrderKey(address(this), 40000, true); + + token0.approve(address(twamm), 100 ether); + + vm.warp(10000); + + twamm.submitOrder(poolKey, orderKey1, 1 ether); + twamm.submitOrder(poolKey, orderKey2, 5 ether); + + vm.warp(60000); + snapStart("TWAMM executTWAMMOrders singleSell 2 intervals"); + twamm.executeTWAMMOrders(poolKey); + snapEnd(); + } + function testTWAMMEndToEndSimSymmetricalOrderPools() public { uint256 orderAmount = 1e18; ITWAMM.OrderKey memory orderKey1 = ITWAMM.OrderKey(address(this), 30000, true); @@ -362,13 +615,14 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { token0.approve(address(twamm), 100e18); token1.approve(address(twamm), 100e18); - modifyLiquidityRouter.modifyLiquidity( - poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES - ); + modifyLiquidityRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES); + // submit symmetrical orders trading against each other (easy numbers) vm.warp(10000); twamm.submitOrder(poolKey, orderKey1, orderAmount); twamm.submitOrder(poolKey, orderKey2, orderAmount); + + // execute half the orders and update individual order info vm.warp(20000); twamm.executeTWAMMOrders(poolKey); twamm.updateOrder(poolKey, orderKey1, 0); @@ -377,6 +631,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 earningsToken0 = twamm.tokensOwed(poolKey.currency0, address(this)); uint256 earningsToken1 = twamm.tokensOwed(poolKey.currency1, address(this)); + // each owner should be owed half the tokens from the opposing order assertEq(earningsToken0, orderAmount / 2); assertEq(earningsToken1, orderAmount / 2); @@ -385,6 +640,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 balance0BeforeThis = poolKey.currency0.balanceOfSelf(); uint256 balance1BeforeThis = poolKey.currency1.balanceOfSelf(); + // conplete order and collect tokens vm.warp(30000); twamm.executeTWAMMOrders(poolKey); twamm.updateOrder(poolKey, orderKey1, 0); @@ -400,6 +656,7 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 balance0AfterThis = poolKey.currency0.balanceOfSelf(); uint256 balance1AfterThis = poolKey.currency1.balanceOfSelf(); + // TWAMM should not have any remaining balance assertEq(balance1AfterTWAMM, 0); assertEq(balance0AfterTWAMM, 0); assertEq(balance0BeforeTWAMM - balance0AfterTWAMM, orderAmount); @@ -408,9 +665,82 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { assertEq(balance1AfterThis - balance1BeforeThis, orderAmount); } + function testTWAMM_isCrossingIinitializedTick_returnsFalseWhenSwappingToSamePrice() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, SQRT_PRICE_1_1); + + assertEq(crossingInitializedTick, false); + assertEq(nextTickInit, 60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsTrueWhenCrossingToTheRight() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, SQRT_PRICE_2_1); + + assertEq(crossingInitializedTick, true); + assertEq(nextTickInit, 60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsTrueWhenCrossingToTheLeft() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, SQRT_PRICE_1_2); + + assertEq(crossingInitializedTick, true); + assertEq(nextTickInit, -60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsFalseWhenSwappingRightBeforeTick() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, TickMath.getSqrtPriceAtTick(59)); + + assertEq(crossingInitializedTick, false); + assertEq(nextTickInit, 60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsFalseWhenSwappingRightToInitializeableTick() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, TickMath.getSqrtPriceAtTick(50)); + + assertEq(crossingInitializedTick, false); + assertEq(nextTickInit, 60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsFalseWhenSwappingLeftBeforeTick() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, TickMath.getSqrtPriceAtTick(-59)); + + assertEq(crossingInitializedTick, false); + assertEq(nextTickInit, -60); + } + + function testTWAMM_isCrossingIinitializedTick_returnsFalseWhenSwappingLeftToInitializeableTick() public view { + TWAMM.PoolParamsOnExecute memory poolParams = TWAMM.PoolParamsOnExecute(SQRT_PRICE_1_1, 1000000 ether); + + (bool crossingInitializedTick, int24 nextTickInit) = + twamm.isCrossingInitializedTick(poolParams, manager, poolKey, TickMath.getSqrtPriceAtTick(-50)); + + assertEq(crossingInitializedTick, false); + assertEq(nextTickInit, -60); + } + function newPoolKeyWithTWAMM(IHooks hooks) public returns (PoolKey memory, PoolId) { - (Currency _token0, Currency _token1) = deployMintAndApprove2Currencies(); - PoolKey memory key = PoolKey(_token0, _token1, 0, 60, hooks); + MockERC20[] memory tokens = deployTokens(2, 2 ** 255); + (MockERC20 t0, MockERC20 t1) = address(tokens[0]) < address(tokens[0]) ? (tokens[0], tokens[1]) : (tokens[1], tokens[0]); + + PoolKey memory key = + PoolKey(Currency.wrap(address(t0)), Currency.wrap(address(t1)), 0, 60, hooks); return (key, key.toId()); } diff --git a/test/shared/implementation/TWAMMImplementation.sol b/test/shared/implementation/TWAMMImplementation.sol index f217db8c..2868aec7 100644 --- a/test/shared/implementation/TWAMMImplementation.sol +++ b/test/shared/implementation/TWAMMImplementation.sol @@ -5,6 +5,8 @@ import {BaseHook} from "../../../contracts/BaseHook.sol"; import {TWAMM} from "../../../contracts/hooks/examples/TWAMM.sol"; import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol"; import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol"; +import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol"; +import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol"; contract TWAMMImplementation is TWAMM { constructor(IPoolManager poolManager, uint256 interval, TWAMM addressToEtch) TWAMM(poolManager, interval) { @@ -13,4 +15,23 @@ contract TWAMMImplementation is TWAMM { // make this a no-op in testing function validateHookAddress(BaseHook _this) internal pure override {} + + + function getOrderPoolEarningsFactorAtInterval(PoolId id, bool zeroForOne, uint256 timestamp) + external + view + returns (uint256 earningsFactor) + { + if (zeroForOne) return twammStates[id].orderPool0For1.earningsFactorAtInterval[timestamp]; + else return twammStates[id].orderPool1For0.earningsFactorAtInterval[timestamp]; + } + + function isCrossingInitializedTick( + PoolParamsOnExecute memory pool, + IPoolManager poolManager, + PoolKey memory poolKey, + uint160 nextSqrtPriceX96 + ) external view returns (bool crossingInitializedTick, int24 nextTickInit) { + return _isCrossingInitializedTick(pool, poolManager, poolKey, nextSqrtPriceX96); + } } From 73cd6c367db2f2f6b55a193743ce9d954afef81e Mon Sep 17 00:00:00 2001 From: Paco Date: Fri, 30 Jun 2023 23:46:01 +0800 Subject: [PATCH 2/3] use earningsFactorAtInterval to calculate the tokensOwed after expiration --- contracts/hooks/examples/TWAMM.sol | 11 ++++++---- test/TWAMM.t.sol | 34 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index dc2eb9bf..a60d75f5 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -260,13 +260,16 @@ contract TWAMM is BaseHook, ITWAMM { if (amountDelta != 0 && orderKey.expiration <= block.timestamp) revert CannotModifyCompletedOrder(orderKey); unchecked { - uint256 earningsFactor = orderPool.earningsFactorCurrent - order.earningsFactorLast; - buyTokensOwed = (earningsFactor * order.sellRate) >> FixedPoint96.RESOLUTION; - earningsFactorLast = orderPool.earningsFactorCurrent; - order.earningsFactorLast = earningsFactorLast; + earningsFactorLast = orderKey.expiration <= block.timestamp + ? orderPool.earningsFactorAtInterval[orderKey.expiration] + : orderPool.earningsFactorCurrent; + buyTokensOwed = + ((earningsFactorLast - order.earningsFactorLast) * order.sellRate) >> FixedPoint96.RESOLUTION; if (orderKey.expiration <= block.timestamp) { delete self.orders[_orderId(orderKey)]; + } else { + order.earningsFactorLast = earningsFactorLast; } if (amountDelta != 0) { diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 4b5fd2a2..95a629b9 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -264,6 +264,40 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { twamm.updateOrder(poolKey, orderKey1, amountDelta); } + function testTWAMM_updatedOrder_calculateTokensOwedAfterExpiration() public { + ITWAMM.OrderKey memory orderKey1; + ITWAMM.OrderKey memory orderKey2; + uint256 orderAmount; + (orderKey1, orderKey2, orderAmount) = submitOrdersBothDirections(); + + // submit two more orders that will expire after order1 & order2 so the earningsFactorCurrent will continue growing after they expire + uint256 extraOrderAmount = 2 ether; + ITWAMM.OrderKey memory orderKey3 = ITWAMM.OrderKey(address(this), 60000, true); + ITWAMM.OrderKey memory orderKey4 = ITWAMM.OrderKey(address(this), 60000, false); + token0.approve(address(twamm), extraOrderAmount); + token1.approve(address(twamm), extraOrderAmount); + twamm.submitOrder(poolKey, orderKey3, extraOrderAmount); + twamm.submitOrder(poolKey, orderKey4, extraOrderAmount); + + // set timestamp to after order1 & order2 expire + vm.warp(40000); + + // update order1 & order2 after expiration, should use the earningsFactorAtInterval at expiration to settle + vm.expectEmit(true, true, true, true); + emit UpdateOrder(poolId, address(this), 30000, true, 0, 20000 << 96); + twamm.updateOrder(poolKey, orderKey1, 0); + + vm.expectEmit(true, true, true, true); + emit UpdateOrder(poolId, address(this), 30000, false, 0, 20000 << 96); + twamm.updateOrder(poolKey, orderKey2, 0); + + uint256 token0Owed = twamm.tokensOwed(poolKey.currency0, orderKey2.owner); + uint256 token1Owed = twamm.tokensOwed(poolKey.currency1, orderKey2.owner); + + assertEq(token0Owed, orderAmount); + assertEq(token1Owed, orderAmount); + } + function testTWAMM_updateOrder_zeroForOne_decreasesSellrateUpdatesSellTokensOwed() public { ITWAMM.OrderKey memory orderKey1; ITWAMM.OrderKey memory orderKey2; From 8c6902af9a653287b128047891a29b2e2e2c2278 Mon Sep 17 00:00:00 2001 From: Emily Williams Date: Wed, 27 Nov 2024 11:58:02 -0500 Subject: [PATCH 3/3] fmt --- contracts/hooks/examples/TWAMM.sol | 8 ++++---- test/TWAMM.t.sol | 11 ++++++----- test/shared/implementation/TWAMMImplementation.sol | 1 - 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/contracts/hooks/examples/TWAMM.sol b/contracts/hooks/examples/TWAMM.sol index a60d75f5..fc1521d9 100644 --- a/contracts/hooks/examples/TWAMM.sol +++ b/contracts/hooks/examples/TWAMM.sol @@ -151,10 +151,10 @@ contract TWAMM is BaseHook, ITWAMM { _executeTWAMMOrders(twamm, manager, key, PoolParamsOnExecute(sqrtPriceX96, manager.getLiquidity(poolId))); if (sqrtPriceLimitX96 != 0 && sqrtPriceLimitX96 != sqrtPriceX96) { - // we trade to the sqrtPriceLimitX96, but v3 math inherently has small imprecision, must set swapAmountLimit - // to balance in case the trade needs more wei than is left in the contract - int256 swapAmountLimit = -int256(zeroForOne ? key.currency0.balanceOfSelf() : key.currency1.balanceOfSelf()); - manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, swapAmountLimit, sqrtPriceLimitX96))); + // we trade to the sqrtPriceLimitX96, but v3 math inherently has small imprecision, must set swapAmountLimit + // to balance in case the trade needs more wei than is left in the contract + int256 swapAmountLimit = -int256(zeroForOne ? key.currency0.balanceOfSelf() : key.currency1.balanceOfSelf()); + manager.unlock(abi.encode(key, IPoolManager.SwapParams(zeroForOne, swapAmountLimit, sqrtPriceLimitX96))); } } diff --git a/test/TWAMM.t.sol b/test/TWAMM.t.sol index 95a629b9..06548142 100644 --- a/test/TWAMM.t.sol +++ b/test/TWAMM.t.sol @@ -43,7 +43,6 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { uint256 earningsFactorLast ); - TWAMMImplementation twamm = TWAMMImplementation( address(uint160(Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG)) ); @@ -649,7 +648,9 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { token0.approve(address(twamm), 100e18); token1.approve(address(twamm), 100e18); - modifyLiquidityRouter.modifyLiquidity(poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES); + modifyLiquidityRouter.modifyLiquidity( + poolKey, IPoolManager.ModifyLiquidityParams(-2400, 2400, 10 ether, 0), ZERO_BYTES + ); // submit symmetrical orders trading against each other (easy numbers) vm.warp(10000); @@ -771,10 +772,10 @@ contract TWAMMTest is Test, Deployers, GasSnapshot { function newPoolKeyWithTWAMM(IHooks hooks) public returns (PoolKey memory, PoolId) { MockERC20[] memory tokens = deployTokens(2, 2 ** 255); - (MockERC20 t0, MockERC20 t1) = address(tokens[0]) < address(tokens[0]) ? (tokens[0], tokens[1]) : (tokens[1], tokens[0]); + (MockERC20 t0, MockERC20 t1) = + address(tokens[0]) < address(tokens[0]) ? (tokens[0], tokens[1]) : (tokens[1], tokens[0]); - PoolKey memory key = - PoolKey(Currency.wrap(address(t0)), Currency.wrap(address(t1)), 0, 60, hooks); + PoolKey memory key = PoolKey(Currency.wrap(address(t0)), Currency.wrap(address(t1)), 0, 60, hooks); return (key, key.toId()); } diff --git a/test/shared/implementation/TWAMMImplementation.sol b/test/shared/implementation/TWAMMImplementation.sol index 2868aec7..5d3034b2 100644 --- a/test/shared/implementation/TWAMMImplementation.sol +++ b/test/shared/implementation/TWAMMImplementation.sol @@ -16,7 +16,6 @@ contract TWAMMImplementation is TWAMM { // make this a no-op in testing function validateHookAddress(BaseHook _this) internal pure override {} - function getOrderPoolEarningsFactorAtInterval(PoolId id, bool zeroForOne, uint256 timestamp) external view