diff --git a/.forge-snapshots/swap CA custom curve + swap noop.snap b/.forge-snapshots/swap CA custom curve + swap noop.snap index a79fb9dc2..d79aad722 100644 --- a/.forge-snapshots/swap CA custom curve + swap noop.snap +++ b/.forge-snapshots/swap CA custom curve + swap noop.snap @@ -1 +1 @@ -127257 \ No newline at end of file +127305 \ No newline at end of file diff --git a/.forge-snapshots/swap CA fee on unspecified.snap b/.forge-snapshots/swap CA fee on unspecified.snap index 694035f26..2b42e7c94 100644 --- a/.forge-snapshots/swap CA fee on unspecified.snap +++ b/.forge-snapshots/swap CA fee on unspecified.snap @@ -1 +1 @@ -155691 \ No newline at end of file +155770 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity with native token.snap b/.forge-snapshots/swap against liquidity with native token.snap index a090cfd2b..49357556f 100644 --- a/.forge-snapshots/swap against liquidity with native token.snap +++ b/.forge-snapshots/swap against liquidity with native token.snap @@ -1 +1 @@ -106660 \ No newline at end of file +106764 \ No newline at end of file diff --git a/.forge-snapshots/swap against liquidity.snap b/.forge-snapshots/swap against liquidity.snap index 8795aa2dd..58bc1d747 100644 --- a/.forge-snapshots/swap against liquidity.snap +++ b/.forge-snapshots/swap against liquidity.snap @@ -1 +1 @@ -117746 \ No newline at end of file +117850 \ No newline at end of file diff --git a/.forge-snapshots/swap burn 6909 for input.snap b/.forge-snapshots/swap burn 6909 for input.snap index 7b637dae0..bd7d2b166 100644 --- a/.forge-snapshots/swap burn 6909 for input.snap +++ b/.forge-snapshots/swap burn 6909 for input.snap @@ -1 +1 @@ -129595 \ No newline at end of file +129702 \ No newline at end of file diff --git a/.forge-snapshots/swap burn native 6909 for input.snap b/.forge-snapshots/swap burn native 6909 for input.snap index 9682aa9ce..ec37ebe6f 100644 --- a/.forge-snapshots/swap burn native 6909 for input.snap +++ b/.forge-snapshots/swap burn native 6909 for input.snap @@ -1 +1 @@ -118816 \ No newline at end of file +118923 \ No newline at end of file diff --git a/.forge-snapshots/swap mint native output as 6909.snap b/.forge-snapshots/swap mint native output as 6909.snap index c1657a67d..9e631dd55 100644 --- a/.forge-snapshots/swap mint native output as 6909.snap +++ b/.forge-snapshots/swap mint native output as 6909.snap @@ -1 +1 @@ -140629 \ No newline at end of file +140733 \ No newline at end of file diff --git a/.forge-snapshots/swap mint output as 6909.snap b/.forge-snapshots/swap mint output as 6909.snap index 84a80bb08..47e544eb9 100644 --- a/.forge-snapshots/swap mint output as 6909.snap +++ b/.forge-snapshots/swap mint output as 6909.snap @@ -1 +1 @@ -156433 \ No newline at end of file +156537 \ No newline at end of file diff --git a/.forge-snapshots/swap skips hook call if hook is caller.snap b/.forge-snapshots/swap skips hook call if hook is caller.snap index 3394318c1..445e460fb 100644 --- a/.forge-snapshots/swap skips hook call if hook is caller.snap +++ b/.forge-snapshots/swap skips hook call if hook is caller.snap @@ -1 +1 @@ -208496 \ No newline at end of file +208600 \ No newline at end of file diff --git a/.forge-snapshots/swap with dynamic fee.snap b/.forge-snapshots/swap with dynamic fee.snap index aa1cc9e71..7b7b7e20b 100644 --- a/.forge-snapshots/swap with dynamic fee.snap +++ b/.forge-snapshots/swap with dynamic fee.snap @@ -1 +1 @@ -140527 \ No newline at end of file +140631 \ No newline at end of file diff --git a/.forge-snapshots/swap with hooks.snap b/.forge-snapshots/swap with hooks.snap index ae921c39e..09ff1ae12 100644 --- a/.forge-snapshots/swap with hooks.snap +++ b/.forge-snapshots/swap with hooks.snap @@ -1 +1 @@ -133197 \ No newline at end of file +133245 \ No newline at end of file diff --git a/.forge-snapshots/swap with lp fee and protocol fee.snap b/.forge-snapshots/swap with lp fee and protocol fee.snap index f55fd26bc..68671fc00 100644 --- a/.forge-snapshots/swap with lp fee and protocol fee.snap +++ b/.forge-snapshots/swap with lp fee and protocol fee.snap @@ -1 +1 @@ -170330 \ No newline at end of file +170437 \ No newline at end of file diff --git a/.forge-snapshots/swap with return dynamic fee.snap b/.forge-snapshots/swap with return dynamic fee.snap index 8ede4c390..9bfcc051a 100644 --- a/.forge-snapshots/swap with return dynamic fee.snap +++ b/.forge-snapshots/swap with return dynamic fee.snap @@ -1 +1 @@ -146758 \ No newline at end of file +146862 \ No newline at end of file diff --git a/.forge-snapshots/update dynamic fee in before swap.snap b/.forge-snapshots/update dynamic fee in before swap.snap index ff2a78a6d..bcbf0acfb 100644 --- a/.forge-snapshots/update dynamic fee in before swap.snap +++ b/.forge-snapshots/update dynamic fee in before swap.snap @@ -1 +1 @@ -149016 \ No newline at end of file +149120 \ No newline at end of file diff --git a/src/test/PoolSwapTest.sol b/src/test/PoolSwapTest.sol index 284a7471a..bdf113519 100644 --- a/src/test/PoolSwapTest.sol +++ b/src/test/PoolSwapTest.sol @@ -57,10 +57,12 @@ contract PoolSwapTest is PoolTestBase { require(deltaBefore1 == 0, "deltaBefore1 is not equal to 0"); BalanceDelta delta = manager.swap(data.key, data.params, data.hookData); - (,, int256 deltaAfter0) = _fetchBalances(data.key.currency0, data.sender, address(this)); (,, int256 deltaAfter1) = _fetchBalances(data.key.currency1, data.sender, address(this)); + bool hookCanReturnDeltaUnspecified = data.key.hooks.hasPermission(Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG) + || data.key.hooks.hasPermission(Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG); + if (data.params.zeroForOne) { if (data.params.amountSpecified < 0) { // exact input, 0 for 1 @@ -69,10 +71,14 @@ contract PoolSwapTest is PoolTestBase { "deltaAfter0 is not greater than or equal to data.params.amountSpecified" ); require(delta.amount0() == deltaAfter0, "delta.amount0() is not equal to deltaAfter0"); - require(deltaAfter1 >= 0, "deltaAfter1 is not greater than or equal to 0"); + if (!hookCanReturnDeltaUnspecified) { + require(deltaAfter1 >= 0, "deltaAfter1 is not greater than or equal to 0"); + } } else { // exact output, 0 for 1 - require(deltaAfter0 <= 0, "deltaAfter0 is not less than or equal to zero"); + if (!hookCanReturnDeltaUnspecified) { + require(deltaAfter0 <= 0, "deltaAfter0 is not less than or equal to zero"); + } require(delta.amount1() == deltaAfter1, "delta.amount1() is not equal to deltaAfter1"); require( deltaAfter1 <= data.params.amountSpecified, @@ -87,10 +93,14 @@ contract PoolSwapTest is PoolTestBase { "deltaAfter1 is not greater than or equal to data.params.amountSpecified" ); require(delta.amount1() == deltaAfter1, "delta.amount1() is not equal to deltaAfter1"); - require(deltaAfter0 >= 0, "deltaAfter0 is not greater than or equal to 0"); + if (!hookCanReturnDeltaUnspecified) { + require(deltaAfter0 >= 0, "deltaAfter0 is not greater than or equal to 0"); + } } else { // exact output, 1 for 0 - require(deltaAfter1 <= 0, "deltaAfter1 is not less than or equal to 0"); + if (!hookCanReturnDeltaUnspecified) { + require(deltaAfter1 <= 0, "deltaAfter1 is not less than or equal to 0"); + } require(delta.amount0() == deltaAfter0, "delta.amount0() is not equal to deltaAfter0"); require( deltaAfter0 <= data.params.amountSpecified, diff --git a/test/CustomAccounting.t.sol b/test/CustomAccounting.t.sol index 9e35d344f..60ca40727 100644 --- a/test/CustomAccounting.t.sol +++ b/test/CustomAccounting.t.sol @@ -20,20 +20,35 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { address hook; + address constant BEFORE_SWAP_FLAGS = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); + address constant AFTER_SWAP_FLAGS = address(uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_SWAP_RETURNS_DELTA_FLAG)); + address constant BEFORE_AND_AFTER_SWAP_FLAGS = address(uint160(BEFORE_SWAP_FLAGS) | uint160(AFTER_SWAP_FLAGS)); + function setUp() public { initializeManagerRoutersAndPoolsWithLiq(IHooks(address(0))); } - function _setUpDeltaReturnFuzzPool() internal { - address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); + function _setUpDeltaReturnFuzzPool(address hookAddr) internal { address impl = address(new DeltaReturningHook(manager)); _etchHookAndInitPool(hookAddr, impl); + + // give the hook tokens so it can give tokens + key.currency0.transfer(hook, type(uint128).max); + key.currency1.transfer(hook, type(uint128).max); + + // give the manager tokens so the hook can take tokens + key.currency0.transfer(address(manager), type(uint128).max); + key.currency1.transfer(address(manager), type(uint128).max); } function _setUpCustomCurvePool() internal { - address hookAddr = address(uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG)); + address hookAddr = BEFORE_SWAP_FLAGS; address impl = address(new CustomCurveHook(manager)); _etchHookAndInitPool(hookAddr, impl); + + // add liquidity by sending tokens straight into the contract + key.currency0.transfer(hook, 10e18); + key.currency1.transfer(hook, 10e18); } function _setUpFeeTakingPool() internal { @@ -104,10 +119,6 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { function test_swap_beforeSwapNoOpsSwap_exactInput() public { _setUpCustomCurvePool(); - // add liquidity by sending tokens straight into the contract - key.currency0.transfer(hook, 10e18); - key.currency1.transfer(hook, 10e18); - uint256 balanceBefore0 = currency0.balanceOf(address(this)); uint256 balanceBefore1 = currency1.balanceOf(address(this)); @@ -130,10 +141,6 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { function test_swap_beforeSwapNoOpsSwap_exactOutput() public { _setUpCustomCurvePool(); - // add liquidity by sending tokens straight into the contract - key.currency0.transfer(hook, 10e18); - key.currency1.transfer(hook, 10e18); - uint256 balanceBefore0 = currency0.balanceOf(address(this)); uint256 balanceBefore1 = currency1.balanceOf(address(this)); @@ -152,7 +159,7 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { assertEq(currency1.balanceOf(address(this)), balanceBefore1 + amountToSwap, "amount 1"); } - // maximum available liquidity in each direction for the pool in test_fuzz_swap_beforeSwap_returnsDeltaSpecified + // maximum available liquidity in each direction for the pool in int128 maxPossibleIn_fuzz_test = -6018336102428409; int128 maxPossibleOut_fuzz_test = 5981737760509662; @@ -162,18 +169,13 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { bool zeroForOne ) public { // ------------------------ SETUP ------------------------ - Currency specifiedCurrency; - bool isExactIn; - _setUpDeltaReturnFuzzPool(); + _setUpDeltaReturnFuzzPool(BEFORE_SWAP_FLAGS); - // initialize the pool and give the hook tokens to pay into swaps - key.currency0.transfer(hook, type(uint128).max); - key.currency1.transfer(hook, type(uint128).max); - - // bound amount specified to be a fair amount less than the amount of liquidity we have - amountSpecified = int128(bound(amountSpecified, -3e11, 3e11)); - isExactIn = amountSpecified < 0; - specifiedCurrency = (isExactIn == zeroForOne) ? key.currency0 : key.currency1; + // bound amount specified, but can be more/less than the available liquidity + amountSpecified = + int128(bound(amountSpecified, maxPossibleIn_fuzz_test - 3e11, maxPossibleOut_fuzz_test + 3e11)); + bool isExactIn = amountSpecified < 0; + Currency specifiedCurrency = (isExactIn == zeroForOne) ? key.currency0 : key.currency1; // bound delta in specified to not take more than the reserves available, nor be the minimum int to // stop the hook reverting on take/settle @@ -264,6 +266,161 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { } } + function test_fuzz_swap_beforeSwap_returnsDeltaUnspecified( + int128 hookDeltaUnspecified, + int256 amountSpecified, + bool zeroForOne + ) public { + // ------------------------ SETUP ------------------------ + _setUpDeltaReturnFuzzPool(BEFORE_SWAP_FLAGS); + + // bound amount specified, but can be more/less than the available liquidity + amountSpecified = + int128(bound(amountSpecified, maxPossibleIn_fuzz_test - 3e11, maxPossibleOut_fuzz_test + 3e11)); + bool isExactIn = amountSpecified < 0; + Currency unspecifiedCurrency = (isExactIn == zeroForOne) ? key.currency1 : key.currency0; + + // bound delta in unspecified to not take more than the reserves available + // lower bound to make sure that hookDeltaUnspecified + amountUnspecified dont exceed min(int128) + uint128 reservesOfUnspecified = uint128(unspecifiedCurrency.balanceOf(address(manager))); + hookDeltaUnspecified = int128( + bound( + hookDeltaUnspecified, int256(type(int128).min) + maxPossibleOut_fuzz_test, int128(reservesOfUnspecified) + ) + ); + + DeltaReturningHook(hook).setDeltaUnspecifiedBeforeSwap(hookDeltaUnspecified); + + // ------------------------ FUZZING CASES ------------------------ + _checkUnspecifiedDeltaFuzzCases(amountSpecified, zeroForOne, unspecifiedCurrency, hookDeltaUnspecified); + } + + function test_fuzz_swap_afterSwap_returnsDeltaUnspecified( + int128 hookDeltaUnspecified, + int256 amountSpecified, + bool zeroForOne + ) public { + // ------------------------ SETUP ------------------------ + _setUpDeltaReturnFuzzPool(AFTER_SWAP_FLAGS); + + // bound amount specified, but can be more/less than the available liquidity + amountSpecified = + int128(bound(amountSpecified, maxPossibleIn_fuzz_test - 3e11, maxPossibleOut_fuzz_test + 3e11)); + bool isExactIn = amountSpecified < 0; + Currency unspecifiedCurrency = (isExactIn == zeroForOne) ? key.currency1 : key.currency0; + + // bound delta in unspecified to not take more than the reserves available + // lower bound to make sure that hookDeltaUnspecified + amountUnspecified dont exceed min(int128) + uint128 reservesOfUnspecified = uint128(unspecifiedCurrency.balanceOf(address(manager))); + hookDeltaUnspecified = int128( + bound( + hookDeltaUnspecified, int256(type(int128).min) + maxPossibleOut_fuzz_test, int128(reservesOfUnspecified) + ) + ); + + DeltaReturningHook(hook).setDeltaUnspecifiedAfterSwap(hookDeltaUnspecified); + + // ------------------------ FUZZING CASES ------------------------ + _checkUnspecifiedDeltaFuzzCases(amountSpecified, zeroForOne, unspecifiedCurrency, hookDeltaUnspecified); + } + + function test_fuzz_swap_beforeSwap_and_afterSwap_returnDeltaUnspecified( + int128 hookDeltaUnspecifiedBeforeSwap, + int128 hookDeltaUnspecifiedAfterSwap, + int256 amountSpecified, + bool zeroForOne + ) public { + // ------------------------ SETUP ------------------------ + _setUpDeltaReturnFuzzPool(BEFORE_AND_AFTER_SWAP_FLAGS); + + // bound amount specified, but can be more/less than the available liquidity + amountSpecified = + int128(bound(amountSpecified, maxPossibleIn_fuzz_test - 3e11, maxPossibleOut_fuzz_test + 3e11)); + bool isExactIn = amountSpecified < 0; + Currency unspecifiedCurrency = (isExactIn == zeroForOne) ? key.currency1 : key.currency0; + + // bound delta in unspecified to not take more than the reserves available + // lower bound to make sure that hookDeltaUnspecified + amountUnspecified dont exceed min(int128) + uint128 reservesOfUnspecified = uint128(unspecifiedCurrency.balanceOf(address(manager))); + hookDeltaUnspecifiedBeforeSwap = int128( + bound( + hookDeltaUnspecifiedBeforeSwap, + int256(type(int128).min) + maxPossibleOut_fuzz_test, + int128(reservesOfUnspecified) + ) + ); + // bound the second delta by the first delta so that combined they do not exceed our bounds + if (hookDeltaUnspecifiedBeforeSwap >= 0) { + hookDeltaUnspecifiedAfterSwap = int128( + bound( + hookDeltaUnspecifiedAfterSwap, + int256(type(int128).min) + maxPossibleOut_fuzz_test, + int256(int128(reservesOfUnspecified) - hookDeltaUnspecifiedBeforeSwap) + ) + ); + } else { + hookDeltaUnspecifiedAfterSwap = int128( + bound( + hookDeltaUnspecifiedAfterSwap, + int256(type(int128).min) + maxPossibleOut_fuzz_test - hookDeltaUnspecifiedBeforeSwap, + int128(reservesOfUnspecified) + ) + ); + } + + DeltaReturningHook(hook).setDeltaUnspecifiedBeforeSwap(hookDeltaUnspecifiedBeforeSwap); + DeltaReturningHook(hook).setDeltaUnspecifiedAfterSwap(hookDeltaUnspecifiedAfterSwap); + int128 hookDeltaUnspecified = hookDeltaUnspecifiedBeforeSwap + hookDeltaUnspecifiedAfterSwap; + + // ------------------------ FUZZING CASES ------------------------ + _checkUnspecifiedDeltaFuzzCases(amountSpecified, zeroForOne, unspecifiedCurrency, hookDeltaUnspecified); + } + + function _checkUnspecifiedDeltaFuzzCases( + int256 amountSpecified, + bool zeroForOne, + Currency unspecifiedCurrency, + int128 hookDeltaUnspecified + ) internal { + IPoolManager.SwapParams memory params = IPoolManager.SwapParams({ + zeroForOne: zeroForOne, + amountSpecified: amountSpecified, + sqrtPriceLimitX96: (zeroForOne ? MIN_PRICE_LIMIT : MAX_PRICE_LIMIT) + }); + PoolSwapTest.TestSettings memory testSettings = + PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}); + + if (params.amountSpecified == 0) { + vm.expectRevert(IPoolManager.SwapAmountCannotBeZero.selector); + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + // successful swaps ! + } else { + uint256 balanceThisBefore = unspecifiedCurrency.balanceOf(address(this)); + uint256 balanceHookBefore = unspecifiedCurrency.balanceOf(hook); + uint256 balanceManagerBefore = unspecifiedCurrency.balanceOf(address(manager)); + + swapRouter.swap(key, params, testSettings, ZERO_BYTES); + + // in all cases the hook gets what they took exactly + assertEq( + balanceHookBefore.toInt256() + hookDeltaUnspecified, + unspecifiedCurrency.balanceOf(hook).toInt256(), + "hook balance change incorrect" + ); + + // positive if exactOut as input balances increases, negative if exactIn as output balance decreases + int256 managerDeltaUnspecified = + unspecifiedCurrency.balanceOf(address(manager)).toInt256() - balanceManagerBefore.toInt256(); + + // any delta that wasnt for the hook must have been for this swapper + assertEq( + balanceThisBefore.toInt256() - (managerDeltaUnspecified + hookDeltaUnspecified), + unspecifiedCurrency.balanceOf(address(this)).toInt256(), + "swapper balance change incorrect" + ); + } + } + // ------------------------ MODIFY LIQUIDITY ------------------------ function test_addLiquidity_withFeeTakingHook() public { @@ -275,7 +432,6 @@ contract CustomAccountingTest is Test, Deployers, GasSnapshot { uint256 hookBalanceBefore1 = currency1.balanceOf(hook); uint256 managerBalanceBefore0 = currency0.balanceOf(address(manager)); uint256 managerBalanceBefore1 = currency1.balanceOf(address(manager)); - // console2.log(address(key.hooks)); modifyLiquidityRouter.modifyLiquidity(key, LIQUIDITY_PARAMS, ZERO_BYTES); snapLastCall("addLiquidity CA fee");