@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 +11 -0
- package/package.json +1 -1
- package/src/JBBuybackHook.sol +133 -67
- package/src/JBBuybackHookRegistry.sol +37 -0
- package/src/interfaces/IJBBuybackHook.sol +20 -3
- package/src/interfaces/IJBBuybackHookRegistry.sol +16 -0
- package/test/Registry.t.sol +128 -0
- package/test/V4BuybackHook.t.sol +5 -3
- package/test/fork/V4ForkTest.t.sol +5 -3
- package/test/fork/V4SandwichForkTest.t.sol +5 -3
- package/test/regression/L44_BalanceDeltaLeftover.t.sol +5 -3
- package/test/regression/L45_OldWindowEvent.t.sol +1 -1
- package/test/regression/M34_SwapFailureMintFallback.t.sol +5 -3
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
package/src/JBBuybackHook.sol
CHANGED
|
@@ -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
|
|
112
|
-
IWETH9 public immutable override
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
235
|
-
// back to
|
|
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
|
|
238
|
-
if (
|
|
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
|
|
327
|
-
address normalizedTerminalToken =
|
|
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
|
-
|
|
346
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
//
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
//
|
|
371
|
-
|
|
364
|
+
// Get the project's token.
|
|
365
|
+
address projectToken = address(TOKENS.tokenOf(projectId));
|
|
372
366
|
|
|
373
|
-
//
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
645
|
-
address terminalTokenWithWETH =
|
|
646
|
-
|
|
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
|
|
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
|
|
658
|
-
|
|
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
|
|
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
|
|
85
|
-
/// @return The
|
|
86
|
-
function
|
|
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
|
}
|
package/test/Registry.t.sol
CHANGED
|
@@ -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
|
//*********************************************************************//
|
package/test/V4BuybackHook.t.sol
CHANGED
|
@@ -82,11 +82,13 @@ contract ForTest_V4BuybackHook is JBBuybackHook {
|
|
|
82
82
|
IJBPrices prices,
|
|
83
83
|
IJBProjects projects,
|
|
84
84
|
IJBTokens tokens,
|
|
85
|
-
IWETH9
|
|
85
|
+
IWETH9 wrappedNativeToken,
|
|
86
86
|
IPoolManager poolManager,
|
|
87
87
|
address trustedForwarder
|
|
88
88
|
)
|
|
89
|
-
JBBuybackHook(
|
|
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
|
-
|
|
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
|
|
155
|
+
IWETH9 wrappedNativeToken,
|
|
156
156
|
IPoolManager poolManager,
|
|
157
157
|
address trustedForwarder
|
|
158
158
|
)
|
|
159
|
-
JBBuybackHook(
|
|
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
|
-
|
|
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
|
|
230
|
+
IWETH9 wrappedNativeToken,
|
|
231
231
|
IPoolManager poolManager,
|
|
232
232
|
address trustedForwarder
|
|
233
233
|
)
|
|
234
|
-
JBBuybackHook(
|
|
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
|
-
|
|
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
|
|
88
|
+
IWETH9 wrappedNativeToken,
|
|
89
89
|
IPoolManager poolManager,
|
|
90
90
|
address trustedForwarder
|
|
91
91
|
)
|
|
92
|
-
JBBuybackHook(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
79
|
+
IWETH9 wrappedNativeToken,
|
|
80
80
|
IPoolManager poolManager,
|
|
81
81
|
address trustedForwarder
|
|
82
82
|
)
|
|
83
|
-
JBBuybackHook(
|
|
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
|
-
|
|
152
|
+
wrappedNativeToken: IWETH9(address(mockWeth)),
|
|
151
153
|
poolManager: IPoolManager(address(mockPM)),
|
|
152
154
|
trustedForwarder: address(0)
|
|
153
155
|
});
|