Skip to content

Commit 496e2c2

Browse files
committed
refactor: rename hook native accounting
1 parent dbe0fc7 commit 496e2c2

3 files changed

Lines changed: 362 additions & 21 deletions

File tree

script/BaseSepoliaE2E.s.sol

Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.26;
3+
4+
import {console} from "forge-std/src/console.sol";
5+
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
6+
import {ForexSwap} from "../src/ForexSwap.sol";
7+
import {BaseScript} from "./Base.s.sol";
8+
import {BaseCustomAccounting} from "uniswap-hooks/src/base/BaseCustomAccounting.sol";
9+
import {IPoolManager} from "v4-core/src/interfaces/IPoolManager.sol";
10+
import {IHooks} from "v4-core/src/interfaces/IHooks.sol";
11+
import {PoolKey} from "v4-core/src/types/PoolKey.sol";
12+
import {Currency} from "v4-core/src/types/Currency.sol";
13+
import {LPFeeLibrary} from "v4-core/src/libraries/LPFeeLibrary.sol";
14+
import {Hooks} from "v4-core/src/libraries/Hooks.sol";
15+
import {TickMath} from "v4-core/src/libraries/TickMath.sol";
16+
import {PoolSwapTest} from "v4-core/src/test/PoolSwapTest.sol";
17+
18+
contract Create2FactorySepolia {
19+
error DeploymentFailed();
20+
21+
function deploy(bytes32 salt, bytes memory creationCode) external returns (address deployed) {
22+
assembly ("memory-safe") {
23+
deployed := create2(0, add(creationCode, 0x20), mload(creationCode), salt)
24+
}
25+
26+
if (deployed == address(0)) revert DeploymentFailed();
27+
}
28+
}
29+
30+
contract BaseSepoliaE2E is BaseScript {
31+
struct RunState {
32+
IPoolManager poolManager;
33+
PoolSwapTest swapRouter;
34+
MockERC20 tokenA;
35+
MockERC20 tokenB;
36+
ForexSwap hook;
37+
Create2FactorySepolia factory;
38+
PoolKey key;
39+
bytes32 salt;
40+
address predictedHook;
41+
}
42+
43+
address internal constant BASE_SEPOLIA_POOL_MANAGER = 0x05E73354cFDd6745C338b50BcFDfA3Aa6fA03408;
44+
address internal constant BASE_SEPOLIA_POOL_SWAP_TEST = 0x8B5bcC363ddE2614281aD875bad385E0A785D3B9;
45+
uint160 internal constant REQUIRED_HOOK_FLAGS = Hooks.BEFORE_INITIALIZE_FLAG | Hooks.BEFORE_ADD_LIQUIDITY_FLAG
46+
| Hooks.BEFORE_REMOVE_LIQUIDITY_FLAG | Hooks.BEFORE_SWAP_FLAG | Hooks.BEFORE_SWAP_RETURNS_DELTA_FLAG;
47+
uint160 internal constant SQRT_PRICE_1_1 = 79228162514264337593543950336;
48+
uint256 internal constant TOKEN_SUPPLY = 1_000_000e18;
49+
uint256 internal constant ADD_AMOUNT0_DESIRED = 100e18;
50+
uint256 internal constant ADD_AMOUNT1_DESIRED = 250e18;
51+
uint256 internal constant SWAP_AMOUNT_IN = 1e18;
52+
int24 internal constant TICK_SPACING = 60;
53+
int24 internal constant TICK_LOWER = -120;
54+
int24 internal constant TICK_UPPER = 120;
55+
56+
function run() public broadcast returns (ForexSwap hook) {
57+
RunState memory s;
58+
s.poolManager = IPoolManager(BASE_SEPOLIA_POOL_MANAGER);
59+
s.swapRouter = PoolSwapTest(BASE_SEPOLIA_POOL_SWAP_TEST);
60+
s.tokenA = new MockERC20("ForexSwap Base Sepolia A", "FXA", 18);
61+
s.tokenB = new MockERC20("ForexSwap Base Sepolia B", "FXB", 18);
62+
s.tokenA.mint(broadcaster, TOKEN_SUPPLY);
63+
s.tokenB.mint(broadcaster, TOKEN_SUPPLY);
64+
65+
(Currency currency0, Currency currency1) = _sortCurrencies(address(s.tokenA), address(s.tokenB));
66+
(s.hook, s.factory, s.salt, s.predictedHook) = _deployHook(s.poolManager);
67+
68+
s.key = PoolKey({
69+
currency0: currency0,
70+
currency1: currency1,
71+
fee: LPFeeLibrary.DYNAMIC_FEE_FLAG,
72+
tickSpacing: TICK_SPACING,
73+
hooks: IHooks(address(s.hook))
74+
});
75+
76+
s.poolManager.initialize(s.key, SQRT_PRICE_1_1);
77+
uint256 deadline = block.timestamp + 1 hours;
78+
(uint256 amount0, uint256 amount1, uint256 shares) = _bootstrapLiquidity(s.hook, s.tokenA, s.tokenB, deadline);
79+
uint256 quotedOut = _executeSwap(s.hook, s.swapRouter, s.key, currency1, s.tokenA, s.tokenB);
80+
_removeHalfLiquidity(s.hook, shares, deadline);
81+
82+
(uint256 reserve0, uint256 reserve1, uint256 liquidityL, uint256 priceWad, bool paused_) = s.hook.getPoolInfo();
83+
require(!paused_, "hook paused");
84+
require(reserve0 > 0 && reserve1 > 0 && liquidityL > 0 && priceWad > 0, "post-state invalid");
85+
86+
console.log("PoolManager:", address(s.poolManager));
87+
console.log("PoolSwapTest:", address(s.swapRouter));
88+
console.log("TokenA:", address(s.tokenA));
89+
console.log("TokenB:", address(s.tokenB));
90+
console.log("CREATE2 factory:", address(s.factory));
91+
console.log("Hook salt:", uint256(s.salt));
92+
console.log("Predicted hook:", s.predictedHook);
93+
console.log("Deployed hook:", address(s.hook));
94+
console.log("Bootstrap amount0:", amount0);
95+
console.log("Bootstrap amount1:", amount1);
96+
console.log("Liquidity shares:", shares);
97+
console.log("Quoted swap output:", quotedOut);
98+
console.log("Final reserve0:", reserve0);
99+
console.log("Final reserve1:", reserve1);
100+
console.log("Final liquidity:", liquidityL);
101+
console.log("Final priceWad:", priceWad);
102+
hook = s.hook;
103+
}
104+
105+
function _deployHook(IPoolManager poolManager)
106+
internal
107+
returns (ForexSwap hook, Create2FactorySepolia factory, bytes32 salt, address predictedHook)
108+
{
109+
factory = new Create2FactorySepolia();
110+
bytes memory creationCode = abi.encodePacked(type(ForexSwap).creationCode, abi.encode(poolManager));
111+
bytes32 initCodeHash = keccak256(creationCode);
112+
(salt, predictedHook) = _mineHookSalt(address(factory), initCodeHash);
113+
hook = ForexSwap(factory.deploy(salt, creationCode));
114+
}
115+
116+
function _bootstrapLiquidity(ForexSwap hook, MockERC20 tokenA, MockERC20 tokenB, uint256 deadline)
117+
internal
118+
returns (uint256 amount0, uint256 amount1, uint256 shares)
119+
{
120+
tokenA.approve(address(hook), type(uint256).max);
121+
tokenB.approve(address(hook), type(uint256).max);
122+
123+
(amount0, amount1, shares) =
124+
hook.addLiquidityWithSlippage(ADD_AMOUNT0_DESIRED, ADD_AMOUNT1_DESIRED, 0, 0, deadline);
125+
require(amount0 > 0 && amount1 > 0 && shares > 0, "bootstrap quote failed");
126+
127+
hook.addLiquidity(
128+
BaseCustomAccounting.AddLiquidityParams({
129+
amount0Desired: ADD_AMOUNT0_DESIRED,
130+
amount1Desired: ADD_AMOUNT1_DESIRED,
131+
amount0Min: 0,
132+
amount1Min: 0,
133+
to: broadcaster,
134+
deadline: deadline,
135+
tickLower: TICK_LOWER,
136+
tickUpper: TICK_UPPER,
137+
salt: ZERO_SALT
138+
})
139+
);
140+
141+
require(hook.balanceOf(broadcaster) == shares, "liquidity shares mismatch");
142+
}
143+
144+
function _executeSwap(
145+
ForexSwap hook,
146+
PoolSwapTest swapRouter,
147+
PoolKey memory key,
148+
Currency currency1,
149+
MockERC20 tokenA,
150+
MockERC20 tokenB
151+
) internal returns (uint256 quotedOut) {
152+
tokenA.approve(address(swapRouter), type(uint256).max);
153+
tokenB.approve(address(swapRouter), type(uint256).max);
154+
155+
quotedOut = hook.calculateAmountOut(SWAP_AMOUNT_IN, true);
156+
require(quotedOut > 0, "swap quote failed");
157+
158+
uint256 token1BalanceBefore = MockERC20(Currency.unwrap(currency1)).balanceOf(broadcaster);
159+
swapRouter.swap(
160+
key,
161+
IPoolManager.SwapParams({
162+
zeroForOne: true,
163+
amountSpecified: -int256(SWAP_AMOUNT_IN),
164+
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
165+
}),
166+
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
167+
""
168+
);
169+
uint256 token1BalanceAfter = MockERC20(Currency.unwrap(currency1)).balanceOf(broadcaster);
170+
require(token1BalanceAfter > token1BalanceBefore, "swap output missing");
171+
}
172+
173+
function _removeHalfLiquidity(ForexSwap hook, uint256 shares, uint256 deadline) internal {
174+
uint256 sharesToRemove = shares / 2;
175+
require(sharesToRemove > 0, "remove amount too small");
176+
hook.removeLiquidity(
177+
BaseCustomAccounting.RemoveLiquidityParams({
178+
liquidity: sharesToRemove,
179+
amount0Min: 0,
180+
amount1Min: 0,
181+
deadline: deadline,
182+
tickLower: TICK_LOWER,
183+
tickUpper: TICK_UPPER,
184+
salt: ZERO_SALT
185+
})
186+
);
187+
}
188+
189+
function _sortCurrencies(address tokenA, address tokenB) internal pure returns (Currency currency0, Currency currency1) {
190+
currency0 = Currency.wrap(tokenA);
191+
currency1 = Currency.wrap(tokenB);
192+
if (Currency.unwrap(currency0) > Currency.unwrap(currency1)) {
193+
(currency0, currency1) = (currency1, currency0);
194+
}
195+
}
196+
197+
function _mineHookSalt(address deployer, bytes32 initCodeHash) internal view returns (bytes32 salt, address hook) {
198+
for (uint256 candidate = 0; candidate < type(uint24).max; candidate++) {
199+
salt = bytes32(candidate);
200+
hook = vm.computeCreate2Address(salt, initCodeHash, deployer);
201+
if (uint160(hook) & Hooks.ALL_HOOK_MASK == REQUIRED_HOOK_FLAGS) {
202+
return (salt, hook);
203+
}
204+
}
205+
206+
revert("No hook salt found");
207+
}
208+
}

0 commit comments

Comments
 (0)