@bananapus/univ4-lp-split-hook-v6 0.0.9 → 0.0.11
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/ADMINISTRATION.md +2 -1
- package/ARCHITECTURE.md +2 -2
- package/README.md +3 -2
- package/SKILLS.md +3 -3
- package/USER_JOURNEYS.md +3 -3
- package/package.json +3 -3
- package/src/JBUniswapV4LPSplitHook.sol +181 -107
- package/test/DeploymentStageTest.t.sol +14 -0
- package/test/SplitHookRegressions.t.sol +6 -7
- package/test/mock/MockJBContracts.sol +20 -3
package/ADMINISTRATION.md
CHANGED
|
@@ -33,7 +33,7 @@ Admin privileges and their scope in univ4-lp-split-hook-v6.
|
|
|
33
33
|
|
|
34
34
|
| Function | Required Role | Permission ID | Scope | What It Does |
|
|
35
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` (26) | Per-project,
|
|
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` (26) | Per-project, single terminal-token path | 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. This permanently commits the hook instance to one terminal-token path for that project. |
|
|
37
37
|
| `rebalanceLiquidity(projectId, terminalToken, ...)` | Project owner or SET_BUYBACK_POOL operator | `JBPermissionIds.SET_BUYBACK_POOL` (26) | 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
38
|
| `claimFeeTokensFor(projectId, beneficiary)` | Project owner or SET_BUYBACK_POOL operator | `JBPermissionIds.SET_BUYBACK_POOL` (26) | 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
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) |
|
|
@@ -98,6 +98,7 @@ What admins **cannot** do:
|
|
|
98
98
|
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`.
|
|
99
99
|
|
|
100
100
|
5. **Cannot deploy a second pool for the same project/terminal-token pair.** `deployPool()` reverts with `PoolAlreadyDeployed` if `tokenIdOf[projectId][terminalToken] != 0`.
|
|
101
|
+
6. **Cannot deploy a second terminal-token pool for the same project.** `processSplitWith` only receives the project token, not the terminal token. Once any pool is deployed, the project enters burn mode and `deployPool()` reverts with `OnlyOneTerminalTokenSupported` for other terminal tokens.
|
|
101
102
|
|
|
102
103
|
6. **Cannot prevent permissionless fee collection.** `collectAndRouteLPFees()` has no access control. Anyone can trigger fee collection and routing for any deployed pool.
|
|
103
104
|
|
package/ARCHITECTURE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Purpose
|
|
4
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
|
|
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 one Uniswap V4 pool for the project and provides initial liquidity. After deployment, burns new tokens and routes LP fees back to the project.
|
|
6
6
|
|
|
7
7
|
**Requirement:** The project must have a deployed ERC-20 token (via `JBTokens.deployERC20For`). Projects using only internal credits (`tokenOf == address(0)`) are rejected — credits cannot be paired as Uniswap V4 LP. `processSplitWith` reverts with `InvalidProjectId` if the token is `address(0)`.
|
|
8
8
|
|
|
@@ -26,7 +26,7 @@ Reserved token distribution → JBMultiTerminal.sendPayoutsOf()
|
|
|
26
26
|
→ Track accumulated balance
|
|
27
27
|
|
|
28
28
|
Owner/Operator → deployPool()
|
|
29
|
-
→ Validate: enough tokens accumulated
|
|
29
|
+
→ Validate: enough tokens accumulated and no other terminal-token pool already exists for the project
|
|
30
30
|
→ Create Uniswap V4 pool (projectToken/terminalToken) with ORACLE_HOOK (TWAP via observe())
|
|
31
31
|
→ Provide initial liquidity from accumulated tokens
|
|
32
32
|
→ Set up LP position
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Juicebox UniV4 LP Split Hook
|
|
2
2
|
|
|
3
|
-
Juicebox split hook that accumulates reserved project tokens over time, then deploys a Uniswap V4 concentrated liquidity position bounded by the project's issuance rate (ceiling) and cash-out rate (floor). After deployment, it manages fee collection, liquidity rebalancing, and routes LP fees back to the project with a configurable fee split.
|
|
3
|
+
Juicebox split hook that accumulates reserved project tokens over time, then deploys a single Uniswap V4 concentrated liquidity position for that project, bounded by the project's issuance rate (ceiling) and cash-out rate (floor). After deployment, it manages fee collection, liquidity rebalancing, and routes LP fees back to the project with a configurable fee split.
|
|
4
4
|
|
|
5
5
|
[Docs](https://docs.juicebox.money) | [Discord](https://discord.gg/juicebox)
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ This hook connects Juicebox's reserved token system to Uniswap V4's concentrated
|
|
|
10
10
|
|
|
11
11
|
**Stage 1 -- Accumulation:** Configure the hook as a reserved-token split in a project's ruleset. Each time reserved tokens are distributed, the hook accumulates them. This continues across multiple ruleset cycles until enough tokens are collected.
|
|
12
12
|
|
|
13
|
-
**Stage 2 -- Deployment & Operation:** The project owner calls `deployPool()` to create a V4 pool and mint an LP position. The hook cashes out a geometrically-optimized fraction of the accumulated tokens (typically 15-30%) for terminal tokens, then provides both as concentrated liquidity bounded by the project's economic rates. After deployment, any newly received reserved tokens are burned to prevent LP dilution.
|
|
13
|
+
**Stage 2 -- Deployment & Operation:** The project owner calls `deployPool()` to create a V4 pool and mint an LP position. This permanently selects the project's terminal-token path for this hook instance. The hook cashes out a geometrically-optimized fraction of the accumulated tokens (typically 15-30%) for terminal tokens, then provides both as concentrated liquidity bounded by the project's economic rates. After deployment, any newly received reserved tokens are burned to prevent LP dilution.
|
|
14
14
|
|
|
15
15
|
The LP position is bounded by two rate-derived ticks:
|
|
16
16
|
- **Lower bound (floor):** The cash-out rate -- what you get from redeeming project tokens via the bonding curve
|
|
@@ -153,6 +153,7 @@ script/
|
|
|
153
153
|
- **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.
|
|
154
154
|
- **Stale tick bounds:** If the project's issuance or cash-out rates change significantly (e.g., new ruleset with different weight), the LP position bounds become stale. The project owner must call `rebalanceLiquidity` to update them.
|
|
155
155
|
- **Cash-out price impact:** The initial `deployPool` cashes out a fraction of accumulated tokens, which affects the bonding curve. Large accumulations may create meaningful price impact.
|
|
156
|
+
- **One terminal token per project:** `processSplitWith` does not receive the terminal token, so once any pool is deployed the hook permanently switches the project into burn mode. A second terminal-token pool is intentionally unsupported and `deployPool()` reverts.
|
|
156
157
|
- **One position per pool:** The hook manages a single V4 NFT position per project/terminal-token pair. Rebalancing destroys and recreates it, temporarily leaving no active position.
|
|
157
158
|
- **Fee-project token accumulation:** Fee-project tokens are held by the hook until claimed via `claimFeeTokensFor`. If the fee project token changes or is not deployed, tokens may be stuck.
|
|
158
159
|
- **Rebalance to zero liquidity:** If both token balances are zero when rebalancing, the transaction reverts with `InsufficientLiquidity` to prevent bricking the position (tokenIdOf would become zero while projectDeployed remains true).
|
package/SKILLS.md
CHANGED
|
@@ -17,13 +17,13 @@ Juicebox reserved-token split hook that accumulates project tokens, deploys a Un
|
|
|
17
17
|
|
|
18
18
|
| Function | What it does |
|
|
19
19
|
|----------|-------------|
|
|
20
|
-
| `processSplitWith(context)` | Called by controller during reserved token distribution. If no pool deployed: accumulates tokens. If pool exists: burns received tokens. Only accepts `groupId == 1` (reserved tokens); reverts on payout splits (`groupId == 0`). |
|
|
20
|
+
| `processSplitWith(context)` | Called by controller during reserved token distribution. If no pool deployed: accumulates tokens. If pool exists: burns received tokens. Only accepts `groupId == 1` (reserved tokens); reverts on payout splits (`groupId == 0`). Because the split context has no terminal token, one deployment permanently switches the project into burn mode. |
|
|
21
21
|
|
|
22
22
|
### Pool Deployment
|
|
23
23
|
|
|
24
24
|
| Function | What it does |
|
|
25
25
|
|----------|-------------|
|
|
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).
|
|
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). Once a pool exists, a different `terminalToken` for the same project is rejected. |
|
|
27
27
|
|
|
28
28
|
### Fee Management
|
|
29
29
|
|
|
@@ -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
|
| `initialWeightOf` | `projectId => uint256` | Ruleset weight when first tokens were accumulated (for 10x decay check) |
|
|
129
129
|
| `projectDeployed` | `projectId => terminalToken => bool` | Whether a V4 pool has been deployed for this project/token pair |
|
|
130
|
-
| `deployedPoolCount` | `projectId => uint256` | Number of pools deployed for project
|
|
130
|
+
| `deployedPoolCount` | `projectId => uint256` | Number of pools deployed for project. Intentionally capped at 1 because processSplitWith cannot distinguish terminal-token paths. |
|
|
131
131
|
| `claimableFeeTokens` | `projectId => uint256` | Fee-project tokens claimable via `claimFeeTokensFor` |
|
|
132
132
|
| `initialized` | `bool` | Prevents re-initialization of clone instances |
|
|
133
133
|
| `ORACLE_HOOK` | `IHooks` (immutable) | Oracle hook for all JB V4 pools. Set in constructor. All pools are created with this hook in the `PoolKey.hooks` field, providing TWAP via `observe()`. |
|
package/USER_JOURNEYS.md
CHANGED
|
@@ -46,7 +46,7 @@ A new `JBUniswapV4LPSplitHook` clone is deployed, initialized, and registered. I
|
|
|
46
46
|
|
|
47
47
|
### Precondition
|
|
48
48
|
|
|
49
|
-
The project's ruleset has a reserved token split configured with `hook = address(lpSplitHook)`. No pool has been deployed yet (`deployedPoolCount[projectId] == 0`).
|
|
49
|
+
The project's ruleset has a reserved token split configured with `hook = address(lpSplitHook)`. No pool has been deployed yet (`deployedPoolCount[projectId] == 0`). This hook instance supports only one terminal-token deployment per project because the split context does not include the terminal token.
|
|
50
50
|
|
|
51
51
|
### Steps
|
|
52
52
|
|
|
@@ -94,14 +94,14 @@ The hook holds project tokens in `accumulatedProjectTokens[projectId]`. The toke
|
|
|
94
94
|
|
|
95
95
|
### Precondition
|
|
96
96
|
|
|
97
|
-
Tokens have been accumulated (`accumulatedProjectTokens[projectId] > 0`). No pool exists for this project
|
|
97
|
+
Tokens have been accumulated (`accumulatedProjectTokens[projectId] > 0`). No pool exists for this project and no terminal-token path has been committed yet.
|
|
98
98
|
|
|
99
99
|
### Steps
|
|
100
100
|
|
|
101
101
|
1. **Caller invokes `deployPool(projectId, terminalToken, minCashOutReturn)`**
|
|
102
102
|
|
|
103
103
|
- Permission check: requires `SET_BUYBACK_POOL` from project owner, unless `ruleset.weight * 10 <= initialWeightOf[projectId]`
|
|
104
|
-
- Checks: `tokenIdOf == 0`, `accumulatedProjectTokens > 0`, `primaryTerminalOf(projectId, terminalToken) != address(0)`
|
|
104
|
+
- Checks: `tokenIdOf == 0`, `deployedPoolCount == 0`, `accumulatedProjectTokens > 0`, `primaryTerminalOf(projectId, terminalToken) != address(0)`
|
|
105
105
|
|
|
106
106
|
2. **`_createAndInitializePool()` creates the V4 pool**
|
|
107
107
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bananapus/univ4-lp-split-hook-v6",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@bananapus/address-registry-v6": "^0.0.10",
|
|
19
|
-
"@bananapus/core-v6": "^0.0.
|
|
19
|
+
"@bananapus/core-v6": "^0.0.23",
|
|
20
20
|
"@bananapus/permission-ids-v6": "^0.0.10",
|
|
21
|
-
"@bananapus/univ4-router-v6": "^0.0.
|
|
21
|
+
"@bananapus/univ4-router-v6": "^0.0.9",
|
|
22
22
|
"@openzeppelin/contracts": "^5.6.1",
|
|
23
23
|
"@prb/math": "^4.1.0",
|
|
24
24
|
"@uniswap/permit2": "github:Uniswap/permit2",
|
|
@@ -21,7 +21,6 @@ import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
|
21
21
|
import {IJBMultiTerminal} from "@bananapus/core-v6/src/interfaces/IJBMultiTerminal.sol";
|
|
22
22
|
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
23
23
|
import {IJBSplitHook} from "@bananapus/core-v6/src/interfaces/IJBSplitHook.sol";
|
|
24
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
25
24
|
import {IJBTokens} from "@bananapus/core-v6/src/interfaces/IJBTokens.sol";
|
|
26
25
|
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
27
26
|
import {JBRulesetMetadataResolver} from "@bananapus/core-v6/src/libraries/JBRulesetMetadataResolver.sol";
|
|
@@ -73,6 +72,7 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
73
72
|
error JBUniswapV4LPSplitHook_InvalidTerminalToken();
|
|
74
73
|
error JBUniswapV4LPSplitHook_NoTokensAccumulated();
|
|
75
74
|
error JBUniswapV4LPSplitHook_NotHookSpecifiedInContext();
|
|
75
|
+
error JBUniswapV4LPSplitHook_OnlyOneTerminalTokenSupported();
|
|
76
76
|
error JBUniswapV4LPSplitHook_Permit2AmountOverflow();
|
|
77
77
|
error JBUniswapV4LPSplitHook_PoolAlreadyDeployed();
|
|
78
78
|
error JBUniswapV4LPSplitHook_SplitSenderNotValidControllerOrTerminal();
|
|
@@ -158,9 +158,9 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
158
158
|
mapping(uint256 projectId => uint256 accumulatedProjectTokens) public accumulatedProjectTokens;
|
|
159
159
|
|
|
160
160
|
/// @notice ProjectID => Number of deployed pools for this project.
|
|
161
|
-
/// @dev
|
|
162
|
-
///
|
|
163
|
-
///
|
|
161
|
+
/// @dev This is intentionally capped at 1. `processSplitWith` only receives the project token and cannot tell
|
|
162
|
+
/// which terminal token a reserved-token distribution is intended for, so one deployment permanently flips
|
|
163
|
+
/// the project from accumulation to burn mode.
|
|
164
164
|
mapping(uint256 projectId => uint256 count) public deployedPoolCount;
|
|
165
165
|
|
|
166
166
|
/// @notice ProjectID => Fee tokens claimable by that project
|
|
@@ -272,11 +272,9 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
272
272
|
try IJBMultiTerminal(
|
|
273
273
|
address(IJBDirectory(DIRECTORY).primaryTerminalOf({projectId: projectId, token: terminalToken}))
|
|
274
274
|
).STORE()
|
|
275
|
-
.
|
|
275
|
+
.currentTotalReclaimableSurplusOf({
|
|
276
276
|
projectId: projectId,
|
|
277
277
|
cashOutCount: _WAD,
|
|
278
|
-
terminals: new IJBTerminal[](0),
|
|
279
|
-
accountingContexts: new JBAccountingContext[](0),
|
|
280
278
|
decimals: _getTokenDecimals(terminalToken),
|
|
281
279
|
// Safe: truncation to uint32 is the standard Juicebox currency encoding.
|
|
282
280
|
// forge-lint: disable-next-line(unsafe-typecast)
|
|
@@ -507,7 +505,6 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
507
505
|
}
|
|
508
506
|
|
|
509
507
|
/// @notice Collect LP fees and route them back to the project
|
|
510
|
-
// slither-disable-next-line reentrancy-events
|
|
511
508
|
// forge-lint: disable-next-line(mixed-case-function)
|
|
512
509
|
function collectAndRouteLPFees(uint256 projectId, address terminalToken) external {
|
|
513
510
|
uint256 tokenId = tokenIdOf[projectId][terminalToken];
|
|
@@ -527,6 +524,7 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
527
524
|
params[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), "");
|
|
528
525
|
params[1] = abi.encode(key.currency0, key.currency1, address(this));
|
|
529
526
|
|
|
527
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
530
528
|
POSITION_MANAGER.modifyLiquidities({
|
|
531
529
|
unlockData: abi.encode(actions, params), deadline: block.timestamp + _DEADLINE_SECONDS
|
|
532
530
|
});
|
|
@@ -536,6 +534,7 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
536
534
|
uint256 amount1 = _currencyBalance(key.currency1) - bal1Before;
|
|
537
535
|
|
|
538
536
|
// Route terminal token fees back to the project
|
|
537
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
539
538
|
_routeCollectedFees({
|
|
540
539
|
projectId: projectId,
|
|
541
540
|
projectToken: projectToken,
|
|
@@ -545,6 +544,7 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
545
544
|
});
|
|
546
545
|
|
|
547
546
|
// Burn collected project token fees
|
|
547
|
+
// slither-disable-next-line reentrancy-events,reentrancy-no-eth
|
|
548
548
|
_burnReceivedTokens({projectId: projectId, projectToken: projectToken});
|
|
549
549
|
}
|
|
550
550
|
|
|
@@ -566,6 +566,7 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
566
566
|
}
|
|
567
567
|
|
|
568
568
|
if (tokenIdOf[projectId][terminalToken] != 0) revert JBUniswapV4LPSplitHook_PoolAlreadyDeployed();
|
|
569
|
+
if (deployedPoolCount[projectId] != 0) revert JBUniswapV4LPSplitHook_OnlyOneTerminalTokenSupported();
|
|
569
570
|
|
|
570
571
|
address projectToken = address(IJBTokens(TOKENS).tokenOf(projectId));
|
|
571
572
|
uint256 projectTokenBalance = accumulatedProjectTokens[projectId];
|
|
@@ -576,6 +577,10 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
576
577
|
address(IJBDirectory(DIRECTORY).primaryTerminalOf({projectId: projectId, token: terminalToken}));
|
|
577
578
|
if (terminal == address(0)) revert JBUniswapV4LPSplitHook_InvalidTerminalToken();
|
|
578
579
|
|
|
580
|
+
// Flip the project into post-deploy burn mode before any external calls so reentrancy cannot
|
|
581
|
+
// observe the project as still being in accumulation mode.
|
|
582
|
+
deployedPoolCount[projectId]++;
|
|
583
|
+
|
|
579
584
|
_deployPoolAndAddLiquidity({
|
|
580
585
|
projectId: projectId,
|
|
581
586
|
projectToken: projectToken,
|
|
@@ -583,8 +588,6 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
583
588
|
minCashOutReturn: minCashOutReturn
|
|
584
589
|
});
|
|
585
590
|
|
|
586
|
-
deployedPoolCount[projectId]++;
|
|
587
|
-
|
|
588
591
|
emit ProjectDeployed(projectId, terminalToken, PoolId.unwrap(_poolKeys[projectId][terminalToken].toId()));
|
|
589
592
|
}
|
|
590
593
|
|
|
@@ -650,105 +653,17 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
650
653
|
address projectToken = address(IJBTokens(TOKENS).tokenOf(projectId));
|
|
651
654
|
PoolKey memory key = _poolKeys[projectId][terminalToken];
|
|
652
655
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
uint256 bal1Before = _currencyBalance(key.currency1);
|
|
657
|
-
|
|
658
|
-
bytes memory feeActions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
|
|
659
|
-
|
|
660
|
-
bytes[] memory feeParams = new bytes[](2);
|
|
661
|
-
feeParams[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), "");
|
|
662
|
-
feeParams[1] = abi.encode(key.currency0, key.currency1, address(this));
|
|
663
|
-
|
|
664
|
-
POSITION_MANAGER.modifyLiquidities({
|
|
665
|
-
unlockData: abi.encode(feeActions, feeParams), deadline: block.timestamp + _DEADLINE_SECONDS
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
uint256 feeAmount0 = _currencyBalance(key.currency0) - bal0Before;
|
|
669
|
-
uint256 feeAmount1 = _currencyBalance(key.currency1) - bal1Before;
|
|
670
|
-
|
|
671
|
-
_routeCollectedFees({
|
|
672
|
-
projectId: projectId,
|
|
673
|
-
projectToken: projectToken,
|
|
674
|
-
terminalToken: terminalToken,
|
|
675
|
-
amount0: feeAmount0,
|
|
676
|
-
amount1: feeAmount1
|
|
677
|
-
});
|
|
678
|
-
_burnReceivedTokens({projectId: projectId, projectToken: projectToken});
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Step 2: Burn position to recover principal via BURN_POSITION + TAKE_PAIR
|
|
682
|
-
{
|
|
683
|
-
bytes memory burnActions = abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR));
|
|
684
|
-
|
|
685
|
-
bytes[] memory burnParams = new bytes[](2);
|
|
686
|
-
// Safe: min amounts are user-provided slippage params; PositionManager accepts uint128.
|
|
687
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
688
|
-
burnParams[0] = abi.encode(tokenId, uint128(decreaseAmount0Min), uint128(decreaseAmount1Min), "");
|
|
689
|
-
burnParams[1] = abi.encode(key.currency0, key.currency1, address(this));
|
|
690
|
-
|
|
691
|
-
POSITION_MANAGER.modifyLiquidities({
|
|
692
|
-
unlockData: abi.encode(burnActions, burnParams), deadline: block.timestamp + _DEADLINE_SECONDS
|
|
693
|
-
});
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Step 2: Mint new position with updated tick bounds
|
|
697
|
-
{
|
|
698
|
-
uint256 projectTokenBalance = IERC20(projectToken).balanceOf(address(this));
|
|
699
|
-
uint256 terminalTokenBalance = _getTerminalTokenBalance(terminalToken);
|
|
700
|
-
|
|
701
|
-
(int24 tickLower, int24 tickUpper) =
|
|
702
|
-
_calculateTickBounds({projectId: projectId, terminalToken: terminalToken, projectToken: projectToken});
|
|
703
|
-
|
|
704
|
-
// Use the actual pool price for liquidity calculation so the target matches the pool's
|
|
705
|
-
// current state. Using JB issuance price here would produce suboptimal liquidity when the
|
|
706
|
-
// pool price has diverged.
|
|
707
|
-
// slither-disable-next-line unused-return
|
|
708
|
-
(uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
|
|
709
|
-
uint160 sqrtPriceA = TickMath.getSqrtPriceAtTick(tickLower);
|
|
710
|
-
uint160 sqrtPriceB = TickMath.getSqrtPriceAtTick(tickUpper);
|
|
711
|
-
|
|
712
|
-
// Sort amounts by currency order
|
|
713
|
-
Currency terminalCurrency = _toCurrency(terminalToken);
|
|
714
|
-
(address token0,) = _sortTokens({tokenA: projectToken, tokenB: Currency.unwrap(terminalCurrency)});
|
|
715
|
-
uint256 amount0 = projectToken == token0 ? projectTokenBalance : terminalTokenBalance;
|
|
716
|
-
uint256 amount1 = projectToken == token0 ? terminalTokenBalance : projectTokenBalance;
|
|
717
|
-
|
|
718
|
-
// Calculate liquidity from amounts
|
|
719
|
-
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts({
|
|
720
|
-
sqrtPriceX96: sqrtPriceX96,
|
|
721
|
-
sqrtPriceAX96: sqrtPriceA,
|
|
722
|
-
sqrtPriceBX96: sqrtPriceB,
|
|
723
|
-
amount0: amount0,
|
|
724
|
-
amount1: amount1
|
|
725
|
-
});
|
|
726
|
-
|
|
727
|
-
if (liquidity > 0) {
|
|
728
|
-
uint256 newTokenId = POSITION_MANAGER.nextTokenId();
|
|
729
|
-
|
|
730
|
-
_mintPosition({
|
|
731
|
-
key: key,
|
|
732
|
-
tickLower: tickLower,
|
|
733
|
-
tickUpper: tickUpper,
|
|
734
|
-
liquidity: liquidity,
|
|
735
|
-
amount0: amount0,
|
|
736
|
-
amount1: amount1
|
|
737
|
-
});
|
|
656
|
+
_collectAndRouteFees({
|
|
657
|
+
projectId: projectId, projectToken: projectToken, terminalToken: terminalToken, tokenId: tokenId, key: key
|
|
658
|
+
});
|
|
738
659
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
// outside tick range making the position single-sided with zero on one side).
|
|
743
|
-
// Revert to prevent bricking the project's LP — the old position was already
|
|
744
|
-
// burned by the BURN_POSITION action above, so this protects the invariant
|
|
745
|
-
// that tokenIdOf is always nonzero for deployed projects.
|
|
746
|
-
revert JBUniswapV4LPSplitHook_InsufficientLiquidity();
|
|
747
|
-
}
|
|
660
|
+
_burnExistingPosition({
|
|
661
|
+
tokenId: tokenId, key: key, decreaseAmount0Min: decreaseAmount0Min, decreaseAmount1Min: decreaseAmount1Min
|
|
662
|
+
});
|
|
748
663
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
664
|
+
_mintRebalancedPosition({
|
|
665
|
+
projectId: projectId, projectToken: projectToken, terminalToken: terminalToken, key: key
|
|
666
|
+
});
|
|
752
667
|
}
|
|
753
668
|
|
|
754
669
|
//*********************************************************************//
|
|
@@ -896,6 +811,38 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
896
811
|
return rounded;
|
|
897
812
|
}
|
|
898
813
|
|
|
814
|
+
/// @notice Burn an existing LP position via `BURN_POSITION` + `TAKE_PAIR` and recover its principal.
|
|
815
|
+
/// @dev Called during rebalancing after fees have already been collected. The recovered tokens remain in
|
|
816
|
+
/// this contract for the subsequent `_mintRebalancedPosition` call.
|
|
817
|
+
/// @param tokenId The Uniswap V4 position NFT token ID to burn.
|
|
818
|
+
/// @param key The pool key identifying the Uniswap V4 pool.
|
|
819
|
+
/// @param decreaseAmount0Min Minimum amount of token0 to receive (slippage protection).
|
|
820
|
+
/// @param decreaseAmount1Min Minimum amount of token1 to receive (slippage protection).
|
|
821
|
+
function _burnExistingPosition(
|
|
822
|
+
uint256 tokenId,
|
|
823
|
+
PoolKey memory key,
|
|
824
|
+
uint256 decreaseAmount0Min,
|
|
825
|
+
uint256 decreaseAmount1Min
|
|
826
|
+
)
|
|
827
|
+
internal
|
|
828
|
+
{
|
|
829
|
+
// BURN_POSITION removes all remaining liquidity and destroys the NFT.
|
|
830
|
+
// TAKE_PAIR transfers the recovered token0 and token1 to this contract.
|
|
831
|
+
bytes memory burnActions = abi.encodePacked(uint8(Actions.BURN_POSITION), uint8(Actions.TAKE_PAIR));
|
|
832
|
+
|
|
833
|
+
bytes[] memory burnParams = new bytes[](2);
|
|
834
|
+
// BURN_POSITION params: (tokenId, minAmount0, minAmount1, hookData).
|
|
835
|
+
// Min amounts are caller-supplied slippage bounds; PositionManager accepts uint128.
|
|
836
|
+
// forge-lint: disable-next-line(unsafe-typecast)
|
|
837
|
+
burnParams[0] = abi.encode(tokenId, uint128(decreaseAmount0Min), uint128(decreaseAmount1Min), "");
|
|
838
|
+
// TAKE_PAIR params: (currency0, currency1, recipient).
|
|
839
|
+
burnParams[1] = abi.encode(key.currency0, key.currency1, address(this));
|
|
840
|
+
|
|
841
|
+
POSITION_MANAGER.modifyLiquidities({
|
|
842
|
+
unlockData: abi.encode(burnActions, burnParams), deadline: block.timestamp + _DEADLINE_SECONDS
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
899
846
|
/// @notice Burn project tokens using the controller
|
|
900
847
|
// slither-disable-next-line incorrect-equality,reentrancy-events
|
|
901
848
|
function _burnProjectTokens(uint256 projectId, address projectToken, uint256 amount, string memory memo) internal {
|
|
@@ -986,6 +933,59 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
986
933
|
}
|
|
987
934
|
}
|
|
988
935
|
|
|
936
|
+
/// @notice Collect accrued Uniswap LP trading fees and route them to the project's terminal balance.
|
|
937
|
+
/// @dev Uses `DECREASE_LIQUIDITY(0)` to trigger fee collection without removing any principal, followed by
|
|
938
|
+
/// `TAKE_PAIR` to transfer the fees to this contract. Terminal-token fees are added to the project's balance;
|
|
939
|
+
/// project-token fees are burned to avoid inflating supply.
|
|
940
|
+
/// @param projectId The ID of the Juicebox project whose LP fees are being collected.
|
|
941
|
+
/// @param projectToken The project's ERC-20 token address.
|
|
942
|
+
/// @param terminalToken The terminal token (e.g. ETH or USDC) paired with the project token.
|
|
943
|
+
/// @param tokenId The Uniswap V4 position NFT token ID to collect fees from.
|
|
944
|
+
/// @param key The pool key identifying the Uniswap V4 pool.
|
|
945
|
+
// slither-disable-next-line reentrancy-eth,reentrancy-benign,reentrancy-events
|
|
946
|
+
function _collectAndRouteFees(
|
|
947
|
+
uint256 projectId,
|
|
948
|
+
address projectToken,
|
|
949
|
+
address terminalToken,
|
|
950
|
+
uint256 tokenId,
|
|
951
|
+
PoolKey memory key
|
|
952
|
+
)
|
|
953
|
+
internal
|
|
954
|
+
{
|
|
955
|
+
// Snapshot balances before collection to isolate fee amounts from any existing balance.
|
|
956
|
+
uint256 bal0Before = _currencyBalance(key.currency0);
|
|
957
|
+
uint256 bal1Before = _currencyBalance(key.currency1);
|
|
958
|
+
|
|
959
|
+
// DECREASE_LIQUIDITY with amount=0 triggers fee collection without removing principal.
|
|
960
|
+
// TAKE_PAIR transfers the collected fees (both currencies) to this contract.
|
|
961
|
+
bytes memory feeActions = abi.encodePacked(uint8(Actions.DECREASE_LIQUIDITY), uint8(Actions.TAKE_PAIR));
|
|
962
|
+
|
|
963
|
+
bytes[] memory feeParams = new bytes[](2);
|
|
964
|
+
// DECREASE_LIQUIDITY params: (tokenId, liquidity=0, minAmount0=0, minAmount1=0, hookData).
|
|
965
|
+
feeParams[0] = abi.encode(tokenId, uint256(0), uint128(0), uint128(0), "");
|
|
966
|
+
// TAKE_PAIR params: (currency0, currency1, recipient).
|
|
967
|
+
feeParams[1] = abi.encode(key.currency0, key.currency1, address(this));
|
|
968
|
+
|
|
969
|
+
POSITION_MANAGER.modifyLiquidities({
|
|
970
|
+
unlockData: abi.encode(feeActions, feeParams), deadline: block.timestamp + _DEADLINE_SECONDS
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Diff balances to determine exactly how much was collected as fees.
|
|
974
|
+
uint256 feeAmount0 = _currencyBalance(key.currency0) - bal0Before;
|
|
975
|
+
uint256 feeAmount1 = _currencyBalance(key.currency1) - bal1Before;
|
|
976
|
+
|
|
977
|
+
// Route terminal-token fees to the project's balance; project-token fees are burned below.
|
|
978
|
+
_routeCollectedFees({
|
|
979
|
+
projectId: projectId,
|
|
980
|
+
projectToken: projectToken,
|
|
981
|
+
terminalToken: terminalToken,
|
|
982
|
+
amount0: feeAmount0,
|
|
983
|
+
amount1: feeAmount1
|
|
984
|
+
});
|
|
985
|
+
// Burn any project tokens received as fees to avoid inflating circulating supply.
|
|
986
|
+
_burnReceivedTokens({projectId: projectId, projectToken: projectToken});
|
|
987
|
+
}
|
|
988
|
+
|
|
989
989
|
/// @notice Compute the initial sqrtPriceX96 for pool initialization
|
|
990
990
|
function _computeInitialSqrtPrice(
|
|
991
991
|
uint256 projectId,
|
|
@@ -1264,6 +1264,80 @@ contract JBUniswapV4LPSplitHook is IJBUniswapV4LPSplitHook, IJBSplitHook, JBPerm
|
|
|
1264
1264
|
});
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
+
/// @notice Mint a new LP position with tick bounds recalculated from current issuance and cash-out rates.
|
|
1268
|
+
/// @dev Called after `_burnExistingPosition` has recovered the old position's principal. Computes liquidity from
|
|
1269
|
+
/// this contract's current token balances and the pool's live `sqrtPriceX96`. Reverts with
|
|
1270
|
+
/// `JBUniswapV4LPSplitHook_InsufficientLiquidity` if the resulting liquidity is zero (e.g. price moved entirely
|
|
1271
|
+
/// outside the new tick range), preventing `tokenIdOf` from being left stale. Any leftover tokens after minting
|
|
1272
|
+
/// are routed back to the project via `_handleLeftoverTokens`.
|
|
1273
|
+
/// @param projectId The ID of the Juicebox project being rebalanced.
|
|
1274
|
+
/// @param projectToken The project's ERC-20 token address.
|
|
1275
|
+
/// @param terminalToken The terminal token paired with the project token.
|
|
1276
|
+
/// @param key The pool key identifying the Uniswap V4 pool.
|
|
1277
|
+
// slither-disable-next-line reentrancy-eth,reentrancy-benign,reentrancy-events
|
|
1278
|
+
function _mintRebalancedPosition(
|
|
1279
|
+
uint256 projectId,
|
|
1280
|
+
address projectToken,
|
|
1281
|
+
address terminalToken,
|
|
1282
|
+
PoolKey memory key
|
|
1283
|
+
)
|
|
1284
|
+
internal
|
|
1285
|
+
{
|
|
1286
|
+
uint256 projectTokenBalance = IERC20(projectToken).balanceOf(address(this));
|
|
1287
|
+
uint256 terminalTokenBalance = _getTerminalTokenBalance(terminalToken);
|
|
1288
|
+
|
|
1289
|
+
(int24 tickLower, int24 tickUpper) =
|
|
1290
|
+
_calculateTickBounds({projectId: projectId, terminalToken: terminalToken, projectToken: projectToken});
|
|
1291
|
+
|
|
1292
|
+
// Use the actual pool price for liquidity calculation so the target matches the pool's
|
|
1293
|
+
// current state. Using JB issuance price here would produce suboptimal liquidity when the
|
|
1294
|
+
// pool price has diverged.
|
|
1295
|
+
// slither-disable-next-line unused-return
|
|
1296
|
+
(uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(key.toId());
|
|
1297
|
+
uint160 sqrtPriceA = TickMath.getSqrtPriceAtTick(tickLower);
|
|
1298
|
+
uint160 sqrtPriceB = TickMath.getSqrtPriceAtTick(tickUpper);
|
|
1299
|
+
|
|
1300
|
+
// Map token balances to (amount0, amount1) matching the pool's currency ordering.
|
|
1301
|
+
Currency terminalCurrency = _toCurrency(terminalToken);
|
|
1302
|
+
(address token0,) = _sortTokens({tokenA: projectToken, tokenB: Currency.unwrap(terminalCurrency)});
|
|
1303
|
+
uint256 amount0 = projectToken == token0 ? projectTokenBalance : terminalTokenBalance;
|
|
1304
|
+
uint256 amount1 = projectToken == token0 ? terminalTokenBalance : projectTokenBalance;
|
|
1305
|
+
|
|
1306
|
+
// Derive the maximum liquidity mintable from our balances at the current pool price.
|
|
1307
|
+
uint128 liquidity = LiquidityAmounts.getLiquidityForAmounts({
|
|
1308
|
+
sqrtPriceX96: sqrtPriceX96,
|
|
1309
|
+
sqrtPriceAX96: sqrtPriceA,
|
|
1310
|
+
sqrtPriceBX96: sqrtPriceB,
|
|
1311
|
+
amount0: amount0,
|
|
1312
|
+
amount1: amount1
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
if (liquidity > 0) {
|
|
1316
|
+
uint256 newTokenId = POSITION_MANAGER.nextTokenId();
|
|
1317
|
+
|
|
1318
|
+
_mintPosition({
|
|
1319
|
+
key: key,
|
|
1320
|
+
tickLower: tickLower,
|
|
1321
|
+
tickUpper: tickUpper,
|
|
1322
|
+
liquidity: liquidity,
|
|
1323
|
+
amount0: amount0,
|
|
1324
|
+
amount1: amount1
|
|
1325
|
+
});
|
|
1326
|
+
|
|
1327
|
+
tokenIdOf[projectId][terminalToken] = newTokenId;
|
|
1328
|
+
} else {
|
|
1329
|
+
// Zero liquidity means the position cannot be re-created (e.g., price moved
|
|
1330
|
+
// outside tick range making the position single-sided with zero on one side).
|
|
1331
|
+
// Revert to prevent bricking the project's LP — the old position was already
|
|
1332
|
+
// burned by the BURN_POSITION action above, so this protects the invariant
|
|
1333
|
+
// that tokenIdOf is always nonzero for deployed projects.
|
|
1334
|
+
revert JBUniswapV4LPSplitHook_InsufficientLiquidity();
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// Return any dust left over after minting (due to rounding or single-sided excess) to the project.
|
|
1338
|
+
_handleLeftoverTokens({projectId: projectId, projectToken: projectToken, terminalToken: terminalToken});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1267
1341
|
/// @notice Approve an ERC20 token via Permit2 so PositionManager can pull it during SETTLE.
|
|
1268
1342
|
function _approveViaPermit2(address token, uint256 amount) internal {
|
|
1269
1343
|
IERC20(token).forceApprove({spender: address(PERMIT2), value: amount});
|
|
@@ -139,6 +139,20 @@ contract DeploymentStageTest is LPSplitHookV4TestBase {
|
|
|
139
139
|
hook.deployPool(PROJECT_ID, address(terminalToken), 0);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/// @notice Once any pool is deployed for a project, a second terminal-token pool is rejected.
|
|
143
|
+
function test_DeployPool_RevertsIf_SecondTerminalTokenRequested() public {
|
|
144
|
+
_accumulateAndDeploy(PROJECT_ID, 100e18);
|
|
145
|
+
|
|
146
|
+
address secondTerminalToken = makeAddr("secondTerminalToken");
|
|
147
|
+
_setDirectoryTerminal(PROJECT_ID, secondTerminalToken, address(terminal));
|
|
148
|
+
|
|
149
|
+
_accumulateTokens(PROJECT_ID, 50e18);
|
|
150
|
+
|
|
151
|
+
vm.expectRevert(JBUniswapV4LPSplitHook.JBUniswapV4LPSplitHook_OnlyOneTerminalTokenSupported.selector);
|
|
152
|
+
vm.prank(owner);
|
|
153
|
+
hook.deployPool(PROJECT_ID, secondTerminalToken, 0);
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
// ─────────────────────────────────────────────────────────────────────
|
|
143
157
|
// 9. deployPool — reverts if terminal token is invalid
|
|
144
158
|
// ─────────────────────────────────────────────────────────────────────
|
|
@@ -254,9 +254,8 @@ contract SplitHookRegressionsTest is LPSplitHookV4TestBase {
|
|
|
254
254
|
);
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
/// @notice
|
|
258
|
-
|
|
259
|
-
function test_M2_multiTerminalToken_independentFlags() public {
|
|
257
|
+
/// @notice Once any pool is deployed, the hook rejects a second terminal-token pool for the project.
|
|
258
|
+
function test_M2_multiTerminalToken_secondDeployReverts() public {
|
|
260
259
|
// PROJECT_ID already has a pool for terminalToken
|
|
261
260
|
assertTrue(
|
|
262
261
|
hook.isPoolDeployed(PROJECT_ID, address(terminalToken)), "First terminal token pool should be deployed"
|
|
@@ -288,9 +287,9 @@ contract SplitHookRegressionsTest is LPSplitHookV4TestBase {
|
|
|
288
287
|
hook.isPoolDeployed(PROJECT_ID, address(terminalToken)), "First terminal token should still be deployed"
|
|
289
288
|
);
|
|
290
289
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
290
|
+
projectToken.mint(address(hook), 10e18);
|
|
291
|
+
vm.expectRevert(JBUniswapV4LPSplitHook.JBUniswapV4LPSplitHook_OnlyOneTerminalTokenSupported.selector);
|
|
292
|
+
vm.prank(owner);
|
|
293
|
+
hook.deployPool(PROJECT_ID, address(secondTerminalToken), 0);
|
|
295
294
|
}
|
|
296
295
|
}
|
|
@@ -542,14 +542,31 @@ contract MockJBTerminalStore {
|
|
|
542
542
|
}
|
|
543
543
|
|
|
544
544
|
/// @dev Matches
|
|
545
|
-
/// IJBTerminalStore.currentReclaimableSurplusOf(uint256,uint256,IJBTerminal[],
|
|
545
|
+
/// IJBTerminalStore.currentReclaimableSurplusOf(uint256,uint256,IJBTerminal[],address[],uint256,uint256)
|
|
546
546
|
function currentReclaimableSurplusOf(
|
|
547
547
|
uint256 projectId,
|
|
548
548
|
uint256 cashOutCount,
|
|
549
549
|
IJBTerminal[] calldata,
|
|
550
550
|
/* terminals */
|
|
551
|
-
|
|
552
|
-
/*
|
|
551
|
+
address[] calldata,
|
|
552
|
+
/* tokens */
|
|
553
|
+
uint256,
|
|
554
|
+
/* decimals */
|
|
555
|
+
uint256 /* currency */
|
|
556
|
+
)
|
|
557
|
+
external
|
|
558
|
+
view
|
|
559
|
+
returns (uint256)
|
|
560
|
+
{
|
|
561
|
+
uint256 surplus = surplusPerToken[projectId];
|
|
562
|
+
if (surplus == 0) return 0;
|
|
563
|
+
return (surplus * cashOutCount) / 1e18;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/// @dev Matches IJBTerminalStore.currentTotalReclaimableSurplusOf(uint256,uint256,uint256,uint256)
|
|
567
|
+
function currentTotalReclaimableSurplusOf(
|
|
568
|
+
uint256 projectId,
|
|
569
|
+
uint256 cashOutCount,
|
|
553
570
|
uint256,
|
|
554
571
|
/* decimals */
|
|
555
572
|
uint256 /* currency */
|