Skip to content

Commit bc3a376

Browse files
committed
feat: add AfterAddLiquidityTest and HookWithReturns for testing liquidity hooks
- Introduced `AfterAddLiquidityTest` to validate the behavior of afterAddLiquidity hooks with various scenarios, including single and multiple hooks, and mixed hook types. - Added `HookWithReturns` contract to facilitate testing by allowing custom BalanceDelta return values from hooks. - Implemented comprehensive test cases to ensure correct handling of balance deltas and proper execution flow for registered hooks.
1 parent 16cf573 commit bc3a376

3 files changed

Lines changed: 346 additions & 1 deletion

File tree

src/base/MultiHookAdapterBase.sol

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ abstract contract MultiHooksAdapterBase is BaseHook, IMultiHookAdapterBase {
281281
combinedDelta = add(combinedDelta, hookDelta);
282282
} else {
283283
(bool success, bytes memory result) = address(subHooks[i]).call(
284-
abi.encodeWithSelector(IHooks.afterAddLiquidity.selector, sender, key, params, delta, data)
284+
abi.encodeWithSelector(
285+
IHooks.afterAddLiquidity.selector, sender, key, params, delta, feesAccrued, data
286+
)
285287
);
286288
require(success, "Sub-hook afterAddLiquidity failed");
287289
require(
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {MultiHookAdapterBaseTest} from "../base/MultiHookAdapterBaseTest.sol";
5+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
6+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
7+
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
8+
import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
9+
import {HookWithEvents} from "../mocs/HookWithEvents.sol";
10+
import {HookWithReturns} from "../mocs/HookWithReturns.sol";
11+
import {BalanceDelta, toBalanceDelta, BalanceDeltaLibrary, add} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
12+
import {Vm} from "forge-std/Vm.sol";
13+
14+
contract AfterAddLiquidityTest is MultiHookAdapterBaseTest {
15+
// Basic ModifyLiquidityParams for testing
16+
ModifyLiquidityParams public testParams;
17+
// Balance deltas for testing
18+
BalanceDelta public testDelta;
19+
BalanceDelta public testFees;
20+
21+
// Hooks with BalanceDelta returns
22+
HookWithReturns public afterAddLiquidityReturnHook;
23+
HookWithReturns public secondAfterAddLiquidityReturnHook;
24+
HookWithReturns public thirdAfterAddLiquidityReturnHook;
25+
26+
function setUp() public override {
27+
super.setUp();
28+
29+
// Set up standard test parameters for liquidity addition (positive liquidityDelta)
30+
testParams = ModifyLiquidityParams({
31+
tickLower: -120,
32+
tickUpper: 120,
33+
liquidityDelta: 1e18, // Positive for addition
34+
salt: bytes32(0)
35+
});
36+
37+
// Set up test deltas
38+
testDelta = toBalanceDelta(1000, 2000); // amount0 = 1000, amount1 = 2000
39+
testFees = toBalanceDelta(10, 20); // amount0 = 10, amount1 = 20
40+
41+
// Deploy hooks with return delta flags at valid addresses
42+
deployReturnHooks();
43+
}
44+
45+
// Helper to deploy hooks with different addresses but same flags
46+
function deployReturnHooks() private {
47+
// First hook - AFTER_ADD_LIQUIDITY_FLAG | AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG
48+
address hookAddress =
49+
address(uint160(Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG));
50+
deployCodeTo("HookWithReturns.sol", "", hookAddress);
51+
afterAddLiquidityReturnHook = HookWithReturns(hookAddress);
52+
53+
// Second hook - same flags but different address (with 0x1000 offset)
54+
address secondHookAddress =
55+
address(uint160(0x1000 | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG));
56+
deployCodeTo("HookWithReturns.sol", "", secondHookAddress);
57+
secondAfterAddLiquidityReturnHook = HookWithReturns(secondHookAddress);
58+
59+
// Third hook - same flags but different address (with 0x2000 offset)
60+
address thirdHookAddress =
61+
address(uint160(0x2000 | Hooks.AFTER_ADD_LIQUIDITY_FLAG | Hooks.AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG));
62+
deployCodeTo("HookWithReturns.sol", "", thirdHookAddress);
63+
thirdAfterAddLiquidityReturnHook = HookWithReturns(thirdHookAddress);
64+
}
65+
66+
// Simple test case with a single hook that returns a BalanceDelta
67+
function test_AfterAddLiquidity_SingleHookWithDelta() public {
68+
// Setup
69+
address sender = address(0x123);
70+
PoolKey memory testPoolKey = createTestPoolKey();
71+
bytes memory hookData = "test data";
72+
73+
// Set up the return value for our hook
74+
BalanceDelta hookDelta = toBalanceDelta(100, 200); // amount0 = 100, amount1 = 200
75+
afterAddLiquidityReturnHook.setReturnValue(hookDelta);
76+
77+
// Register a single hook with both AFTER_ADD_LIQUIDITY_FLAG and AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG
78+
address[] memory hooks = new address[](1);
79+
hooks[0] = address(afterAddLiquidityReturnHook);
80+
adapter.registerHooks(testPoolKey, hooks);
81+
82+
// Impersonate the pool manager
83+
impersonatePoolManager();
84+
85+
// Call afterAddLiquidity as the pool manager
86+
(bytes4 result, BalanceDelta resultDelta) =
87+
adapter.afterAddLiquidity(sender, testPoolKey, testParams, testDelta, testFees, hookData);
88+
89+
// Verify the result selector is correct
90+
assertEq(result, IHooks.afterAddLiquidity.selector, "Should return afterAddLiquidity selector");
91+
92+
// Verify the result delta matches what our hook returned
93+
assertEq(
94+
BalanceDeltaLibrary.amount0(resultDelta),
95+
BalanceDeltaLibrary.amount0(hookDelta),
96+
"amount0 delta should match"
97+
);
98+
assertEq(
99+
BalanceDeltaLibrary.amount1(resultDelta),
100+
BalanceDeltaLibrary.amount1(hookDelta),
101+
"amount1 delta should match"
102+
);
103+
}
104+
105+
// Test case with multiple hooks that all return BalanceDelta values that should be combined
106+
function test_AfterAddLiquidity_MultipleHooksWithDelta() public {
107+
// Setup
108+
address sender = address(0x123);
109+
PoolKey memory testPoolKey = createTestPoolKey();
110+
bytes memory hookData = "test data";
111+
112+
// Set up different return values for each hook
113+
BalanceDelta firstHookDelta = toBalanceDelta(100, 200); // amount0 = 100, amount1 = 200
114+
BalanceDelta secondHookDelta = toBalanceDelta(300, 400); // amount0 = 300, amount1 = 400
115+
BalanceDelta thirdHookDelta = toBalanceDelta(50, 150); // amount0 = 50, amount1 = 150
116+
117+
// Calculate expected combined delta
118+
BalanceDelta expectedCombinedDelta = add(firstHookDelta, secondHookDelta);
119+
expectedCombinedDelta = add(expectedCombinedDelta, thirdHookDelta);
120+
121+
// Set hook return values
122+
afterAddLiquidityReturnHook.setReturnValue(firstHookDelta);
123+
secondAfterAddLiquidityReturnHook.setReturnValue(secondHookDelta);
124+
thirdAfterAddLiquidityReturnHook.setReturnValue(thirdHookDelta);
125+
126+
// Register all three hooks
127+
address[] memory hooks = new address[](3);
128+
hooks[0] = address(afterAddLiquidityReturnHook);
129+
hooks[1] = address(secondAfterAddLiquidityReturnHook);
130+
hooks[2] = address(thirdAfterAddLiquidityReturnHook);
131+
adapter.registerHooks(testPoolKey, hooks);
132+
133+
// Impersonate the pool manager
134+
impersonatePoolManager();
135+
136+
// Call afterAddLiquidity as the pool manager
137+
(bytes4 result, BalanceDelta resultDelta) =
138+
adapter.afterAddLiquidity(sender, testPoolKey, testParams, testDelta, testFees, hookData);
139+
140+
// Verify the result selector is correct
141+
assertEq(result, IHooks.afterAddLiquidity.selector, "Should return afterAddLiquidity selector");
142+
143+
// Verify the combined delta is correct
144+
assertEq(
145+
BalanceDeltaLibrary.amount0(resultDelta),
146+
BalanceDeltaLibrary.amount0(expectedCombinedDelta),
147+
"Combined amount0 delta should match sum of all hooks"
148+
);
149+
assertEq(
150+
BalanceDeltaLibrary.amount1(resultDelta),
151+
BalanceDeltaLibrary.amount1(expectedCombinedDelta),
152+
"Combined amount1 delta should match sum of all hooks"
153+
);
154+
155+
// Verify we get the expected value by checking individual components
156+
int128 expectedAmount0 = 100 + 300 + 50; // 450
157+
int128 expectedAmount1 = 200 + 400 + 150; // 750
158+
assertEq(BalanceDeltaLibrary.amount0(resultDelta), expectedAmount0, "Combined amount0 delta should equal 450");
159+
assertEq(BalanceDeltaLibrary.amount1(resultDelta), expectedAmount1, "Combined amount1 delta should equal 750");
160+
}
161+
162+
// Test case with a mix of hooks, some with return delta flag and some without
163+
function test_AfterAddLiquidity_MixedHooks() public {
164+
// Setup
165+
address sender = address(0x123);
166+
PoolKey memory testPoolKey = createTestPoolKey();
167+
bytes memory hookData = "test data";
168+
169+
// Set return values for hooks with returns
170+
BalanceDelta firstHookDelta = toBalanceDelta(100, 200); // amount0 = 100, amount1 = 200
171+
BalanceDelta secondHookDelta = toBalanceDelta(300, 400); // amount0 = 300, amount1 = 400
172+
173+
// Calculate expected combined delta (only from hooks with returns flag)
174+
BalanceDelta expectedCombinedDelta = add(firstHookDelta, secondHookDelta);
175+
176+
afterAddLiquidityReturnHook.setReturnValue(firstHookDelta);
177+
secondAfterAddLiquidityReturnHook.setReturnValue(secondHookDelta);
178+
179+
// First, check if the HookWithEvents has a compatible afterAddLiquidity function signature
180+
// We need a regular hook that doesn't implement the RETURNS_DELTA functionality but still
181+
// has the compatible parameter list for afterAddLiquidity
182+
183+
// We're going to test using a HookWithReturns for the regular hook too, but without the RETURNS_DELTA flag
184+
// This ensures it will have the right parameter signature
185+
address regularHookAddr = address(uint160(0x3000 | Hooks.AFTER_ADD_LIQUIDITY_FLAG)); // Only AFTER_ADD_LIQUIDITY_FLAG
186+
deployCodeTo("HookWithReturns.sol", "", regularHookAddr);
187+
HookWithReturns regularHook = HookWithReturns(regularHookAddr);
188+
189+
// Register a mix of hook types:
190+
// 1. Hook with AFTER_ADD_LIQUIDITY_FLAG and AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG
191+
// 2. Hook with only AFTER_ADD_LIQUIDITY_FLAG (no RETURNS_DELTA)
192+
// 3. Hook with AFTER_ADD_LIQUIDITY_FLAG and AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG
193+
address[] memory hooks = new address[](3);
194+
hooks[0] = address(afterAddLiquidityReturnHook); // Has both flags
195+
hooks[1] = address(regularHook); // Only has AFTER_ADD_LIQUIDITY_FLAG
196+
hooks[2] = address(secondAfterAddLiquidityReturnHook); // Has both flags
197+
198+
adapter.registerHooks(testPoolKey, hooks);
199+
200+
// Impersonate the pool manager
201+
impersonatePoolManager();
202+
203+
// Call afterAddLiquidity as the pool manager
204+
(bytes4 result, BalanceDelta resultDelta) =
205+
adapter.afterAddLiquidity(sender, testPoolKey, testParams, testDelta, testFees, hookData);
206+
207+
// Verify the result selector is correct
208+
assertEq(result, IHooks.afterAddLiquidity.selector, "Should return afterAddLiquidity selector");
209+
210+
// Verify the combined delta includes only hooks with return delta flag
211+
assertEq(
212+
BalanceDeltaLibrary.amount0(resultDelta),
213+
BalanceDeltaLibrary.amount0(expectedCombinedDelta),
214+
"Combined amount0 delta should only include hooks with return delta flag"
215+
);
216+
assertEq(
217+
BalanceDeltaLibrary.amount1(resultDelta),
218+
BalanceDeltaLibrary.amount1(expectedCombinedDelta),
219+
"Combined amount1 delta should only include hooks with return delta flag"
220+
);
221+
222+
// Verify the expected amounts directly
223+
assertEq(
224+
BalanceDeltaLibrary.amount0(resultDelta),
225+
100 + 300, // 400 (only hooks with return delta flag)
226+
"Combined amount0 delta should equal 400"
227+
);
228+
assertEq(
229+
BalanceDeltaLibrary.amount1(resultDelta),
230+
200 + 400, // 600 (only hooks with return delta flag)
231+
"Combined amount1 delta should equal 600"
232+
);
233+
}
234+
}

test/mocs/HookWithReturns.sol

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.24;
3+
4+
import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
5+
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
6+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
7+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
8+
import {PoolId} from "@uniswap/v4-core/src/types/PoolId.sol";
9+
import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
10+
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
11+
import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
12+
13+
/**
14+
* @title HookWithReturns
15+
* @notice A configurable hook implementation that returns custom BalanceDelta values
16+
* @dev Used for testing functions that return and aggregate BalanceDelta values
17+
*/
18+
contract HookWithReturns is IHooks {
19+
// Custom return values for testing
20+
BalanceDelta private _customBalanceDelta;
21+
22+
// Events for tracking hook calls
23+
event AfterAddLiquidityCalled(address sender, bytes32 poolId, BalanceDelta delta, BalanceDelta fees);
24+
event AfterRemoveLiquidityCalled(address sender, bytes32 poolId, BalanceDelta delta, BalanceDelta fees);
25+
26+
// Set a custom BalanceDelta to return from hooks
27+
function setReturnValue(BalanceDelta delta) external {
28+
_customBalanceDelta = delta;
29+
}
30+
31+
// Get the current custom BalanceDelta
32+
function getReturnValue() external view returns (BalanceDelta) {
33+
return _customBalanceDelta;
34+
}
35+
36+
// Implementation of hook functions that return BalanceDelta
37+
function afterAddLiquidity(
38+
address sender,
39+
PoolKey calldata key,
40+
ModifyLiquidityParams calldata,
41+
BalanceDelta delta,
42+
BalanceDelta fees,
43+
bytes calldata
44+
) external returns (bytes4, BalanceDelta) {
45+
emit AfterAddLiquidityCalled(sender, keccak256(abi.encode(key)), delta, fees);
46+
return (IHooks.afterAddLiquidity.selector, _customBalanceDelta);
47+
}
48+
49+
function afterRemoveLiquidity(
50+
address sender,
51+
PoolKey calldata key,
52+
ModifyLiquidityParams calldata,
53+
BalanceDelta delta,
54+
BalanceDelta fees,
55+
bytes calldata
56+
) external returns (bytes4, BalanceDelta) {
57+
emit AfterRemoveLiquidityCalled(sender, keccak256(abi.encode(key)), delta, fees);
58+
return (IHooks.afterRemoveLiquidity.selector, _customBalanceDelta);
59+
}
60+
61+
// Basic implementations of other required functions that don't need to return values for our tests
62+
function beforeInitialize(address, PoolKey calldata, uint160) external pure returns (bytes4) {
63+
return IHooks.beforeInitialize.selector;
64+
}
65+
66+
function afterInitialize(address, PoolKey calldata, uint160, int24) external pure returns (bytes4) {
67+
return IHooks.afterInitialize.selector;
68+
}
69+
70+
function beforeAddLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata)
71+
external
72+
pure
73+
returns (bytes4)
74+
{
75+
return IHooks.beforeAddLiquidity.selector;
76+
}
77+
78+
function beforeRemoveLiquidity(address, PoolKey calldata, ModifyLiquidityParams calldata, bytes calldata)
79+
external
80+
pure
81+
returns (bytes4)
82+
{
83+
return IHooks.beforeRemoveLiquidity.selector;
84+
}
85+
86+
function beforeSwap(address, PoolKey calldata, SwapParams calldata, bytes calldata)
87+
external
88+
pure
89+
returns (bytes4, BeforeSwapDelta, uint24)
90+
{
91+
return (IHooks.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
92+
}
93+
94+
function afterSwap(address, PoolKey calldata, SwapParams calldata, BalanceDelta, bytes calldata)
95+
external
96+
pure
97+
returns (bytes4, int128)
98+
{
99+
return (IHooks.afterSwap.selector, 0);
100+
}
101+
102+
function beforeDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) {
103+
return IHooks.beforeDonate.selector;
104+
}
105+
106+
function afterDonate(address, PoolKey calldata, uint256, uint256, bytes calldata) external pure returns (bytes4) {
107+
return IHooks.afterDonate.selector;
108+
}
109+
}

0 commit comments

Comments
 (0)