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

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.
@@ -1,21 +1,15 @@
1
1
  name: lint
2
2
  on:
3
3
  pull_request:
4
- branches:
5
- - main
4
+ branches: [main]
6
5
  push:
7
- branches:
8
- - main
6
+ branches: [main]
9
7
  jobs:
10
- forge-lint:
8
+ forge-fmt:
11
9
  runs-on: ubuntu-latest
12
10
  steps:
13
11
  - uses: actions/checkout@v4
14
- with:
15
- submodules: recursive
16
12
  - name: Install Foundry
17
13
  uses: foundry-rs/foundry-toolchain@v1
18
- - name: Install npm dependencies
19
- run: npm ci
20
- - name: Check linting
14
+ - name: Check formatting
21
15
  run: forge fmt --check
@@ -1,24 +1,23 @@
1
1
  name: slither
2
2
  on:
3
- pull_request:
4
- branches:
5
- - main
6
- push:
7
- branches:
8
- - main
3
+ pull_request:
4
+ branches: [main]
5
+ push:
6
+ branches: [main]
9
7
  jobs:
10
- analyze:
8
+ slither:
11
9
  runs-on: ubuntu-latest
12
10
  steps:
13
11
  - uses: actions/checkout@v4
14
12
  with:
15
13
  submodules: recursive
16
- - name: Install Foundry
17
- uses: foundry-rs/foundry-toolchain@v1
14
+ - uses: actions/setup-node@v4
15
+ with:
16
+ node-version: 22.4.x
18
17
  - name: Install npm dependencies
19
- run: npm ci
20
- - name: Run slither
18
+ run: npm install --omit=dev
19
+ - name: Run Slither
21
20
  uses: crytic/slither-action@v0.3.1
22
21
  with:
23
- slither-config: slither-ci.config.json
24
- fail-on: medium
22
+ slither-config: slither-ci.config.json
23
+ fail-on: medium
@@ -1,13 +1,11 @@
1
1
  name: test
2
2
  on:
3
- push:
3
+ pull_request:
4
4
  branches:
5
5
  - main
6
- pull_request:
6
+ push:
7
7
  branches:
8
8
  - main
9
- workflow_dispatch:
10
-
11
9
  jobs:
12
10
  forge-test:
13
11
  runs-on: ubuntu-latest
@@ -15,11 +13,14 @@ jobs:
15
13
  - uses: actions/checkout@v4
16
14
  with:
17
15
  submodules: recursive
16
+ - uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 22.4.x
19
+ - name: Install npm dependencies
20
+ run: npm install --omit=dev
18
21
  - name: Install Foundry
19
22
  uses: foundry-rs/foundry-toolchain@v1
20
- - name: Install npm dependencies
21
- run: npm ci
22
23
  - name: Run tests
23
- run: forge test --fail-fast
24
+ run: forge test --fail-fast --summary --detailed --skip "*/script/**"
24
25
  - name: Check contract sizes
25
- run: forge build --sizes --skip "*/test/**" --skip "*/script/**"
26
+ run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
@@ -0,0 +1,109 @@
1
+ # Administration
2
+
3
+ Admin privileges and their scope in univ4-lp-split-hook-v6.
4
+
5
+ ## Roles
6
+
7
+ ### 1. Project Owner
8
+
9
+ - **Assigned by:** Owning the JBProjects ERC-721 NFT for the given `projectId`.
10
+ - **Scope:** Per-project. Each project has its own owner, and ownership is verified through `IJBDirectory(DIRECTORY).PROJECTS().ownerOf(projectId)`.
11
+ - **Used for:** Gating `deployPool`, `rebalanceLiquidity`, and `claimFeeTokensFor`.
12
+
13
+ ### 2. Authorized Operator (SET_BUYBACK_POOL)
14
+
15
+ - **Assigned by:** Project owner granting `JBPermissionIds.SET_BUYBACK_POOL` (permission ID 23) via `JBPermissions.setPermission(operator, account, projectId, permissionId, true)`.
16
+ - **Scope:** Per-project, per-operator. The operator can act on behalf of the project owner for functions that require `SET_BUYBACK_POOL`.
17
+ - **Used for:** Same functions as the project owner -- `deployPool`, `rebalanceLiquidity`, and `claimFeeTokensFor`.
18
+
19
+ ### 3. Hook Deployer (Anyone)
20
+
21
+ - **Assigned by:** No assignment required. Anyone can call `UniV4DeploymentSplitHookDeployer.deployHookFor()`.
22
+ - **Scope:** Global. The caller becomes the initial context for the deployed clone (their address is included in the CREATE2 salt scoping).
23
+
24
+ ### 4. JB Controller (System Role)
25
+
26
+ - **Assigned by:** The Juicebox directory's `controllerOf(projectId)` mapping. Not directly assignable by the hook.
27
+ - **Scope:** Per-project. Only the controller registered for a given project can call `processSplitWith`.
28
+ - **Used for:** Sending reserved tokens to the hook during distribution.
29
+
30
+ ## Privileged Functions
31
+
32
+ ### UniV4DeploymentSplitHook
33
+
34
+ | Function | Required Role | Permission ID | Scope | What It Does |
35
+ |----------|--------------|---------------|-------|-------------|
36
+ | `deployPool(projectId, terminalToken, amount0Min, amount1Min, minCashOutReturn)` | Project owner or SET_BUYBACK_POOL operator. **Becomes permissionless** when the current ruleset weight has decayed to 1/10th or less of `initialWeightOf[projectId]`. | `JBPermissionIds.SET_BUYBACK_POOL` (23) | Per-project, per-terminal-token | Creates a Uniswap V4 pool at the geometric mean of issuance/cashout rates. Cashes out a computed fraction of accumulated project tokens for terminal tokens, mints a concentrated LP position, and transitions the project from accumulation to burn mode. (Lines 491-531) |
37
+ | `rebalanceLiquidity(projectId, terminalToken, ...)` | Project owner or SET_BUYBACK_POOL operator | `JBPermissionIds.SET_BUYBACK_POOL` (23) | Per-project, per-terminal-token | Burns the existing LP position NFT, collects and routes accrued fees, recalculates tick bounds from current issuance/cashout rates, and mints a new position with updated bounds. Reverts with `InsufficientLiquidity` if the new position would have zero liquidity. (Lines 559-658) |
38
+ | `claimFeeTokensFor(projectId, beneficiary)` | Project owner or SET_BUYBACK_POOL operator | `JBPermissionIds.SET_BUYBACK_POOL` (23) | Per-project | Transfers accumulated fee-project tokens to the specified beneficiary address. Validates the caller's permission, not the beneficiary's identity. Zeroes `claimableFeeTokens[projectId]` before transferring. (Lines 441-456) |
39
+ | `processSplitWith(context)` | JB Controller (system) | None (checked via `controllerOf`) | Per-project | Only callable by the project's registered controller. Accumulates project tokens (pre-deployment) or burns them (post-deployment). Validates `context.split.hook == address(this)`, `groupId == 1`, and controller identity. (Lines 534-555) |
40
+ | `initialize(feeProjectId, feePercent)` | Anyone (once only) | None | Per-clone instance | Sets `FEE_PROJECT_ID` and `FEE_PERCENT` on a clone. Can only be called once per clone (`initialized` flag). In practice, called immediately by the deployer factory. (Lines 177-194) |
41
+
42
+ ### UniV4DeploymentSplitHookDeployer
43
+
44
+ | Function | Required Role | Permission ID | Scope | What It Does |
45
+ |----------|--------------|---------------|-------|-------------|
46
+ | `deployHookFor(feeProjectId, feePercent, salt)` | Anyone | None | Global | Deploys a new hook clone via `LibClone`, calls `initialize()` on it, and registers it in the `JBAddressRegistry`. CREATE2 salt is scoped to `msg.sender`. (Lines 52-84) |
47
+
48
+ ### Permissionless Functions (No Privilege Required)
49
+
50
+ | Function | Scope | What It Does |
51
+ |----------|-------|-------------|
52
+ | `collectAndRouteLPFees(projectId, terminalToken)` | Per-project, per-terminal-token | Collects accrued V4 position fees and routes them: `FEE_PERCENT` of terminal token fees to the fee project via `terminal.pay()`, the remainder to the original project via `addToBalanceOf()`. Project token fees are burned. Safe because funds always go to verified project terminals. (Lines 459-488) |
53
+ | `isPoolDeployed(projectId, terminalToken)` | View | Returns whether `tokenIdOf[projectId][terminalToken] != 0`. |
54
+ | `poolKeyOf(projectId, terminalToken)` | View | Returns the stored `PoolKey` for a deployed pool. |
55
+ | `supportsInterface(interfaceId)` | View | Returns `true` for `IUniV4DeploymentSplitHook` and `IJBSplitHook`. |
56
+ | `receive()` | Accepts ETH | Required for cash-out with native ETH and V4 TAKE operations. |
57
+
58
+ ## Immutable Configuration
59
+
60
+ These values are set at deploy time and cannot be changed afterward.
61
+
62
+ ### Implementation-Level (Constructor, Shared Across All Clones)
63
+
64
+ | Parameter | Set In | Value Source |
65
+ |-----------|--------|-------------|
66
+ | `DIRECTORY` | Constructor (line 168) | JBDirectory address |
67
+ | `TOKENS` | Constructor (line 169) | JBTokens address |
68
+ | `POOL_MANAGER` | Constructor (line 170) | Uniswap V4 PoolManager address |
69
+ | `POSITION_MANAGER` | Constructor (line 171) | Uniswap V4 PositionManager address |
70
+ | `PERMISSIONS` | Inherited from `JBPermissioned` constructor (line 161) | JBPermissions address |
71
+
72
+ ### Clone-Level (initialize(), Per-Instance)
73
+
74
+ | Parameter | Set In | Value Source |
75
+ |-----------|--------|-------------|
76
+ | `FEE_PROJECT_ID` | `initialize()` (line 192) | Project ID receiving LP fee share |
77
+ | `FEE_PERCENT` | `initialize()` (line 193) | Basis points (0-10000) of LP fees routed to fee project |
78
+
79
+ ### Protocol Constants (Hardcoded)
80
+
81
+ | Constant | Value | Purpose |
82
+ |----------|-------|---------|
83
+ | `BPS` | 10,000 | 100% in basis points |
84
+ | `POOL_FEE` | 10,000 | 1% Uniswap V4 fee tier |
85
+ | `TICK_SPACING` | 200 | Tick spacing for the 1% fee tier |
86
+
87
+ ## Admin Boundaries
88
+
89
+ What admins **cannot** do:
90
+
91
+ 1. **Cannot change fee configuration after initialization.** `FEE_PROJECT_ID` and `FEE_PERCENT` are write-once via `initialize()`. The `initialized` flag prevents re-initialization, even by the original deployer.
92
+
93
+ 2. **Cannot withdraw accumulated tokens directly.** There is no `withdraw()` or `rescue()` function. Accumulated project tokens can only exit via `deployPool()` (into LP) or post-deployment burning.
94
+
95
+ 3. **Cannot redirect LP fees to arbitrary addresses.** Fee routing is hardcoded to go through `primaryTerminalOf` for both the fee project and the original project. There is no admin-settable destination.
96
+
97
+ 4. **Cannot modify pool parameters after deployment.** The `PoolKey` (fee tier, tick spacing, hook address, currency pair) is set during `_createAndInitializePool()` and stored immutably in `_poolKeys`.
98
+
99
+ 5. **Cannot deploy a second pool for the same project/terminal-token pair.** `deployPool()` reverts with `PoolAlreadyDeployed` if `tokenIdOf[projectId][terminalToken] != 0`.
100
+
101
+ 6. **Cannot prevent permissionless fee collection.** `collectAndRouteLPFees()` has no access control. Anyone can trigger fee collection and routing for any deployed pool.
102
+
103
+ 7. **Cannot prevent permissionless pool deployment after 10x weight decay.** Once the current ruleset weight drops to 1/10th of `initialWeightOf[projectId]`, the `SET_BUYBACK_POOL` permission check is bypassed and anyone can deploy.
104
+
105
+ 8. **Cannot change the Uniswap V4 infrastructure contracts.** `POOL_MANAGER`, `POSITION_MANAGER`, `DIRECTORY`, `TOKENS`, and `PERMISSIONS` are immutable, set in the implementation constructor, and shared across all clones.
106
+
107
+ 9. **Cannot control which project tokens are sent via `processSplitWith`.** The controller decides when and how much to distribute. The hook only receives what the JB protocol sends it.
108
+
109
+ 10. **Cannot recover funds sent to the wrong clone.** If tokens are sent directly (not through `processSplitWith`), there is no mechanism to retrieve them. Only project tokens sent via the controller accumulate correctly.
@@ -0,0 +1,58 @@
1
+ # univ4-lp-split-hook-v6 — Architecture
2
+
3
+ ## Purpose
4
+
5
+ Uniswap V4 liquidity pool deployment hook for Juicebox V6. Receives project tokens via reserved token splits, accumulates them until a deployment threshold is met, then creates a Uniswap V4 pool and provides initial liquidity. After deployment, burns new tokens and routes LP fees back to the project.
6
+
7
+ ## Contract Map
8
+
9
+ ```
10
+ src/
11
+ ├── UniV4DeploymentSplitHook.sol — Split hook: token accumulation, pool deployment, LP management
12
+ ├── UniV4DeploymentSplitHookDeployer.sol — Factory for deploying split hooks
13
+ └── interfaces/
14
+ └── IUniV4DeploymentSplitHook.sol
15
+ ```
16
+
17
+ ## Key Data Flows
18
+
19
+ ### Pre-Deployment (Accumulation)
20
+ ```
21
+ Reserved token distribution → JBMultiTerminal.sendPayoutsOf()
22
+ → Split to UniV4DeploymentSplitHook.processSplitWith()
23
+ → Accumulate project tokens
24
+ → Track accumulated balance
25
+
26
+ Owner/Operator → deployPool()
27
+ → Validate: enough tokens accumulated
28
+ → Create Uniswap V4 pool (projectToken/terminalToken)
29
+ → Provide initial liquidity from accumulated tokens
30
+ → Set up LP position
31
+ ```
32
+
33
+ ### Post-Deployment (Maintenance)
34
+ ```
35
+ Reserved token distribution → processSplitWith()
36
+ → Burn newly received project tokens (no more accumulation)
37
+
38
+ Anyone → rebalanceLiquidity()
39
+ → Adjust LP position based on current rates
40
+ → Route LP fees back to project
41
+ ```
42
+
43
+ ## Extension Points
44
+
45
+ | Point | Interface | Purpose |
46
+ |-------|-----------|---------|
47
+ | Split hook | `IJBSplitHook` | Receives tokens from reserved distribution |
48
+ | Pool deployment | Uniswap V4 PositionManager | Creates and manages LP positions |
49
+
50
+ ## Dependencies
51
+ - `@bananapus/core-v6` — Core protocol
52
+ - `@bananapus/permission-ids-v6` — SET_BUYBACK_POOL permission
53
+ - `@bananapus/ownable-v6` — JB ownership
54
+ - `@openzeppelin/contracts` — SafeERC20, Ownable
55
+ - `@prb/math` — mulDiv, sqrt
56
+ - `@uniswap/v4-core` — Pool manager, currency types
57
+ - `@uniswap/v4-periphery` — Position manager, actions
58
+ - `@rev-net/core-v6` — Revnet integration
package/README.md CHANGED
@@ -26,7 +26,7 @@ This ensures the AMM price always trades within the project's intrinsic economic
26
26
  2. Controller distributes reserved tokens → processSplitWith()
27
27
  → Tokens accumulate in accumulatedProjectTokens[projectId]
28
28
  |
29
- 3. Project owner calls deployPool(projectId, terminalToken, ...)
29
+ 3. Project owner (or anyone after 10x weight decay) calls deployPool(...)
30
30
  → Creates V4 pool at geometric mean of [cashOut, issuance] rates
31
31
  → Cashes out optimal fraction of tokens for terminal tokens
32
32
  → Mints concentrated LP position bounded by rate-derived ticks
@@ -96,7 +96,7 @@ forge install
96
96
  # foundry.toml
97
97
  [profile.default]
98
98
  solc = '0.8.26'
99
- evm_version = 'paris'
99
+ evm_version = 'cancun'
100
100
  optimizer_runs = 200
101
101
  via_ir = true
102
102
 
@@ -122,7 +122,9 @@ test/
122
122
  NativeETHTest.t.sol # Native ETH handling
123
123
  PriceMathTest.t.sol # Price conversion math
124
124
  SecurityTest.t.sol # Permission checks, access control
125
+ WeightDecayDeployTest.t.sol # Permissionless deploy after 10x weight decay
125
126
  IntegrationLifecycle.t.sol # Full end-to-end workflow
127
+ Fork.t.sol # Fork tests with real V4 + JB core
126
128
  TestBaseV4.sol # Shared test infrastructure
127
129
  regression/ # Audit finding regression tests (M-31, M-32, L-25)
128
130
  script/
@@ -133,11 +135,13 @@ script/
133
135
 
134
136
  | Permission | Required For |
135
137
  |------------|-------------|
136
- | `SET_BUYBACK_POOL` | `deployPool` -- create V4 pool and mint LP position |
138
+ | `SET_BUYBACK_POOL` | `deployPool` -- create V4 pool and mint LP position (unless weight has decayed 10x) |
137
139
  | `SET_BUYBACK_POOL` | `claimFeeTokensFor` -- claim accumulated fee-project tokens |
138
140
 
139
141
  Note: `collectAndRouteLPFees` and `rebalanceLiquidity` are **permissionless** -- anyone can call them. This is safe because they only operate on existing positions and route funds to verified project terminals.
140
142
 
143
+ `deployPool` also becomes **permissionless** when the current ruleset's weight has decayed to 1/10th or less of the weight when the hook first started accumulating tokens. This prevents a stale owner from blocking LP deployment indefinitely.
144
+
141
145
  ## Risks
142
146
 
143
147
  - **Impermanent loss:** The LP position is subject to standard concentrated liquidity IL. If the market price moves outside the [cashOut, issuance] range, the position becomes single-sided.
package/RISKS.md ADDED
@@ -0,0 +1,235 @@
1
+ # univ4-lp-split-hook-v6 -- Risks
2
+
3
+ Deep implementation-level risk analysis. Line references are to `src/UniV4DeploymentSplitHook.sol` unless otherwise noted.
4
+
5
+ ## Trust Assumptions
6
+
7
+ 1. **Project Owner** -- Can trigger pool deployment (`deployPool`, line 491) and manage LP positions (`rebalanceLiquidity`, line 559). Has `SET_BUYBACK_POOL` permission and can delegate it. Cannot modify fee configuration post-initialization.
8
+ 2. **Uniswap V4 Pool Manager + Position Manager** -- LP positions are managed through V4 PositionManager (immutable at line 107). Pool manager bugs or governance changes could affect all positions managed by this hook. The hook has no way to migrate positions to a different V4 deployment.
9
+ 3. **JB Core Protocol** -- The hook trusts that `controllerOf(projectId)` returns the legitimate controller (line 537-539), that `processSplitWith` is called with accurate `context.amount` (line 534), and that `primaryTerminalOf` resolves correctly (line 236, 346-347, 521-522). Compromise of the JB directory would break all assumptions.
10
+ 4. **Price Oracle (Implicit)** -- Initial pool price is derived from the project's bonding curve rates via `currentReclaimableSurplusOf` (line 238) and `currentRulesetOf` weight (line 290, 344, 401). These are on-chain reads, not external oracle feeds. Manipulation requires changing the project's actual surplus or ruleset weight.
11
+ 5. **Fee Project** -- The `FEE_PROJECT_ID` must have a functioning terminal that accepts the terminal token. If the fee project's terminal disappears or reverts, fee routing silently fails (the terminal check at line 1105 returns early, and the fee amount is retained in the contract).
12
+
13
+ ## Audit History
14
+
15
+ A Nemesis audit (`.audit/findings/nemesis-verified.md`) identified 6 true positives. All findings have been addressed in the current codebase:
16
+
17
+ | Finding | Severity | Status | Fix Applied |
18
+ |---------|----------|--------|-------------|
19
+ | NM-001: Permissionless rebalance bricks LP | HIGH | **Fixed** | Added `SET_BUYBACK_POOL` permission check to `rebalanceLiquidity` (line 569-573). Added `InsufficientLiquidity` revert guard when new position would have zero liquidity (line 646-652). |
20
+ | NM-002: Placeholder disables fee routing in rebalance | MEDIUM | **Fixed** | Replaced `_getAmountForCurrency()` with balance-delta tracking in `rebalanceLiquidity` (lines 587-602), matching the pattern used in `collectAndRouteLPFees`. |
21
+ | NM-003: Per-project flag prevents multi-terminal-token pools | MEDIUM | **Fixed** | Changed `projectDeployed` to `mapping(uint256 => mapping(address => bool))` (line 129). Added `deployedPoolCount` (line 134) for the accumulate-vs-burn decision in `processSplitWith` (line 545). |
22
+ | NM-004: Implementation contract initializable by anyone | LOW | **Acknowledged** | The implementation is not intended for direct use. Clones have separate storage. Low practical impact. |
23
+ | NM-005: Dead variables in rebalanceLiquidity | LOW | **Fixed** | Dead variables removed; balance-delta tracking replaces them. |
24
+ | NM-006: _poolKeys not cleared when tokenIdOf zeroed | LOW | **Mitigated** | `rebalanceLiquidity` now reverts instead of zeroing `tokenIdOf` (line 652), making the stale-data path unreachable. |
25
+
26
+ ## Known Risks
27
+
28
+ ### Severity: HIGH
29
+
30
+ #### H-1. Rebalance Sandwich Attack (MEV)
31
+
32
+ - **Severity:** HIGH
33
+ - **Tested:** Partially. `RebalanceTest.t.sol` tests the function mechanics and authorization but does not simulate MEV sandwich attacks.
34
+ - **Lines:** 559-658 (rebalanceLiquidity), specifically 607-613 (BURN_POSITION + TAKE_PAIR)
35
+ - **Description:** `rebalanceLiquidity` burns the entire LP position and mints a new one in a single transaction. Between the BURN_POSITION (which removes liquidity at the current pool price) and MINT_POSITION (which adds liquidity at new tick bounds), the hook holds both token types as raw balances. A sophisticated MEV bot can sandwich this transaction:
36
+ 1. Front-run: Swap in the V4 pool to move the price to one extreme of the tick range, making the BURN return skewed token amounts.
37
+ 2. The rebalance executes, minting a new position at the manipulated price point.
38
+ 3. Back-run: Swap back, extracting value from the new position's skewed liquidity.
39
+ - **Slippage parameters** (`decreaseAmount0Min`, `decreaseAmount1Min`, `increaseAmount0Min`, `increaseAmount1Min`) provide some protection but require the caller to set them correctly. The default of `0` offers no protection.
40
+ - **Mitigation:** Callers should use private mempools (e.g., Flashbots Protect) and set non-zero slippage parameters. The 1% fee tier (POOL_FEE = 10,000) increases the cost of sandwich attacks.
41
+
42
+ #### H-2. collectAndRouteLPFees Sandwich Attack (MEV)
43
+
44
+ - **Severity:** HIGH
45
+ - **Tested:** `FeeRoutingTest.t.sol` tests fee arithmetic and routing but not MEV vectors.
46
+ - **Lines:** 459-488 (collectAndRouteLPFees), specifically 471-477 (DECREASE_LIQUIDITY + TAKE_PAIR)
47
+ - **Description:** `collectAndRouteLPFees` is permissionless (no access control). A MEV bot can:
48
+ 1. Observe a pending `collectAndRouteLPFees` transaction.
49
+ 2. Front-run: Swap in the pool to manipulate the price, affecting the value of collected fees.
50
+ 3. The fee collection executes, routing fees at the manipulated value.
51
+ 4. Back-run: Swap back.
52
+ - **Impact is lower than H-1** because fee collection only touches accrued fees (not the full position principal), but large accumulated fees create meaningful MEV opportunities.
53
+ - **Mitigation:** The 1% pool fee makes sandwich attacks costly relative to extracted value. Frequent fee collection reduces the size of any single extraction.
54
+
55
+ ### Severity: MEDIUM
56
+
57
+ #### M-1. Impermanent Loss on Concentrated Liquidity
58
+
59
+ - **Severity:** MEDIUM (inherent to AMM design)
60
+ - **Tested:** Not directly tested. The tick bounds are tested in `PriceMathTest.t.sol` (test_TickBounds_Normal, test_TickBounds_AlignedToSpacing).
61
+ - **Lines:** 798-820 (_calculateTickBounds), 862-922 (_computeOptimalCashOutAmount)
62
+ - **Description:** The LP position is concentrated between the cashout rate (price floor, line 807) and issuance rate (price ceiling, line 808). If the market price moves outside this range, the position becomes 100% single-sided and stops earning fees. Concentrated liquidity amplifies impermanent loss compared to full-range V3/V2 positions. With a 1% fee tier and 200-tick spacing, the position range is relatively wide, limiting but not eliminating this risk.
63
+ - **Mitigation:** `rebalanceLiquidity` allows repositioning to track changing rates. The tick range is derived from the project's actual issuance and cashout parameters, so it tracks fundamental value.
64
+
65
+ #### M-2. Initial Pool Price Manipulation
66
+
67
+ - **Severity:** MEDIUM
68
+ - **Tested:** `PriceMathTest.t.sol` tests the geometric mean calculation (test_GeometricMean_BetweenBounds, test_GeometricMean_FallbackOnZeroCashOut). `WeightDecayDeployTest.t.sol` tests the weight-zero edge case.
69
+ - **Lines:** 822-859 (_computeInitialSqrtPrice), 702-706 (cash-out amount computation)
70
+ - **Description:** The initial pool price is the geometric mean of the cashout and issuance rates (line 851). These rates are derived from on-chain state (`currentReclaimableSurplusOf` at line 238, `currentRulesetOf` at line 290). If an attacker can manipulate the project's surplus (by paying in then cashing out) or trigger a ruleset change just before `deployPool`, the initial price will be skewed.
71
+ - **Attack scenario:**
72
+ 1. Attacker pays a large amount into the project, inflating surplus.
73
+ 2. Owner calls `deployPool`, which computes the initial price from the inflated surplus.
74
+ 3. Pool is created at an artificially high cashout rate.
75
+ 4. Attacker cashes out, reducing surplus back to normal.
76
+ 5. The pool's initial price is now misaligned, creating arbitrage profit for the attacker.
77
+ - **Mitigation:** The bonding curve math in JB core limits the degree of price manipulation. The `minCashOutReturn` parameter (line 496) provides slippage protection on the cash-out portion. The 1% auto-tolerance (line 716-721) provides a default safety margin.
78
+
79
+ #### M-3. Token Accumulation Period: No Yield, Counterparty Risk
80
+
81
+ - **Severity:** MEDIUM
82
+ - **Tested:** `AccumulationStageTest.t.sol` covers accumulation mechanics. `DeploymentStageTest.t.sol` tests the transition.
83
+ - **Lines:** 665-667 (_accumulateTokens), 545-551 (processSplitWith accumulation branch)
84
+ - **Description:** Between the first `processSplitWith` call and the eventual `deployPool`, project tokens sit in the contract earning no yield. During this period:
85
+ - The contract holds raw ERC-20 tokens with no protective mechanism.
86
+ - Token value may decrease as the project's issuance rate decays (the weight cut mechanism).
87
+ - If the project owner never calls `deployPool`, tokens are stranded until weight decays 10x (line 506) and becomes permissionless.
88
+ - **Mitigation:** The 10x weight decay permissionless deployment (line 500-512, tested in `WeightDecayDeployTest.t.sol`) ensures pools can eventually be deployed even without owner cooperation. The `initialWeightOf` tracking (line 547-549) records the weight at first accumulation.
89
+
90
+ #### M-4. Rebalance Reverts: Temporary Position Gap
91
+
92
+ - **Severity:** MEDIUM
93
+ - **Tested:** `M31_StaleTokenIdOf.t.sol` tests the `InsufficientLiquidity` revert. `UniV4DeploymentSplitHook_AuditFindings.t.sol` (test_H2_rebalance_zeroLiquidity_reverts) confirms the guard.
94
+ - **Lines:** 605-614 (BURN_POSITION in rebalanceLiquidity), 638-653 (liquidity check and revert)
95
+ - **Description:** `rebalanceLiquidity` burns the old position (line 607-613) before minting the new one (line 641-643). If the MINT_POSITION step fails (e.g., due to price moving outside tick bounds causing zero liquidity), the transaction reverts with `InsufficientLiquidity` (line 652), rolling back the burn. This is the correct behavior (prevents bricking), but it means the rebalance cannot succeed until conditions change. During the revert, no state changes occur, and the old position remains intact.
96
+ - **Edge case:** If the V4 PositionManager itself has a bug or is paused, neither burn nor mint would succeed, effectively freezing the position in place.
97
+
98
+ #### M-5. Fee Project Terminal Disappearance
99
+
100
+ - **Severity:** MEDIUM
101
+ - **Tested:** `L25_FeeProjectIdValidation.t.sol` tests the `initialize` validation. Fee routing is tested in `FeeRoutingTest.t.sol`.
102
+ - **Lines:** 1103-1137 (_routeFeesToProject), specifically 1103-1105 (fee terminal lookup)
103
+ - **Description:** If the fee project's primary terminal for the terminal token is removed or changed to `address(0)`, the fee routing silently skips the fee payment (line 1105: `if (feeTerminal != address(0))`). The `feeAmount` is computed (line 1098) but never transferred. The terminal token fee amount is retained in the contract and eventually gets absorbed into the next liquidity operation.
104
+ - **Mitigation:** The `initialize` validation (line 184-188) checks that the fee project has a controller at initialization time. However, the terminal could be removed later. This is a graceful degradation -- the project's share (`remainingAmount`) is still routed correctly.
105
+
106
+ ### Severity: LOW
107
+
108
+ #### L-1. Implementation Contract Initializable
109
+
110
+ - **Tested:** `M32_ReinitAfterRenounce.t.sol` tests clone re-initialization prevention.
111
+ - **Lines:** 177-194 (initialize)
112
+ - **Description:** The implementation contract deployed by the factory never calls `initialize` in its constructor. Anyone can call `initialize()` on the implementation with arbitrary parameters. This has no practical impact because clones have separate storage, and the implementation is not used directly.
113
+
114
+ #### L-2. processSplitWith Burns for All Terminal Tokens After First Pool
115
+
116
+ - **Tested:** `UniV4DeploymentSplitHook_AuditFindings.t.sol` (test_M2_processSplitWith_burnsAfterDeploy, test_M2_multiTerminalToken_independentFlags).
117
+ - **Lines:** 545 (deployedPoolCount check), 134 (deployedPoolCount mapping)
118
+ - **Description:** `processSplitWith` uses `deployedPoolCount[projectId]` (per-project, not per-terminal-token) to decide whether to accumulate or burn (line 545). Once any pool is deployed for a project, all subsequent reserved token splits burn tokens. This prevents accumulation for a second terminal token's pool. The `JBSplitHookContext` does not include the terminal token, so per-token accumulation is not possible with the current interface.
119
+ - **Mitigation:** This is a known architectural constraint. To deploy pools for multiple terminal tokens, the project must deploy separate hook clones.
120
+
121
+ #### L-3. Irreversible Pool Deployment
122
+
123
+ - **Tested:** `DeploymentStageTest.t.sol` (test_DeployPool_RevertsIf_PoolAlreadyDeployed).
124
+ - **Lines:** 514 (PoolAlreadyDeployed check), 527 (projectDeployed set to true)
125
+ - **Description:** Once `deployPool` succeeds, there is no way to undeploy, reconfigure, or redeploy the pool for the same project/terminal-token pair. The `projectDeployed` flag is a one-way latch. If the pool is deployed at a suboptimal price or with wrong parameters, the only remedy is `rebalanceLiquidity` (which adjusts tick bounds but cannot change the pool's fee tier, hook address, or currency pair).
126
+
127
+ #### L-4. Rounding in Fee Split Arithmetic
128
+
129
+ - **Tested:** `FeeRoutingTest.t.sol` (test_RouteFees_SplitsBetweenFeeAndOriginal) tests the split with 1000e18.
130
+ - **Lines:** 1098 (fee calculation: `feeAmount = (amount * FEE_PERCENT) / BPS`)
131
+ - **Description:** Integer division truncates in favor of the original project (the fee project receives slightly less). For a 38% fee with an amount of `N`, the fee project receives `floor(N * 3800 / 10000)` and the project receives `N - floor(N * 3800 / 10000)`. The rounding error is at most 1 wei per fee routing operation. This matches the Juicebox core convention.
132
+
133
+ #### L-5. No Reentrancy Guard
134
+
135
+ - **Tested:** `SecurityTest.t.sol` (test_ClaimFeeTokens_ClearsBeforeTransfer) verifies the checks-effects-interactions pattern for `claimFeeTokensFor`.
136
+ - **Lines:** 448-449 (claimFeeTokensFor: zeroes balance before transfer), 724-733 (deployPool: external call to terminal.cashOutTokensOf)
137
+ - **Description:** The contract has no explicit `ReentrancyGuard`. It relies on state ordering: `claimFeeTokensFor` zeroes `claimableFeeTokens` before the `safeTransfer` (line 449 before 453). `deployPool` and `rebalanceLiquidity` make multiple external calls (to `PositionManager`, `terminal.cashOutTokensOf`, `terminal.pay`, `addToBalanceOf`). The state is generally updated before external calls, but the call chains are complex. Reentrancy through V4 PositionManager callbacks is theoretically possible but requires a malicious PoolManager or hook contract (both of which are immutable and trusted).
138
+ - **Mitigation:** The trust model assumes V4 infrastructure is not malicious. JB terminal calls use try-catch internally. The `processSplitWith` function validates `msg.sender == controllerOf(projectId)` (line 539), preventing reentrancy through the split hook interface.
139
+
140
+ #### L-6. Permissionless Fee Collection Timing
141
+
142
+ - **Tested:** `SecurityTest.t.sol` (test_CollectFees_Permissionless).
143
+ - **Lines:** 459-488 (collectAndRouteLPFees, no access control)
144
+ - **Description:** Anyone can trigger `collectAndRouteLPFees` at any time. While this is a feature (enabling keepers), it means an adversary can time fee collection to their advantage. For example, collecting fees just before a large swap (when the pool price is at one extreme) versus after. The practical impact is minimal because fee collection does not change the pool price -- it only harvests accrued swap fees.
145
+
146
+ ## Concrete Attack Scenarios
147
+
148
+ ### Scenario 1: Rebalance Sandwich (H-1)
149
+
150
+ **Attacker:** MEV bot monitoring the mempool.
151
+ **Cost:** Gas + swap fees (1% per swap in the V4 pool).
152
+ **Profit potential:** Proportional to position size and price impact achievable.
153
+
154
+ ```
155
+ 1. Observe pending rebalanceLiquidity(projectId, ETH, 0, 0, 0, 0) in mempool
156
+ 2. Front-run: Swap large amount of project tokens into the pool, pushing price down
157
+ 3. rebalanceLiquidity executes:
158
+ - BURN_POSITION returns skewed amounts (mostly project tokens)
159
+ - New tick bounds computed from current Juicebox rates (not the pool price)
160
+ - MINT_POSITION creates position at new bounds with skewed amounts
161
+ 4. Back-run: Swap back, buying cheap project tokens from the new position
162
+ ```
163
+
164
+ **Defense:** Set non-zero slippage parameters. Use Flashbots or private mempool. The 1% fee tier makes each swap leg expensive.
165
+
166
+ ### Scenario 2: Stale Owner Blocks Pool Deployment (M-3)
167
+
168
+ **Attacker:** Project owner who loses keys or becomes unresponsive.
169
+ **Timeline:** Until weight decays 10x from `initialWeightOf`.
170
+
171
+ ```
172
+ 1. Project accumulates tokens via reserved splits
173
+ 2. Owner never calls deployPool
174
+ 3. Tokens sit idle, losing value as issuance rate decays
175
+ 4. Eventually (after enough ruleset cycles with weight cut), weight drops below 1/10th
176
+ 5. Anyone can call deployPool permissionlessly
177
+ ```
178
+
179
+ **Defense:** Built into the protocol. The 10x decay threshold (line 506) ensures eventual permissionless deployment. For a project with 80% weight cut per cycle and 1-day duration, this takes approximately 3 days (confirmed in `Fork.t.sol` test_fork_deployPool_permissionlessAfterWeightDecay).
180
+
181
+ ### Scenario 3: Initial Price Front-Running (M-2)
182
+
183
+ **Attacker:** Anyone who can pay into the project.
184
+ **Cost:** JB protocol fees (2.5%) on the pay-in amount.
185
+
186
+ ```
187
+ 1. Observe pending deployPool transaction
188
+ 2. Front-run: Pay large amount into the project to inflate surplus
189
+ 3. deployPool executes with inflated cashout rate, creating pool at wrong price
190
+ 4. Back-run: Cash out to reclaim most of the paid amount
191
+ 5. Arbitrage the mispriced pool
192
+ ```
193
+
194
+ **Defense:** The 2.5% JB protocol fee on payments makes this expensive. The `minCashOutReturn` parameter limits how much value can be extracted during the pool's initial cash-out. Concentrated liquidity's narrow range limits the total arbitrageable amount.
195
+
196
+ ## Test Coverage Analysis
197
+
198
+ ### Well-Tested Areas
199
+
200
+ | Area | Test Files | Coverage |
201
+ |------|-----------|----------|
202
+ | Access control (processSplitWith) | SecurityTest, AccumulationStageTest | Comprehensive: controller-only, wrong hook, wrong groupId, invalid project |
203
+ | Access control (deployPool) | DeploymentStageTest, WeightDecayDeployTest, SecurityTest | Comprehensive: unauthorized, owner, operator, weight-decay permissionless, edge cases |
204
+ | Access control (rebalanceLiquidity) | AuditFindingsTest, RebalanceTest | Comprehensive: unauthorized reverts, owner succeeds, operator succeeds, zero-liquidity revert |
205
+ | Access control (claimFeeTokensFor) | SecurityTest, FeeRoutingTest | Comprehensive: valid operator, invalid operator, zero-balance no-op |
206
+ | Fee routing arithmetic | FeeRoutingTest | Thorough: 38/62 split verified, zero-fee no-op, claimable tracking, event emission |
207
+ | Accumulation mechanics | AccumulationStageTest | Thorough: single, multiple, zero-amount, cross-project isolation |
208
+ | Pool deployment lifecycle | DeploymentStageTest, IntegrationLifecycle | Thorough: creates pool, sets tokenId, clears accumulated, handles leftovers |
209
+ | Price math | PriceMathTest | Thorough: issuance rate (0/10/100% reserved), cashout rate (0/positive surplus), sqrtPriceX96, tick bounds, alignment, geometric mean, optimal cashout |
210
+ | Native ETH handling | NativeETHTest | Good: isNativeToken, receive(), accounting setup, end-to-end deploy with NATIVE_TOKEN |
211
+ | Clone deployment | DeployerTest | Good: CREATE, CREATE2, address registry, initialization, events |
212
+ | Re-initialization prevention | M32_ReinitAfterRenounce, ConstructorTest | Good: double-init reverts, initialized flag |
213
+ | Fork integration | Fork.t.sol | Good: real V4 contracts, full lifecycle with real JB core, weight-decay permissionless deploy |
214
+ | Token conservation | PositionManagerIntegrationTest | Good: token flows, no creation from thin air, partial usage with sweep |
215
+
216
+ ### Untested or Lightly Tested Areas
217
+
218
+ | Area | Gap | Risk Level |
219
+ |------|-----|------------|
220
+ | MEV/sandwich attacks | No simulation of front-running or back-running around rebalance/collect operations | HIGH |
221
+ | Extreme price scenarios | No tests for MIN_TICK/MAX_TICK boundaries in production conditions | LOW |
222
+ | Fee-on-transfer tokens | No tests with non-standard ERC-20 tokens (not applicable -- JB project tokens are standard) | N/A |
223
+ | Concurrent multi-project rebalance | No tests for multiple projects rebalancing in the same block | LOW |
224
+ | V4 PositionManager edge cases | Mock-based tests do not cover real PositionManager revert conditions (e.g., insufficient pool liquidity for burn) | MEDIUM |
225
+ | Gas limits | No tests for gas consumption with large accumulated balances or many fee collection cycles | LOW |
226
+
227
+ ## Privileged Roles
228
+
229
+ | Role | Permission | Scope |
230
+ |------|-----------|-------|
231
+ | Project owner | `SET_BUYBACK_POOL` -- deploy pool, rebalance, claim fees | Per-project |
232
+ | Authorized operator | `SET_BUYBACK_POOL` -- same as owner when granted | Per-project, delegated |
233
+ | Anyone (post-10x-decay) | `deployPool` -- bypasses permission check | Per-project, conditional |
234
+ | Anyone (post-deployment) | `collectAndRouteLPFees` -- trigger fee collection | Permissionless |
235
+ | JB Controller | `processSplitWith` -- send tokens to hook | System role, per-project |
package/SKILLS.md CHANGED
@@ -23,7 +23,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
23
23
 
24
24
  | Function | What it does |
25
25
  |----------|-------------|
26
- | `deployPool(projectId, terminalToken, amount0Min, amount1Min, minCashOutReturn)` | Requires `SET_BUYBACK_POOL` permission. Creates V4 pool at geometric mean of [cashOut, issuance] rates. Computes optimal cash-out fraction, cashes out tokens via terminal, mints concentrated LP position, handles leftovers (burns project tokens, adds terminal tokens to project balance). Sets `projectDeployed = true`. |
26
+ | `deployPool(projectId, terminalToken, amount0Min, amount1Min, minCashOutReturn)` | Requires `SET_BUYBACK_POOL` permission unless the current ruleset's weight has decayed to 1/10th or less of `initialWeightOf[projectId]` (becomes permissionless). Creates V4 pool at geometric mean of [cashOut, issuance] rates. Computes optimal cash-out fraction, cashes out tokens via terminal, mints concentrated LP position, handles leftovers (burns project tokens, adds terminal tokens to project balance). Sets `projectDeployed = true`. |
27
27
 
28
28
  ### Fee Management
29
29
 
@@ -125,6 +125,7 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
125
125
  | `_poolKeys` | `projectId => terminalToken => PoolKey` | V4 pool key per project/token pair |
126
126
  | `tokenIdOf` | `projectId => terminalToken => uint256` | V4 PositionManager NFT ID per pool |
127
127
  | `accumulatedProjectTokens` | `projectId => uint256` | Pre-deployment token accumulation |
128
+ | `initialWeightOf` | `projectId => uint256` | Ruleset weight when first tokens were accumulated (for 10x decay check) |
128
129
  | `projectDeployed` | `projectId => bool` | Switches accumulate (Stage 1) to burn (Stage 2) |
129
130
  | `claimableFeeTokens` | `projectId => uint256` | Fee-project tokens claimable via `claimFeeTokensFor` |
130
131
  | `initialized` | `bool` | Prevents re-initialization of clone instances |
@@ -134,14 +135,14 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
134
135
  1. **This is a V4 hook, not V3.** Despite the repo history, the current implementation uses Uniswap V4 (`IPoolManager`, `IPositionManager`, `PoolKey`, `Actions`). All V3 references in older docs are outdated.
135
136
  2. **Requires `via_ir = true` in foundry.toml.** Stack-too-deep errors occur without the IR pipeline, particularly in `_addUniswapLiquidity` and V4 `PositionManager` interactions.
136
137
  3. **Only accepts reserved-token splits (`groupId == 1`).** Reverts with `TerminalTokensNotAllowed` if called from a payout split (`groupId == 0`). This is intentional -- it only manages project tokens.
137
- 4. **`deployPool` requires `SET_BUYBACK_POOL` permission.** The caller must be the project owner or have been granted this permission via `JBPermissions`.
138
+ 4. **`deployPool` requires `SET_BUYBACK_POOL` permission, unless weight has decayed 10x.** The caller must be the project owner or have been granted this permission via `JBPermissions`. However, if the current ruleset's weight has decayed to 1/10th or less of the weight when the hook first started accumulating tokens (`initialWeightOf`), anyone can call `deployPool`. This prevents a stale owner from blocking LP deployment indefinitely.
138
139
  5. **`collectAndRouteLPFees` and `rebalanceLiquidity` are permissionless.** Anyone can call them. Safe because they only operate on existing positions and route funds to verified project terminals.
139
140
  6. **`claimFeeTokensFor` requires `SET_BUYBACK_POOL` permission** from the project owner. It validates the caller, not the beneficiary.
140
141
  7. **Cash-out fraction is geometrically optimized, not 50/50.** `_computeOptimalCashOutAmount` uses concentrated liquidity math to compute the exact ratio needed, typically 15-30%. Safety-capped at 50%.
141
142
  8. **Pool initialization price is the geometric mean** of [cashOutRate, issuanceRate] in tick space. Falls back to issuance rate if cash-out rate is 0 or ticks are equal.
142
143
  9. **One LP position per project/terminal-token pair.** The hook manages a single V4 NFT position. Rebalancing burns the old NFT and mints a new one, briefly leaving no active position.
143
144
  10. **After deployment, newly received reserved tokens are burned.** This is intentional -- prevents inflating the project token supply without corresponding LP rebalancing.
144
- 11. **Native ETH handling:** Juicebox uses `JBConstants.NATIVE_TOKEN` (`0x...EEEe`), V4 uses `Currency.wrap(address(0))`. The hook converts between them via `_toCurrency()`. The contract has `receive() external payable {}` to accept ETH during cash-outs and V4 TAKE operations.
145
+ 11. **Native ETH handling:** Juicebox uses `JBConstants.NATIVE_TOKEN` (`0x000000000000000000000000000000000000EEEe`), V4 uses `Currency.wrap(address(0))`. The hook converts between them via `_toCurrency()`. The contract has `receive() external payable {}` to accept ETH during cash-outs and V4 TAKE operations.
145
146
  12. **Deployed as clones via factory.** `UniV4DeploymentSplitHookDeployer` uses Solady's `LibClone`. The constructor sets shared infrastructure (directory, tokens, V4 contracts). Per-clone config (fee project, fee percent) is set via `initialize()`, which can only be called once.
146
147
  13. **Tick alignment:** All ticks are aligned to `TICK_SPACING = 200`. Negative ticks use floor semantics in `_alignTickToSpacing()`.
147
148
  14. **`minCashOutReturn = 0` defaults to 1% tolerance.** If no minimum is specified for `deployPool`, the hook applies a 1% slippage tolerance on the cash-out automatically.