@bananapus/buyback-hook-v6 0.0.9 → 0.0.10

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/STYLE_GUIDE.md CHANGED
@@ -386,6 +386,8 @@ jobs:
386
386
  uses: foundry-rs/foundry-toolchain@v1
387
387
  - name: Run tests
388
388
  run: forge test --fail-fast --summary --detailed --skip "*/script/**"
389
+ env:
390
+ RPC_ETHEREUM_MAINNET: ${{ secrets.RPC_ETHEREUM_MAINNET }}
389
391
  - name: Check contract sizes
390
392
  run: FOUNDRY_PROFILE=ci_sizes forge build --sizes --skip "*/test/**" --skip "*/script/**" --skip SphinxUtils
391
393
  ```
@@ -450,6 +452,15 @@ Run `forge fmt` before committing. The `[fmt]` config in `foundry.toml` enforces
450
452
 
451
453
  CI checks formatting via `forge fmt --check`.
452
454
 
455
+ ### CI Secrets
456
+
457
+ | Secret | Purpose |
458
+ |--------|--------|
459
+ | `NPM_TOKEN` | npm publish access (used by `publish.yml`) |
460
+ | `RPC_ETHEREUM_MAINNET` | Ethereum mainnet RPC URL for fork tests (used by `test.yml`) |
461
+
462
+ Fork tests require `RPC_ETHEREUM_MAINNET` — they fail if it's missing.
463
+
453
464
  ### Branching
454
465
 
455
466
  - `main` is the primary branch
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bananapus/buyback-hook-v6",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -29,6 +29,7 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
29
29
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
30
30
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
31
31
  import {mulDiv} from "@prb/math/src/Common.sol";
32
+ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
32
33
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
33
34
  import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
34
35
  import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
@@ -108,8 +109,8 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
108
109
  /// @notice The Uniswap V4 PoolManager singleton.
109
110
  IPoolManager public immutable override POOL_MANAGER;
110
111
 
111
- /// @notice The wETH contract.
112
- IWETH9 public immutable override WETH;
112
+ /// @notice The wrapped native token contract (e.g. WETH on Ethereum, WMATIC on Polygon).
113
+ IWETH9 public immutable override WRAPPED_NATIVE_TOKEN;
113
114
 
114
115
  //*********************************************************************//
115
116
  // --------------------- public stored properties -------------------- //
@@ -117,7 +118,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
117
118
 
118
119
  /// @notice The PoolKey for a given project's token and terminal token pair.
119
120
  /// @custom:param projectId The ID of the project whose token is traded in the pool.
120
- /// @custom:param terminalToken The address of the terminal token (normalized to WETH for native).
121
+ /// @custom:param terminalToken The address of the terminal token (normalized to wrapped native token for native).
121
122
  mapping(uint256 projectId => mapping(address terminalToken => PoolKey)) internal _poolKeyOf;
122
123
 
123
124
  /// @notice The address of each project's token.
@@ -157,7 +158,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
157
158
  /// @param prices The contract that exposes price feeds.
158
159
  /// @param projects The project registry.
159
160
  /// @param tokens The token registry.
160
- /// @param weth The WETH contract.
161
+ /// @param wrappedNativeToken The wrapped native token contract (e.g. WETH on Ethereum).
161
162
  /// @param poolManager The Uniswap V4 PoolManager singleton.
162
163
  /// @param trustedForwarder A trusted forwarder of transactions to this contract.
163
164
  constructor(
@@ -166,7 +167,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
166
167
  IJBPrices prices,
167
168
  IJBProjects projects,
168
169
  IJBTokens tokens,
169
- IWETH9 weth,
170
+ IWETH9 wrappedNativeToken,
170
171
  IPoolManager poolManager,
171
172
  address trustedForwarder
172
173
  )
@@ -178,7 +179,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
178
179
  PROJECTS = projects;
179
180
  PRICES = prices;
180
181
  POOL_MANAGER = poolManager;
181
- WETH = weth;
182
+ WRAPPED_NATIVE_TOKEN = wrappedNativeToken;
182
183
  }
183
184
 
184
185
  //*********************************************************************//
@@ -231,11 +232,11 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
231
232
  revert JBBuybackHook_SpecifiedSlippageExceeded(exactSwapAmountOut, minimumSwapAmountOut);
232
233
  }
233
234
 
234
- // If native ETH was wrapped to WETH for the swap (pool uses WETH), unwrap any leftover WETH
235
- // back to ETH so the balance delta below correctly captures leftovers.
235
+ // If native tokens were wrapped for the swap, unwrap any leftover wrapped tokens
236
+ // back to native so the balance delta below correctly captures leftovers.
236
237
  if (context.forwardedAmount.token == JBConstants.NATIVE_TOKEN) {
237
- uint256 wethBalance = IERC20(address(WETH)).balanceOf(address(this));
238
- if (wethBalance != 0) WETH.withdraw(wethBalance);
238
+ uint256 wrappedBalance = IERC20(address(WRAPPED_NATIVE_TOKEN)).balanceOf(address(this));
239
+ if (wrappedBalance != 0) WRAPPED_NATIVE_TOKEN.withdraw(wrappedBalance);
239
240
  }
240
241
 
241
242
  // Compute leftover terminal tokens as a delta (balanceAfter - balanceBefore).
@@ -323,63 +324,57 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
323
324
  account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_BUYBACK_POOL
324
325
  });
325
326
 
326
- // Normalize the terminal token — use WETH for native.
327
- address normalizedTerminalToken = terminalToken == JBConstants.NATIVE_TOKEN ? address(WETH) : terminalToken;
328
-
329
- // Make sure this pool hasn't already been set for this project/token pair.
330
- if (_poolIsSet[projectId][normalizedTerminalToken]) {
331
- revert JBBuybackHook_PoolAlreadySet(_poolKeyOf[projectId][normalizedTerminalToken].toId());
332
- }
333
-
334
- // Make sure the provided TWAP window is within reasonable bounds.
335
- if (twapWindow < MIN_TWAP_WINDOW || twapWindow > MAX_TWAP_WINDOW) {
336
- revert JBBuybackHook_InvalidTwapWindow(twapWindow, MIN_TWAP_WINDOW, MAX_TWAP_WINDOW);
337
- }
338
-
339
- // Make sure the terminal token is not zero.
340
- if (normalizedTerminalToken == address(0)) revert JBBuybackHook_ZeroTerminalToken();
327
+ // Normalize the terminal token — use wrapped native token for native.
328
+ address normalizedTerminalToken =
329
+ terminalToken == JBConstants.NATIVE_TOKEN ? address(WRAPPED_NATIVE_TOKEN) : terminalToken;
341
330
 
342
331
  // Get the project's token.
343
332
  address projectToken = address(TOKENS.tokenOf(projectId));
344
333
 
345
- // Make sure the project has issued a token.
346
- if (projectToken == address(0)) revert JBBuybackHook_ZeroProjectToken();
347
-
348
- // Make sure the terminal token is not the project token.
349
- if (normalizedTerminalToken == projectToken) {
350
- revert JBBuybackHook_TerminalTokenIsProjectToken(normalizedTerminalToken, projectToken);
351
- }
352
-
353
- // Validate the pool is initialized in the PoolManager.
354
- PoolId poolId = poolKey.toId();
355
- // slither-disable-next-line unused-return
356
- (uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(poolId);
357
- if (sqrtPriceX96 == 0) revert JBBuybackHook_PoolNotInitialized(poolId);
334
+ _setPoolFor(projectId, poolKey, twapWindow, normalizedTerminalToken, projectToken);
335
+ }
358
336
 
359
- // Validate the PoolKey currencies match the project token and terminal token.
360
- address currency0 = Currency.unwrap(poolKey.currency0);
361
- address currency1 = Currency.unwrap(poolKey.currency1);
362
- bool validPair = (currency0 == projectToken && currency1 == normalizedTerminalToken)
363
- || (currency0 == normalizedTerminalToken && currency1 == projectToken);
364
- require(validPair, "JBBuybackHook: pool key currencies mismatch");
337
+ /// @notice Set the V4 pool to use for a given project and terminal token pair, constructing the PoolKey internally.
338
+ /// @dev Uses address(0) for the hooks field. The hook sorts the project token and terminal token into the correct
339
+ /// currency order. Pool keys are intentionally immutable once set.
340
+ /// @param projectId The ID of the project to set the pool for.
341
+ /// @param fee The Uniswap V4 pool fee tier.
342
+ /// @param tickSpacing The Uniswap V4 pool tick spacing.
343
+ /// @param twapWindow The period of time over which the TWAP is computed.
344
+ /// @param terminalToken The address of the terminal token that payments to the project are made in.
345
+ function setPoolFor(
346
+ uint256 projectId,
347
+ uint24 fee,
348
+ int24 tickSpacing,
349
+ uint256 twapWindow,
350
+ address terminalToken
351
+ )
352
+ external
353
+ override
354
+ {
355
+ // Enforce permissions.
356
+ _requirePermissionFrom({
357
+ account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_BUYBACK_POOL
358
+ });
365
359
 
366
- // Store the pool key and mark it as set.
367
- _poolKeyOf[projectId][normalizedTerminalToken] = poolKey;
368
- _poolIsSet[projectId][normalizedTerminalToken] = true;
360
+ // Normalize the terminal token use wrapped native token for native.
361
+ address normalizedTerminalToken =
362
+ terminalToken == JBConstants.NATIVE_TOKEN ? address(WRAPPED_NATIVE_TOKEN) : terminalToken;
369
363
 
370
- // Read the current TWAP window before overwriting (for accurate event emission).
371
- uint256 oldWindow = twapWindowOf[projectId];
364
+ // Get the project's token.
365
+ address projectToken = address(TOKENS.tokenOf(projectId));
372
366
 
373
- // Store the TWAP window and project token.
374
- twapWindowOf[projectId] = twapWindow;
375
- projectTokenOf[projectId] = projectToken;
367
+ // Sort currencies numerically (lower address = currency0).
368
+ (Currency currency0, Currency currency1) = normalizedTerminalToken < projectToken
369
+ ? (Currency.wrap(normalizedTerminalToken), Currency.wrap(projectToken))
370
+ : (Currency.wrap(projectToken), Currency.wrap(normalizedTerminalToken));
376
371
 
377
- emit TwapWindowChanged({
378
- projectId: projectId, oldWindow: oldWindow, newWindow: twapWindow, caller: _msgSender()
379
- });
380
- emit PoolAdded({
381
- projectId: projectId, terminalToken: normalizedTerminalToken, poolId: poolId, caller: _msgSender()
372
+ // Construct the pool key with no hooks.
373
+ PoolKey memory poolKey = PoolKey({
374
+ currency0: currency0, currency1: currency1, fee: fee, tickSpacing: tickSpacing, hooks: IHooks(address(0))
382
375
  });
376
+
377
+ _setPoolFor(projectId, poolKey, twapWindow, normalizedTerminalToken, projectToken);
383
378
  }
384
379
 
385
380
  /// @notice Change the TWAP window for a project.
@@ -476,7 +471,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
476
471
  return abi.encode(outputAmount);
477
472
  }
478
473
 
479
- /// @notice Receive native ETH. Required for V4 native ETH take() and WETH unwrap.
474
+ /// @notice Receive native tokens. Required for V4 native take() and wrapped token unwrap.
480
475
  receive() external payable {}
481
476
 
482
477
  //*********************************************************************//
@@ -560,7 +555,8 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
560
555
  address projectToken = projectTokenOf[context.projectId];
561
556
 
562
557
  // Keep a reference to the token being used by the terminal. Default to wETH if the terminal uses native.
563
- address terminalToken = context.amount.token == JBConstants.NATIVE_TOKEN ? address(WETH) : context.amount.token;
558
+ address terminalToken =
559
+ context.amount.token == JBConstants.NATIVE_TOKEN ? address(WRAPPED_NATIVE_TOKEN) : context.amount.token;
564
560
 
565
561
  // Always compute the TWAP-based minimum.
566
562
  uint256 twapMinimum = _getQuote({
@@ -607,7 +603,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
607
603
 
608
604
  /// @notice Returns the PoolKey for a given project and terminal token pair.
609
605
  /// @param projectId The ID of the project.
610
- /// @param terminalToken The terminal token address (normalized to WETH for native).
606
+ /// @param terminalToken The terminal token address (normalized to wrapped native token for native).
611
607
  /// @return key The V4 PoolKey.
612
608
  function poolKeyOf(uint256 projectId, address terminalToken) public view override returns (PoolKey memory key) {
613
609
  return _poolKeyOf[projectId][terminalToken];
@@ -623,6 +619,75 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
623
619
  // ---------------------- internal transactions ---------------------- //
624
620
  //*********************************************************************//
625
621
 
622
+ /// @notice Shared internal logic for setting a pool. Both `setPoolFor` overloads delegate here after permission
623
+ /// checks and token normalization.
624
+ /// @param projectId The ID of the project.
625
+ /// @param poolKey The V4 PoolKey identifying the pool.
626
+ /// @param twapWindow The period of time over which the TWAP is computed.
627
+ /// @param normalizedTerminalToken The terminal token address (already normalized: wrapped native token for native).
628
+ /// @param projectToken The project's ERC-20 token address.
629
+ function _setPoolFor(
630
+ uint256 projectId,
631
+ PoolKey memory poolKey,
632
+ uint256 twapWindow,
633
+ address normalizedTerminalToken,
634
+ address projectToken
635
+ )
636
+ internal
637
+ {
638
+ // Make sure this pool hasn't already been set for this project/token pair.
639
+ if (_poolIsSet[projectId][normalizedTerminalToken]) {
640
+ revert JBBuybackHook_PoolAlreadySet(_poolKeyOf[projectId][normalizedTerminalToken].toId());
641
+ }
642
+
643
+ // Make sure the provided TWAP window is within reasonable bounds.
644
+ if (twapWindow < MIN_TWAP_WINDOW || twapWindow > MAX_TWAP_WINDOW) {
645
+ revert JBBuybackHook_InvalidTwapWindow(twapWindow, MIN_TWAP_WINDOW, MAX_TWAP_WINDOW);
646
+ }
647
+
648
+ // Make sure the terminal token is not zero.
649
+ if (normalizedTerminalToken == address(0)) revert JBBuybackHook_ZeroTerminalToken();
650
+
651
+ // Make sure the project has issued a token.
652
+ if (projectToken == address(0)) revert JBBuybackHook_ZeroProjectToken();
653
+
654
+ // Make sure the terminal token is not the project token.
655
+ if (normalizedTerminalToken == projectToken) {
656
+ revert JBBuybackHook_TerminalTokenIsProjectToken(normalizedTerminalToken, projectToken);
657
+ }
658
+
659
+ // Validate the pool is initialized in the PoolManager.
660
+ PoolId poolId = poolKey.toId();
661
+ // slither-disable-next-line unused-return
662
+ (uint160 sqrtPriceX96,,,) = POOL_MANAGER.getSlot0(poolId);
663
+ if (sqrtPriceX96 == 0) revert JBBuybackHook_PoolNotInitialized(poolId);
664
+
665
+ // Validate the PoolKey currencies match the project token and terminal token.
666
+ address currency0 = Currency.unwrap(poolKey.currency0);
667
+ address currency1 = Currency.unwrap(poolKey.currency1);
668
+ bool validPair = (currency0 == projectToken && currency1 == normalizedTerminalToken)
669
+ || (currency0 == normalizedTerminalToken && currency1 == projectToken);
670
+ require(validPair, "JBBuybackHook: pool key currencies mismatch");
671
+
672
+ // Store the pool key and mark it as set.
673
+ _poolKeyOf[projectId][normalizedTerminalToken] = poolKey;
674
+ _poolIsSet[projectId][normalizedTerminalToken] = true;
675
+
676
+ // Read the current TWAP window before overwriting (for accurate event emission).
677
+ uint256 oldWindow = twapWindowOf[projectId];
678
+
679
+ // Store the TWAP window and project token.
680
+ twapWindowOf[projectId] = twapWindow;
681
+ projectTokenOf[projectId] = projectToken;
682
+
683
+ emit TwapWindowChanged({
684
+ projectId: projectId, oldWindow: oldWindow, newWindow: twapWindow, caller: _msgSender()
685
+ });
686
+ emit PoolAdded({
687
+ projectId: projectId, terminalToken: normalizedTerminalToken, poolId: poolId, caller: _msgSender()
688
+ });
689
+ }
690
+
626
691
  /// @notice Swap the terminal token to receive project tokens via V4.
627
692
  /// @param context The `afterPayRecordedContext` passed in by the terminal.
628
693
  /// @param projectTokenIs0 Whether the project token is currency0 in the pool.
@@ -641,21 +706,22 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
641
706
  {
642
707
  uint256 amountToSwapWith = context.forwardedAmount.value;
643
708
 
644
- // Get the terminal token, normalized to WETH.
645
- address terminalTokenWithWETH =
646
- context.forwardedAmount.token == JBConstants.NATIVE_TOKEN ? address(WETH) : context.forwardedAmount.token;
709
+ // Get the terminal token, normalized to the wrapped native token.
710
+ address terminalTokenWithWETH = context.forwardedAmount.token == JBConstants.NATIVE_TOKEN
711
+ ? address(WRAPPED_NATIVE_TOKEN)
712
+ : context.forwardedAmount.token;
647
713
 
648
714
  // Get the pool key for this project/token pair.
649
715
  PoolKey memory key = _poolKeyOf[context.projectId][terminalTokenWithWETH];
650
716
 
651
- // Wrap native tokens to WETH if needed (for ERC-20 settle path).
717
+ // Wrap native tokens if needed (for ERC-20 settle path).
652
718
  // For native ETH pools (currency0 or currency1 is address(0)), we use settle{value:} instead.
653
719
  bool inputIsNative = context.forwardedAmount.token == JBConstants.NATIVE_TOKEN;
654
720
  Currency inputCurrency = projectTokenIs0 ? key.currency1 : key.currency0;
655
721
 
656
722
  if (inputIsNative && !inputCurrency.isAddressZero()) {
657
- // Pool uses WETH, but we received ETH — wrap it.
658
- WETH.deposit{value: amountToSwapWith}();
723
+ // Pool uses the wrapped native token, but we received native — wrap it.
724
+ WRAPPED_NATIVE_TOKEN.deposit{value: amountToSwapWith}();
659
725
  }
660
726
 
661
727
  // Encode the callback data.
@@ -706,7 +772,7 @@ contract JBBuybackHook is JBPermissioned, ERC2771Context, IUnlockCallback, IJBBu
706
772
  /// @param projectId The ID of the project.
707
773
  /// @param projectToken The project token being swapped for.
708
774
  /// @param amountIn The number of terminal tokens being used to swap.
709
- /// @param terminalToken The terminal token being paid in (normalized to WETH for native).
775
+ /// @param terminalToken The terminal token being paid in (normalized to wrapped native token for native).
710
776
  /// @return amountOut The minimum number of tokens to receive based on the TWAP and slippage.
711
777
  function _getQuote(
712
778
  uint256 projectId,
@@ -16,6 +16,7 @@ import {ERC2771Context} from "@openzeppelin/contracts/metatx/ERC2771Context.sol"
16
16
  import {Context} from "@openzeppelin/contracts/utils/Context.sol";
17
17
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
18
18
 
19
+ import {IJBBuybackHook} from "./interfaces/IJBBuybackHook.sol";
19
20
  import {IJBBuybackHookRegistry} from "./interfaces/IJBBuybackHookRegistry.sol";
20
21
 
21
22
  contract JBBuybackHookRegistry is IJBBuybackHookRegistry, ERC2771Context, JBPermissioned, Ownable {
@@ -177,6 +178,42 @@ contract JBBuybackHookRegistry is IJBBuybackHookRegistry, ERC2771Context, JBPerm
177
178
  emit JBBuybackHookRegistry_SetHook(projectId, hook);
178
179
  }
179
180
 
181
+ /// @notice Set the Uniswap V4 pool for a project by forwarding to the resolved buyback hook implementation.
182
+ /// @param projectId The ID of the project to set the pool for.
183
+ /// @param fee The Uniswap V4 pool fee tier.
184
+ /// @param tickSpacing The Uniswap V4 pool tick spacing.
185
+ /// @param twapWindow The period of time over which the TWAP is computed.
186
+ /// @param terminalToken The address of the terminal token that payments to the project are made in.
187
+ function setPoolFor(
188
+ uint256 projectId,
189
+ uint24 fee,
190
+ int24 tickSpacing,
191
+ uint256 twapWindow,
192
+ address terminalToken
193
+ )
194
+ external
195
+ override
196
+ {
197
+ // Enforce permissions.
198
+ _requirePermissionFrom({
199
+ account: PROJECTS.ownerOf(projectId), projectId: projectId, permissionId: JBPermissionIds.SET_BUYBACK_POOL
200
+ });
201
+
202
+ // Get the hook for the project (falls back to default).
203
+ IJBRulesetDataHook hook = _hookOf[projectId];
204
+ if (hook == IJBRulesetDataHook(address(0))) hook = defaultHook;
205
+
206
+ // Forward the call to the resolved hook.
207
+ IJBBuybackHook(address(hook))
208
+ .setPoolFor({
209
+ projectId: projectId,
210
+ fee: fee,
211
+ tickSpacing: tickSpacing,
212
+ twapWindow: twapWindow,
213
+ terminalToken: terminalToken
214
+ });
215
+ }
216
+
180
217
  //*********************************************************************//
181
218
  // ------------------------- external views -------------------------- //
182
219
  //*********************************************************************//
@@ -81,9 +81,9 @@ interface IJBBuybackHook is IJBPayHook, IJBRulesetDataHook {
81
81
  /// @return The slippage denominator.
82
82
  function TWAP_SLIPPAGE_DENOMINATOR() external view returns (uint256);
83
83
 
84
- /// @notice The wETH contract.
85
- /// @return The WETH contract.
86
- function WETH() external view returns (IWETH9);
84
+ /// @notice The wrapped native token contract (e.g. WETH on Ethereum, WMATIC on Polygon).
85
+ /// @return The wrapped native token contract.
86
+ function WRAPPED_NATIVE_TOKEN() external view returns (IWETH9);
87
87
 
88
88
  /// @notice The PoolKey for a given project and terminal token pair.
89
89
  /// @param projectId The ID of the project.
@@ -114,6 +114,23 @@ interface IJBBuybackHook is IJBPayHook, IJBRulesetDataHook {
114
114
  )
115
115
  external;
116
116
 
117
+ /// @notice Set the pool to use for a given project and terminal token, constructing the PoolKey internally.
118
+ /// @dev Uses address(0) for the hooks field. The hook sorts the project token and terminal token into the correct
119
+ /// currency order.
120
+ /// @param projectId The ID of the project to set the pool for.
121
+ /// @param fee The Uniswap V4 pool fee tier.
122
+ /// @param tickSpacing The Uniswap V4 pool tick spacing.
123
+ /// @param twapWindow The period of time over which the TWAP is computed.
124
+ /// @param terminalToken The address of the terminal token that payments to the project are made in.
125
+ function setPoolFor(
126
+ uint256 projectId,
127
+ uint24 fee,
128
+ int24 tickSpacing,
129
+ uint256 twapWindow,
130
+ address terminalToken
131
+ )
132
+ external;
133
+
117
134
  /// @notice Change the TWAP window for a project.
118
135
  /// @param projectId The ID of the project to set the TWAP window of.
119
136
  /// @param newWindow The new TWAP window.
@@ -71,4 +71,20 @@ interface IJBBuybackHookRegistry is IJBRulesetDataHook {
71
71
  /// @param projectId The ID of the project to set the hook for.
72
72
  /// @param hook The hook to set.
73
73
  function setHookFor(uint256 projectId, IJBRulesetDataHook hook) external;
74
+
75
+ /// @notice Set the Uniswap V4 pool for a project, forwarding to the resolved buyback hook implementation.
76
+ /// @dev Resolves the hook for the project (or the default), then calls setPoolFor on it.
77
+ /// @param projectId The ID of the project to set the pool for.
78
+ /// @param fee The Uniswap V4 pool fee tier.
79
+ /// @param tickSpacing The Uniswap V4 pool tick spacing.
80
+ /// @param twapWindow The period of time over which the TWAP is computed.
81
+ /// @param terminalToken The address of the terminal token that payments to the project are made in.
82
+ function setPoolFor(
83
+ uint256 projectId,
84
+ uint24 fee,
85
+ int24 tickSpacing,
86
+ uint256 twapWindow,
87
+ address terminalToken
88
+ )
89
+ external;
74
90
  }
@@ -17,6 +17,7 @@ import "@bananapus/core-v6/src/libraries/JBConstants.sol";
17
17
  import "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
18
18
 
19
19
  import "src/JBBuybackHookRegistry.sol";
20
+ import {IJBBuybackHook} from "src/interfaces/IJBBuybackHook.sol";
20
21
 
21
22
  /// @notice Unit tests for `JBBuybackHookRegistry`.
22
23
  contract Test_BuybackHookRegistry_Unit is Test {
@@ -479,6 +480,133 @@ contract Test_BuybackHookRegistry_Unit is Test {
479
480
  registry.lockHookFor(projectId, hookB);
480
481
  }
481
482
 
483
+ //*********************************************************************//
484
+ // --- setPoolFor ---------------------------------------------------- //
485
+ //*********************************************************************//
486
+
487
+ function test_setPoolFor_forwardsToResolvedHook() public {
488
+ // Set hookA as default.
489
+ vm.prank(owner);
490
+ registry.setDefaultHook(hookA);
491
+
492
+ // Build the expected calldata for the simplified setPoolFor overload.
493
+ bytes memory expectedCalldata = abi.encodeWithSignature(
494
+ "setPoolFor(uint256,uint24,int24,uint256,address)",
495
+ projectId,
496
+ uint24(10_000),
497
+ int24(60),
498
+ uint256(2 days),
499
+ address(0xEEEe)
500
+ );
501
+
502
+ // Mock and expect the call on hookA.
503
+ vm.mockCall(address(hookA), expectedCalldata, abi.encode());
504
+ vm.expectCall(address(hookA), expectedCalldata);
505
+
506
+ registry.setPoolFor({
507
+ projectId: projectId, fee: 10_000, tickSpacing: 60, twapWindow: 2 days, terminalToken: address(0xEEEe)
508
+ });
509
+ }
510
+
511
+ function test_setPoolFor_usesProjectSpecificHook() public {
512
+ // Allow and set hookB for this project.
513
+ vm.prank(owner);
514
+ registry.allowHook(hookB);
515
+ vm.prank(projectOwner);
516
+ registry.setHookFor(projectId, hookB);
517
+
518
+ // Set a different default (hookA) to prove the project hook is used.
519
+ vm.prank(owner);
520
+ registry.setDefaultHook(hookA);
521
+
522
+ // Build the expected calldata.
523
+ bytes memory expectedCalldata = abi.encodeWithSignature(
524
+ "setPoolFor(uint256,uint24,int24,uint256,address)",
525
+ projectId,
526
+ uint24(3000),
527
+ int24(10),
528
+ uint256(1 days),
529
+ address(0xEEEe)
530
+ );
531
+
532
+ // Mock and expect the call goes to hookB, NOT hookA.
533
+ vm.mockCall(address(hookB), expectedCalldata, abi.encode());
534
+ vm.expectCall(address(hookB), expectedCalldata);
535
+
536
+ registry.setPoolFor({
537
+ projectId: projectId, fee: 3000, tickSpacing: 10, twapWindow: 1 days, terminalToken: address(0xEEEe)
538
+ });
539
+ }
540
+
541
+ function test_setPoolFor_revertsIfUnauthorized() public {
542
+ // Set hookA as default.
543
+ vm.prank(owner);
544
+ registry.setDefaultHook(hookA);
545
+
546
+ // Mock permissions to return false — caller has no SET_BUYBACK_POOL permission.
547
+ vm.mockCall(
548
+ address(permissions), abi.encodeWithSelector(IJBPermissions.hasPermission.selector), abi.encode(false)
549
+ );
550
+
551
+ vm.prank(dude);
552
+ vm.expectRevert();
553
+ registry.setPoolFor({
554
+ projectId: projectId, fee: 10_000, tickSpacing: 60, twapWindow: 2 days, terminalToken: address(0xEEEe)
555
+ });
556
+ }
557
+
558
+ function test_setPoolFor_succeedsForProjectOwner() public {
559
+ // Set hookA as default.
560
+ vm.prank(owner);
561
+ registry.setDefaultHook(hookA);
562
+
563
+ // Build expected calldata.
564
+ bytes memory expectedCalldata = abi.encodeWithSignature(
565
+ "setPoolFor(uint256,uint24,int24,uint256,address)",
566
+ projectId,
567
+ uint24(10_000),
568
+ int24(60),
569
+ uint256(2 days),
570
+ address(0xEEEe)
571
+ );
572
+
573
+ // Mock the forwarded call on hookA.
574
+ vm.mockCall(address(hookA), expectedCalldata, abi.encode());
575
+ vm.expectCall(address(hookA), expectedCalldata);
576
+
577
+ // Project owner should be able to call setPoolFor.
578
+ vm.prank(projectOwner);
579
+ registry.setPoolFor({
580
+ projectId: projectId, fee: 10_000, tickSpacing: 60, twapWindow: 2 days, terminalToken: address(0xEEEe)
581
+ });
582
+ }
583
+
584
+ function test_setPoolFor_succeedsForPermissionedOperator() public {
585
+ // Set hookA as default.
586
+ vm.prank(owner);
587
+ registry.setDefaultHook(hookA);
588
+
589
+ // Build expected calldata.
590
+ bytes memory expectedCalldata = abi.encodeWithSignature(
591
+ "setPoolFor(uint256,uint24,int24,uint256,address)",
592
+ projectId,
593
+ uint24(10_000),
594
+ int24(60),
595
+ uint256(2 days),
596
+ address(0xEEEe)
597
+ );
598
+
599
+ // Mock the forwarded call on hookA.
600
+ vm.mockCall(address(hookA), expectedCalldata, abi.encode());
601
+ vm.expectCall(address(hookA), expectedCalldata);
602
+
603
+ // Dude (not project owner) has permission via mock — should succeed.
604
+ vm.prank(dude);
605
+ registry.setPoolFor({
606
+ projectId: projectId, fee: 10_000, tickSpacing: 60, twapWindow: 2 days, terminalToken: address(0xEEEe)
607
+ });
608
+ }
609
+
482
610
  //*********************************************************************//
483
611
  // --- Helpers ------------------------------------------------------- //
484
612
  //*********************************************************************//
@@ -82,11 +82,13 @@ contract ForTest_V4BuybackHook is JBBuybackHook {
82
82
  IJBPrices prices,
83
83
  IJBProjects projects,
84
84
  IJBTokens tokens,
85
- IWETH9 weth,
85
+ IWETH9 wrappedNativeToken,
86
86
  IPoolManager poolManager,
87
87
  address trustedForwarder
88
88
  )
89
- JBBuybackHook(directory, permissions, prices, projects, tokens, weth, poolManager, trustedForwarder)
89
+ JBBuybackHook(
90
+ directory, permissions, prices, projects, tokens, wrappedNativeToken, poolManager, trustedForwarder
91
+ )
90
92
  {}
91
93
 
92
94
  /// @notice Directly initialize pool state for testing without going through setPoolFor permission checks.
@@ -179,7 +181,7 @@ contract V4BuybackHookTest is Test {
179
181
  prices: prices,
180
182
  projects: projects,
181
183
  tokens: tokens,
182
- weth: IWETH9(address(mockWeth)),
184
+ wrappedNativeToken: IWETH9(address(mockWeth)),
183
185
  poolManager: IPoolManager(address(mockPM)),
184
186
  trustedForwarder: address(0)
185
187
  });
@@ -152,11 +152,13 @@ contract ForTest_ForkBuybackHook is JBBuybackHook {
152
152
  IJBPrices prices,
153
153
  IJBProjects projects,
154
154
  IJBTokens tokens,
155
- IWETH9 weth,
155
+ IWETH9 wrappedNativeToken,
156
156
  IPoolManager poolManager,
157
157
  address trustedForwarder
158
158
  )
159
- JBBuybackHook(directory, permissions, prices, projects, tokens, weth, poolManager, trustedForwarder)
159
+ JBBuybackHook(
160
+ directory, permissions, prices, projects, tokens, wrappedNativeToken, poolManager, trustedForwarder
161
+ )
160
162
  {}
161
163
  }
162
164
 
@@ -253,7 +255,7 @@ contract V4ForkTest is Test {
253
255
  prices: prices,
254
256
  projects: projects,
255
257
  tokens: tokens,
256
- weth: weth,
258
+ wrappedNativeToken: weth,
257
259
  poolManager: poolManager,
258
260
  trustedForwarder: address(0)
259
261
  });
@@ -227,11 +227,13 @@ contract ForTest_SandwichBuybackHook is JBBuybackHook {
227
227
  IJBPrices prices,
228
228
  IJBProjects projects,
229
229
  IJBTokens tokens,
230
- IWETH9 weth,
230
+ IWETH9 wrappedNativeToken,
231
231
  IPoolManager poolManager,
232
232
  address trustedForwarder
233
233
  )
234
- JBBuybackHook(directory, permissions, prices, projects, tokens, weth, poolManager, trustedForwarder)
234
+ JBBuybackHook(
235
+ directory, permissions, prices, projects, tokens, wrappedNativeToken, poolManager, trustedForwarder
236
+ )
235
237
  {}
236
238
  }
237
239
 
@@ -321,7 +323,7 @@ contract V4SandwichForkTest is Test {
321
323
  prices: prices,
322
324
  projects: projects,
323
325
  tokens: tokens,
324
- weth: weth,
326
+ wrappedNativeToken: weth,
325
327
  poolManager: poolManager,
326
328
  trustedForwarder: address(0)
327
329
  });
@@ -85,11 +85,13 @@ contract L44_ForTest_BuybackHook is JBBuybackHook {
85
85
  IJBPrices prices,
86
86
  IJBProjects projects,
87
87
  IJBTokens tokens,
88
- IWETH9 weth,
88
+ IWETH9 wrappedNativeToken,
89
89
  IPoolManager poolManager,
90
90
  address trustedForwarder
91
91
  )
92
- JBBuybackHook(directory, permissions, prices, projects, tokens, weth, poolManager, trustedForwarder)
92
+ JBBuybackHook(
93
+ directory, permissions, prices, projects, tokens, wrappedNativeToken, poolManager, trustedForwarder
94
+ )
93
95
  {}
94
96
 
95
97
  function ForTest_initPool(
@@ -158,7 +160,7 @@ contract L44_BalanceDeltaLeftover is Test {
158
160
  prices: prices,
159
161
  projects: projects,
160
162
  tokens: tokens,
161
- weth: IWETH9(address(mockWeth)),
163
+ wrappedNativeToken: IWETH9(address(mockWeth)),
162
164
  poolManager: IPoolManager(address(mockPM)),
163
165
  trustedForwarder: address(0)
164
166
  });
@@ -72,7 +72,7 @@ contract L45_OldWindowEvent is Test {
72
72
  prices: prices,
73
73
  projects: projects,
74
74
  tokens: tokens,
75
- weth: IWETH9(address(wethToken)),
75
+ wrappedNativeToken: IWETH9(address(wethToken)),
76
76
  poolManager: IPoolManager(address(mockPM)),
77
77
  trustedForwarder: address(0)
78
78
  });
@@ -76,11 +76,13 @@ contract M34_ForTest_BuybackHook is JBBuybackHook {
76
76
  IJBPrices prices,
77
77
  IJBProjects projects,
78
78
  IJBTokens tokens,
79
- IWETH9 weth,
79
+ IWETH9 wrappedNativeToken,
80
80
  IPoolManager poolManager,
81
81
  address trustedForwarder
82
82
  )
83
- JBBuybackHook(directory, permissions, prices, projects, tokens, weth, poolManager, trustedForwarder)
83
+ JBBuybackHook(
84
+ directory, permissions, prices, projects, tokens, wrappedNativeToken, poolManager, trustedForwarder
85
+ )
84
86
  {}
85
87
 
86
88
  function ForTest_initPool(
@@ -147,7 +149,7 @@ contract M34_SwapFailureMintFallback is Test {
147
149
  prices: prices,
148
150
  projects: projects,
149
151
  tokens: tokens,
150
- weth: IWETH9(address(mockWeth)),
152
+ wrappedNativeToken: IWETH9(address(mockWeth)),
151
153
  poolManager: IPoolManager(address(mockPM)),
152
154
  trustedForwarder: address(0)
153
155
  });