@bananapus/univ4-lp-split-hook-v6 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILLS.md CHANGED
@@ -8,7 +8,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
8
8
 
9
9
  | Contract | Role |
10
10
  |----------|------|
11
- | `UniV4DeploymentSplitHook` | Core split hook. Implements `IJBSplitHook.processSplitWith` to accumulate or burn tokens. Manages V4 pool creation, LP position minting, fee collection, and liquidity rebalancing. Inherits `JBPermissioned`, `Ownable`. Deployed as clones via factory. |
11
+ | `UniV4DeploymentSplitHook` | Core split hook. Implements `IJBSplitHook.processSplitWith` to accumulate or burn tokens. Manages V4 pool creation, LP position minting, fee collection, and liquidity rebalancing. Inherits `JBPermissioned`. Deployed as clones via factory. |
12
12
  | `UniV4DeploymentSplitHookDeployer` | Factory that deploys hook clones via `LibClone` (Solady). Supports CREATE2 deterministic deployment. Initializes clones with `feeProjectId` and `feePercent`. Registers each deployed clone in `JBAddressRegistry` so frontends can verify the deployer. |
13
13
 
14
14
  ## Key Functions
@@ -71,7 +71,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
71
71
  | `@bananapus/permission-ids-v6` | `JBPermissionIds` | `SET_BUYBACK_POOL` permission ID |
72
72
  | `@uniswap/v4-core` | `IPoolManager`, `PoolKey`, `PoolId`, `Currency`, `TickMath`, `IHooks` | V4 pool creation, price math, currency handling |
73
73
  | `@uniswap/v4-periphery` | `IPositionManager`, `Actions`, `LiquidityAmounts` | V4 position management: mint, modify, burn, collect |
74
- | `@openzeppelin/contracts` | `IERC20`, `IERC20Metadata`, `SafeERC20`, `Ownable` | Token operations, ownership |
74
+ | `@openzeppelin/contracts` | `IERC20`, `IERC20Metadata`, `SafeERC20` | Token operations |
75
75
  | `@prb/math` | `mulDiv`, `sqrt` | Overflow-safe arithmetic for sqrtPriceX96 calculations |
76
76
  | `@bananapus/address-registry-v6` | `IJBAddressRegistry` | On-chain registry mapping deployed hooks to their deployer contract |
77
77
  | `solady` | `LibClone` | Clone factory for deploying hook instances |
@@ -107,7 +107,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
107
107
  | `UniV4DeploymentSplitHook_InvalidFeePercent` | `feePercent > BPS` (> 100%) |
108
108
  | `UniV4DeploymentSplitHook_InvalidTerminalToken` | No primary terminal found for project/token pair |
109
109
  | `UniV4DeploymentSplitHook_PoolAlreadyDeployed` | `deployPool` called for a pair that already has a position |
110
- | `UniV4DeploymentSplitHook_AlreadyInitialized` | `initialize` called on a clone that was already initialized (uses explicit `initialized` flag, safe against `renounceOwnership` re-init) |
110
+ | `UniV4DeploymentSplitHook_AlreadyInitialized` | `initialize` called on a clone that was already initialized |
111
111
  | `UniV4DeploymentSplitHook_FeePercentWithoutFeeProject` | `initialize` called with `feePercent > 0` but `feeProjectId == 0` (fees would get stuck since `primaryTerminalOf(0, token)` returns `address(0)`) |
112
112
 
113
113
  ## Constants
@@ -127,7 +127,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
127
127
  | `accumulatedProjectTokens` | `projectId => uint256` | Pre-deployment token accumulation |
128
128
  | `projectDeployed` | `projectId => bool` | Switches accumulate (Stage 1) to burn (Stage 2) |
129
129
  | `claimableFeeTokens` | `projectId => uint256` | Fee-project tokens claimable via `claimFeeTokensFor` |
130
- | `initialized` | `bool` | Prevents re-initialization after `renounceOwnership()` (explicit flag instead of relying on `owner() == address(0)`) |
130
+ | `initialized` | `bool` | Prevents re-initialization of clone instances |
131
131
 
132
132
  ## Gotchas
133
133
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/univ4-lp-split-hook-v6",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,7 +19,6 @@ import {JBConstants} from "@bananapus/core/libraries/JBConstants.sol";
19
19
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
20
20
  import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
21
21
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
22
- import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
23
22
  import {mulDiv, sqrt} from "@prb/math/src/Common.sol";
24
23
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
25
24
  import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
@@ -52,7 +51,7 @@ import {IUniV4DeploymentSplitHook} from "./interfaces/IUniV4DeploymentSplitHook.
52
51
  * @dev For any given Uniswap V4 pool, the contract will control a single LP position.
53
52
  * @dev Pool deployment requires SET_BUYBACK_POOL permission from the project owner.
54
53
  */
55
- contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JBPermissioned, Ownable {
54
+ contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JBPermissioned {
56
55
  using JBRulesetMetadataResolver for JBRuleset;
57
56
  using SafeERC20 for IERC20;
58
57
  using PoolIdLibrary for PoolKey;
@@ -129,7 +128,7 @@ contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JB
129
128
  /// @notice ProjectID => Fee tokens claimable by that project
130
129
  mapping(uint256 projectId => uint256 claimableFeeTokens) public claimableFeeTokens;
131
130
 
132
- /// @notice Whether this clone instance has been initialized (prevents re-initialization after renounceOwnership).
131
+ /// @notice Whether this clone instance has been initialized.
133
132
  bool public initialized;
134
133
 
135
134
  //*********************************************************************//
@@ -149,7 +148,6 @@ contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JB
149
148
  IPositionManager positionManager
150
149
  )
151
150
  JBPermissioned(permissions)
152
- Ownable(msg.sender)
153
151
  {
154
152
  if (directory == address(0)) revert UniV4DeploymentSplitHook_ZeroAddressNotAllowed();
155
153
  if (tokens == address(0)) revert UniV4DeploymentSplitHook_ZeroAddressNotAllowed();
@@ -163,12 +161,9 @@ contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JB
163
161
  }
164
162
 
165
163
  /// @notice Initialize per-instance config on a clone. Can only be called once.
166
- /// @dev Uses an explicit `initialized` flag rather than relying on owner() == address(0),
167
- /// which would allow re-initialization after renounceOwnership().
168
- /// @param initialOwner The owner of this clone instance.
169
164
  /// @param feeProjectId Project ID to receive LP fees.
170
165
  /// @param feePercent Percentage of LP fees to route to fee project (in basis points, e.g., 3800 = 38%).
171
- function initialize(address initialOwner, uint256 feeProjectId, uint256 feePercent) external {
166
+ function initialize(uint256 feeProjectId, uint256 feePercent) external {
172
167
  if (initialized) revert UniV4DeploymentSplitHook_AlreadyInitialized();
173
168
 
174
169
  if (feePercent > BPS) revert UniV4DeploymentSplitHook_InvalidFeePercent();
@@ -185,8 +180,6 @@ contract UniV4DeploymentSplitHook is IUniV4DeploymentSplitHook, IJBSplitHook, JB
185
180
  initialized = true;
186
181
  FEE_PROJECT_ID = feeProjectId;
187
182
  FEE_PERCENT = feePercent;
188
-
189
- _transferOwnership(initialOwner);
190
183
  }
191
184
 
192
185
  /// @notice Accept ETH transfers (needed for cashOut with native ETH and V4 TAKE operations).
@@ -66,7 +66,7 @@ contract UniV4DeploymentSplitHookDeployer is IUniV4DeploymentSplitHookDeployer {
66
66
  })
67
67
  );
68
68
 
69
- IUniV4DeploymentSplitHook(address(hook)).initialize(msg.sender, feeProjectId, feePercent);
69
+ IUniV4DeploymentSplitHook(address(hook)).initialize(feeProjectId, feePercent);
70
70
 
71
71
  emit HookDeployed(feeProjectId, feePercent, hook, msg.sender);
72
72
 
@@ -35,11 +35,10 @@ interface IUniV4DeploymentSplitHook {
35
35
 
36
36
  /**
37
37
  * @notice Initialize per-instance config on a clone.
38
- * @param initialOwner The owner of this clone instance.
39
38
  * @param feeProjectId Project ID to receive LP fees.
40
39
  * @param feePercent Percentage of LP fees to route to fee project (in basis points).
41
40
  */
42
- function initialize(address initialOwner, uint256 feeProjectId, uint256 feePercent) external;
41
+ function initialize(uint256 feeProjectId, uint256 feePercent) external;
43
42
 
44
43
  /**
45
44
  * @notice Check if a pool has been deployed for a project/terminal token pair
@@ -25,11 +25,10 @@ contract ConstructorTest is LPSplitHookV4TestBase {
25
25
  assertEq(address(hook.POSITION_MANAGER()), address(positionManager), "POSITION_MANAGER mismatch");
26
26
  }
27
27
 
28
- /// @notice Verify initialize() sets per-clone config (owner, feeProjectId, feePercent).
28
+ /// @notice Verify initialize() sets per-clone config (feeProjectId, feePercent).
29
29
  function test_Initialize_SetsCloneConfig() public view {
30
30
  assertEq(hook.FEE_PROJECT_ID(), FEE_PROJECT_ID, "FEE_PROJECT_ID mismatch");
31
31
  assertEq(hook.FEE_PERCENT(), FEE_PERCENT, "FEE_PERCENT mismatch");
32
- assertEq(hook.owner(), owner, "owner mismatch");
33
32
  }
34
33
 
35
34
  /// @notice Constructor reverts when directory is address(0).
@@ -94,11 +93,9 @@ contract ConstructorTest is LPSplitHookV4TestBase {
94
93
  IPoolManager(address(1)),
95
94
  IPositionManager(address(positionManager))
96
95
  );
97
- // Zero out slot 0 (owner) so initialize() can be called
98
- vm.store(address(impl), bytes32(uint256(0)), bytes32(0));
99
96
 
100
97
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_InvalidFeePercent.selector);
101
- impl.initialize(owner, FEE_PROJECT_ID, 10_001);
98
+ impl.initialize(FEE_PROJECT_ID, 10_001);
102
99
  }
103
100
 
104
101
  /// @notice initialize() reverts when feeProjectId is 0 but feePercent > 0 (L-25 fix).
@@ -111,11 +108,9 @@ contract ConstructorTest is LPSplitHookV4TestBase {
111
108
  IPoolManager(address(1)),
112
109
  IPositionManager(address(positionManager))
113
110
  );
114
- // Zero out slot 0 (owner) so initialize() can be called
115
- vm.store(address(impl), bytes32(uint256(0)), bytes32(0));
116
111
 
117
112
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_FeePercentWithoutFeeProject.selector);
118
- impl.initialize(owner, 0, FEE_PERCENT);
113
+ impl.initialize(0, FEE_PERCENT);
119
114
  }
120
115
 
121
116
  /// @notice When both feeProjectId and feePercent are 0, initialize() succeeds (no fees configured).
@@ -128,14 +123,11 @@ contract ConstructorTest is LPSplitHookV4TestBase {
128
123
  IPoolManager(address(1)),
129
124
  IPositionManager(address(positionManager))
130
125
  );
131
- // Zero out slot 0 (owner) so initialize() can be called
132
- vm.store(address(impl), bytes32(uint256(0)), bytes32(0));
133
126
 
134
- impl.initialize(owner, 0, 0);
127
+ impl.initialize(0, 0);
135
128
 
136
129
  assertEq(impl.FEE_PROJECT_ID(), 0, "FEE_PROJECT_ID should be 0");
137
130
  assertEq(impl.FEE_PERCENT(), 0, "FEE_PERCENT should be 0");
138
- assertEq(impl.owner(), owner, "owner mismatch");
139
131
  }
140
132
 
141
133
  /// @notice Calling initialize() a second time reverts with AlreadyInitialized.
@@ -148,14 +140,12 @@ contract ConstructorTest is LPSplitHookV4TestBase {
148
140
  IPoolManager(address(1)),
149
141
  IPositionManager(address(positionManager))
150
142
  );
151
- // Zero out slot 0 (owner) so initialize() can be called
152
- vm.store(address(impl), bytes32(uint256(0)), bytes32(0));
153
143
 
154
144
  // First init succeeds
155
- impl.initialize(owner, FEE_PROJECT_ID, FEE_PERCENT);
145
+ impl.initialize(FEE_PROJECT_ID, FEE_PERCENT);
156
146
 
157
147
  // Second init reverts
158
148
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_AlreadyInitialized.selector);
159
- impl.initialize(owner, FEE_PROJECT_ID, FEE_PERCENT);
149
+ impl.initialize(FEE_PROJECT_ID, FEE_PERCENT);
160
150
  }
161
151
  }
@@ -99,7 +99,6 @@ contract DeployerTest is Test {
99
99
  IUniV4DeploymentSplitHook hook = deployer.deployHookFor(FEE_PROJECT_ID, FEE_PERCENT, bytes32(0));
100
100
 
101
101
  UniV4DeploymentSplitHook concreteHook = UniV4DeploymentSplitHook(payable(address(hook)));
102
- assertEq(concreteHook.owner(), caller, "owner not set");
103
102
  assertEq(concreteHook.FEE_PROJECT_ID(), FEE_PROJECT_ID, "feeProjectId not set");
104
103
  assertEq(concreteHook.FEE_PERCENT(), FEE_PERCENT, "feePercent not set");
105
104
  }
@@ -43,8 +43,7 @@ contract NativeETHTest is LPSplitHookV4TestBase {
43
43
  IPoolManager(address(1)),
44
44
  IPositionManager(address(positionManager))
45
45
  );
46
- vm.store(address(testableHook), bytes32(uint256(0)), bytes32(0));
47
- testableHook.initialize(owner, FEE_PROJECT_ID, FEE_PERCENT);
46
+ testableHook.initialize(FEE_PROJECT_ID, FEE_PERCENT);
48
47
  }
49
48
 
50
49
  // -----------------------------------------------------------------------
@@ -137,8 +137,7 @@ contract PriceMathTest is LPSplitHookV4TestBase {
137
137
  IPoolManager(address(1)),
138
138
  IPositionManager(address(positionManager))
139
139
  );
140
- vm.store(address(testableHook), bytes32(uint256(0)), bytes32(0));
141
- testableHook.initialize(owner, FEE_PROJECT_ID, FEE_PERCENT);
140
+ testableHook.initialize(FEE_PROJECT_ID, FEE_PERCENT);
142
141
  }
143
142
 
144
143
  // ─────────────────────────────────────────────────────────────────────
@@ -142,7 +142,7 @@ contract LPSplitHookV4TestBase is Test {
142
142
  IPositionManager(address(positionManager))
143
143
  );
144
144
  hook = UniV4DeploymentSplitHook(payable(LibClone.clone(address(hookImpl))));
145
- hook.initialize(owner, FEE_PROJECT_ID, FEE_PERCENT);
145
+ hook.initialize(FEE_PROJECT_ID, FEE_PERCENT);
146
146
  }
147
147
 
148
148
  // ─── Directory Helpers (write to fallback-based mock) ───────────────
@@ -53,18 +53,17 @@ contract L25_FeeProjectIdValidationTest is Test {
53
53
  UniV4DeploymentSplitHook clone = UniV4DeploymentSplitHook(payable(LibClone.clone(address(hookImpl))));
54
54
 
55
55
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_FeePercentWithoutFeeProject.selector);
56
- clone.initialize(address(this), 0, 3800); // feeProjectId=0, feePercent=38%
56
+ clone.initialize(0, 3800); // feeProjectId=0, feePercent=38%
57
57
  }
58
58
 
59
59
  /// @notice initialize succeeds when feePercent == 0 and feeProjectId == 0 (no fees configured).
60
60
  function test_initialize_succeeds_zero_feePercent_zero_feeProjectId() public {
61
61
  UniV4DeploymentSplitHook clone = UniV4DeploymentSplitHook(payable(LibClone.clone(address(hookImpl))));
62
62
 
63
- clone.initialize(address(this), 0, 0); // both zero is fine
63
+ clone.initialize(0, 0); // both zero is fine
64
64
 
65
65
  assertEq(clone.FEE_PERCENT(), 0);
66
66
  assertEq(clone.FEE_PROJECT_ID(), 0);
67
- assertEq(clone.owner(), address(this));
68
67
  }
69
68
 
70
69
  /// @notice initialize succeeds when feePercent > 0 and feeProjectId != 0 (valid fee config).
@@ -75,10 +74,9 @@ contract L25_FeeProjectIdValidationTest is Test {
75
74
 
76
75
  UniV4DeploymentSplitHook clone = UniV4DeploymentSplitHook(payable(LibClone.clone(address(hookImpl))));
77
76
 
78
- clone.initialize(address(this), 2, 3800); // feeProjectId=2, feePercent=38%
77
+ clone.initialize(2, 3800); // feeProjectId=2, feePercent=38%
79
78
 
80
79
  assertEq(clone.FEE_PERCENT(), 3800);
81
80
  assertEq(clone.FEE_PROJECT_ID(), 2);
82
- assertEq(clone.owner(), address(this));
83
81
  }
84
82
  }
@@ -21,10 +21,8 @@ import {
21
21
  MockJBPermissions
22
22
  } from "../mock/MockJBContracts.sol";
23
23
 
24
- /// @notice Regression test for M-32: Re-initialization after renounceOwnership.
25
- /// @dev After renounceOwnership() sets owner to address(0), the old owner() != address(0) check
26
- /// in initialize() would pass again, allowing an attacker to re-initialize with malicious
27
- /// fee parameters. The fix uses an explicit `initialized` boolean.
24
+ /// @notice Regression test for M-32: Re-initialization protection.
25
+ /// @dev The `initialized` boolean prevents calling initialize() more than once.
28
26
  contract M32_ReinitAfterRenounceTest is Test {
29
27
  UniV4DeploymentSplitHook public hookImpl;
30
28
  UniV4DeploymentSplitHook public hook;
@@ -34,11 +32,9 @@ contract M32_ReinitAfterRenounceTest is Test {
34
32
  MockJBPermissions public permissions;
35
33
  MockPositionManager public positionManager;
36
34
 
37
- address public owner;
38
35
  address public attacker;
39
36
 
40
37
  function setUp() public {
41
- owner = makeAddr("owner");
42
38
  attacker = makeAddr("attacker");
43
39
 
44
40
  directory = new MockJBDirectory();
@@ -61,38 +57,29 @@ contract M32_ReinitAfterRenounceTest is Test {
61
57
 
62
58
  // Clone and initialize
63
59
  hook = UniV4DeploymentSplitHook(payable(LibClone.clone(address(hookImpl))));
64
- hook.initialize(owner, 2, 3800); // feeProjectId=2, feePercent=38%
60
+ hook.initialize(2, 3800); // feeProjectId=2, feePercent=38%
65
61
  }
66
62
 
67
- /// @notice After renounceOwnership, re-initialization should still revert.
68
- function test_reinitialize_after_renounce_reverts() public {
69
- // Verify initial state
70
- assertEq(hook.owner(), owner);
63
+ /// @notice Re-initialization should revert.
64
+ function test_reinitialize_reverts() public {
71
65
  assertEq(hook.FEE_PROJECT_ID(), 2);
72
66
  assertEq(hook.FEE_PERCENT(), 3800);
73
67
  assertTrue(hook.initialized());
74
68
 
75
- // Owner renounces ownership
76
- vm.prank(owner);
77
- hook.renounceOwnership();
78
-
79
- // owner() is now address(0)
80
- assertEq(hook.owner(), address(0));
81
-
82
69
  // Attacker tries to re-initialize with malicious parameters
83
70
  vm.prank(attacker);
84
71
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_AlreadyInitialized.selector);
85
- hook.initialize(attacker, 2, 10_000); // trying to set 100% fee
72
+ hook.initialize(2, 10_000); // trying to set 100% fee
86
73
  }
87
74
 
88
75
  /// @notice The `initialized` flag is set to true after first initialization.
89
- function test_initialized_flag_set() public {
76
+ function test_initialized_flag_set() public view {
90
77
  assertTrue(hook.initialized(), "initialized should be true after initialize()");
91
78
  }
92
79
 
93
- /// @notice Double initialization (without renounce) also still reverts.
80
+ /// @notice Double initialization also still reverts.
94
81
  function test_double_init_reverts() public {
95
82
  vm.expectRevert(UniV4DeploymentSplitHook.UniV4DeploymentSplitHook_AlreadyInitialized.selector);
96
- hook.initialize(attacker, 2, 5000);
83
+ hook.initialize(2, 5000);
97
84
  }
98
85
  }