@rev-net/core-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.
Files changed (46) hide show
  1. package/ARCHITECTURE.md +1 -1
  2. package/README.md +4 -4
  3. package/SKILLS.md +2 -8
  4. package/STYLE_GUIDE.md +127 -51
  5. package/docs/src/README.md +2 -2
  6. package/foundry.toml +3 -0
  7. package/package.json +12 -9
  8. package/remappings.txt +1 -1
  9. package/script/Deploy.s.sol +1 -1
  10. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  11. package/src/REVDeployer.sol +30 -26
  12. package/src/REVLoans.sol +1 -0
  13. package/test/{REVDeployerAuditRegressions.t.sol → REVDeployerRegressions.t.sol} +1 -1
  14. package/test/REVInvincibility.t.sol +15 -19
  15. package/test/REVLifecycle.t.sol +0 -1
  16. package/test/REVLoansAttacks.t.sol +3 -7
  17. package/test/REVLoansFeeRecovery.t.sol +0 -2
  18. package/test/{REVLoans_AuditFindings.t.sol → REVLoansFindings.t.sol} +6 -6
  19. package/test/{REVLoansAuditRegressions.t.sol → REVLoansRegressions.t.sol} +2 -2
  20. package/test/REVLoansSourced.t.sol +3 -1
  21. package/test/{TestPR26_BurnHeldTokens.t.sol → TestBurnHeldTokens.t.sol} +1 -1
  22. package/test/{TestPR27_CEIPattern.t.sol → TestCEIPattern.t.sol} +3 -3
  23. package/test/{TestPR15_CashOutCallerValidation.t.sol → TestCashOutCallerValidation.t.sol} +1 -3
  24. package/test/{TestPR09_ConversionDocumentation.t.sol → TestConversionDocumentation.t.sol} +1 -1
  25. package/test/{TestPR13_CrossSourceReallocation.t.sol → TestCrossSourceReallocation.t.sol} +1 -1
  26. package/test/{TestPR12_FlashLoanSurplus.t.sol → TestFlashLoanSurplus.t.sol} +1 -1
  27. package/test/{TestPR22_HookArrayOOB.t.sol → TestHookArrayOOB.t.sol} +1 -1
  28. package/test/{TestPR10_LiquidationBehavior.t.sol → TestLiquidationBehavior.t.sol} +4 -4
  29. package/test/{TestPR11_LowFindings.t.sol → TestLowFindings.t.sol} +1 -1
  30. package/test/{TestPR32_MixedFixes.t.sol → TestMixedFixes.t.sol} +1 -1
  31. package/test/TestSplitWeightFork.t.sol +118 -159
  32. package/test/{TestPR29_SwapTerminalPermission.t.sol → TestSwapTerminalPermission.t.sol} +1 -1
  33. package/test/{TestPR21_Uint112Overflow.t.sol → TestUint112Overflow.t.sol} +4 -4
  34. package/test/{TestPR16_ZeroRepayment.t.sol → TestZeroRepayment.t.sol} +4 -6
  35. package/test/fork/ForkTestBase.sol +83 -51
  36. package/test/fork/TestCashOutFork.t.sol +12 -11
  37. package/test/fork/TestLoanBorrowFork.t.sol +10 -12
  38. package/test/fork/TestLoanCrossRulesetFork.t.sol +300 -0
  39. package/test/fork/TestLoanLiquidationFork.t.sol +13 -8
  40. package/test/fork/TestLoanReallocateFork.t.sol +21 -12
  41. package/test/fork/TestLoanRepayFork.t.sol +17 -14
  42. package/test/fork/TestSplitWeightFork.t.sol +34 -34
  43. package/test/mock/MockBuybackDataHook.sol +4 -7
  44. package/test/mock/MockBuybackDataHookMintPath.sol +5 -8
  45. package/test/regression/{TestI20_CumulativeLoanCounter.t.sol → TestCumulativeLoanCounter.t.sol} +4 -4
  46. package/test/regression/{TestL27_LiquidateGapHandling.t.sol → TestLiquidateGapHandling.t.sol} +3 -3
@@ -41,7 +41,6 @@ import {REVCroptopAllowedPost} from "../src/structs/REVCroptopAllowedPost.sol";
41
41
  import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
42
42
  import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
43
43
  import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
44
- import {IWETH9} from "@bananapus/buyback-hook-v6/src/interfaces/external/IWETH9.sol";
45
44
  import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
46
45
 
47
46
  // Uniswap V4
@@ -52,16 +51,21 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
52
51
  import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
53
52
  import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
54
53
  import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
55
- import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
54
+ import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
56
55
  import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
57
56
  import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
58
57
 
59
58
  import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
60
59
 
61
- /// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
60
+ /// @notice Helper that adds liquidity to and swaps on a V4 pool via the unlock/callback pattern.
62
61
  contract LiquidityHelper is IUnlockCallback {
63
62
  IPoolManager public immutable poolManager;
64
63
 
64
+ enum Action {
65
+ ADD_LIQUIDITY,
66
+ SWAP
67
+ }
68
+
65
69
  struct AddLiqParams {
66
70
  PoolKey key;
67
71
  int24 tickLower;
@@ -69,6 +73,13 @@ contract LiquidityHelper is IUnlockCallback {
69
73
  int256 liquidityDelta;
70
74
  }
71
75
 
76
+ struct DoSwapParams {
77
+ PoolKey key;
78
+ bool zeroForOne;
79
+ int256 amountSpecified;
80
+ uint160 sqrtPriceLimitX96;
81
+ }
82
+
72
83
  constructor(IPoolManager _poolManager) {
73
84
  poolManager = _poolManager;
74
85
  }
@@ -82,13 +93,38 @@ contract LiquidityHelper is IUnlockCallback {
82
93
  external
83
94
  payable
84
95
  {
85
- bytes memory data = abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta));
96
+ bytes memory data =
97
+ abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
98
+ poolManager.unlock(data);
99
+ }
100
+
101
+ function swap(
102
+ PoolKey calldata key,
103
+ bool zeroForOne,
104
+ int256 amountSpecified,
105
+ uint160 sqrtPriceLimitX96
106
+ )
107
+ external
108
+ payable
109
+ {
110
+ bytes memory data =
111
+ abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
86
112
  poolManager.unlock(data);
87
113
  }
88
114
 
89
115
  function unlockCallback(bytes calldata data) external override returns (bytes memory) {
90
116
  require(msg.sender == address(poolManager), "only PM");
91
117
 
118
+ (Action action, bytes memory inner) = abi.decode(data, (Action, bytes));
119
+
120
+ if (action == Action.ADD_LIQUIDITY) {
121
+ return _handleAddLiquidity(inner);
122
+ } else {
123
+ return _handleSwap(inner);
124
+ }
125
+ }
126
+
127
+ function _handleAddLiquidity(bytes memory data) internal returns (bytes memory) {
92
128
  AddLiqParams memory params = abi.decode(data, (AddLiqParams));
93
129
 
94
130
  (BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
@@ -110,6 +146,28 @@ contract LiquidityHelper is IUnlockCallback {
110
146
  return abi.encode(callerDelta);
111
147
  }
112
148
 
149
+ function _handleSwap(bytes memory data) internal returns (bytes memory) {
150
+ DoSwapParams memory params = abi.decode(data, (DoSwapParams));
151
+
152
+ BalanceDelta delta = poolManager.swap(
153
+ params.key, SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96), ""
154
+ );
155
+
156
+ // Settle (pay) what we owe, take what we're owed.
157
+ if (delta.amount0() < 0) {
158
+ _settleIfNegative(params.key.currency0, delta.amount0());
159
+ } else {
160
+ _takeIfPositive(params.key.currency0, delta.amount0());
161
+ }
162
+ if (delta.amount1() < 0) {
163
+ _settleIfNegative(params.key.currency1, delta.amount1());
164
+ } else {
165
+ _takeIfPositive(params.key.currency1, delta.amount1());
166
+ }
167
+
168
+ return abi.encode(delta);
169
+ }
170
+
113
171
  function _settleIfNegative(Currency currency, int128 delta) internal {
114
172
  if (delta >= 0) return;
115
173
  uint256 amount = uint256(uint128(-delta));
@@ -148,11 +206,10 @@ contract TestSplitWeightFork is TestBaseWorkflow {
148
206
  // ─────────────────────────
149
207
 
150
208
  address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
151
- address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
152
209
 
153
- /// @notice Full-range tick bounds for tickSpacing = 60.
154
- int24 constant TICK_LOWER = -887_220;
155
- int24 constant TICK_UPPER = 887_220;
210
+ /// @notice Full-range tick bounds for tickSpacing = 200.
211
+ int24 constant TICK_LOWER = -887_200;
212
+ int24 constant TICK_UPPER = 887_200;
156
213
 
157
214
  // ───────────────────────── State
158
215
  // ─────────────────────────
@@ -168,7 +225,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
168
225
  IJBSuckerRegistry SUCKER_REGISTRY;
169
226
  CTPublisher PUBLISHER;
170
227
  IPoolManager poolManager;
171
- IWETH9 weth;
172
228
  LiquidityHelper liqHelper;
173
229
 
174
230
  uint256 FEE_PROJECT_ID;
@@ -186,13 +242,8 @@ contract TestSplitWeightFork is TestBaseWorkflow {
186
242
  // ─────────────────────────
187
243
 
188
244
  function setUp() public override {
189
- // Fork mainnet first we need the real V4 PoolManager.
190
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
191
- if (bytes(rpcUrl).length == 0) {
192
- vm.skip(true);
193
- return;
194
- }
195
- vm.createSelectFork(rpcUrl);
245
+ // Fork mainnet at a stable block deterministic and post-V4 deployment.
246
+ vm.createSelectFork("ethereum", 21_700_000);
196
247
 
197
248
  // Verify V4 PoolManager is deployed.
198
249
  require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
@@ -201,7 +252,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
201
252
  super.setUp();
202
253
 
203
254
  poolManager = IPoolManager(POOL_MANAGER_ADDR);
204
- weth = IWETH9(WETH_ADDR);
205
255
  liqHelper = new LiquidityHelper(poolManager);
206
256
 
207
257
  FEE_PROJECT_ID = jbProjects().createFor(multisig());
@@ -221,8 +271,8 @@ contract TestSplitWeightFork is TestBaseWorkflow {
221
271
  jbPrices(),
222
272
  jbProjects(),
223
273
  jbTokens(),
224
- weth,
225
274
  poolManager,
275
+ IHooks(address(0)), // oracleHook
226
276
  address(0) // trustedForwarder
227
277
  );
228
278
 
@@ -262,12 +312,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
262
312
  vm.deal(PAYER, 100 ether);
263
313
  }
264
314
 
265
- modifier onlyFork() {
266
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
267
- if (bytes(rpcUrl).length == 0) return;
268
- _;
269
- }
270
-
271
315
  // ───────────────────────── Helpers
272
316
  // ─────────────────────────
273
317
 
@@ -401,68 +445,44 @@ contract TestSplitWeightFork is TestBaseWorkflow {
401
445
  });
402
446
  }
403
447
 
404
- /// @notice Set up a V4 pool for the revnet's project token / WETH pair and register it with the buyback hook.
448
+ /// @notice Set up a V4 pool for the revnet's project token / native ETH pair and register it with the buyback hook.
405
449
  function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
406
450
  // Get the project token.
407
451
  address projectToken = address(jbTokens().tokenOf(revnetId));
408
452
  require(projectToken != address(0), "project token not deployed");
409
453
 
410
- // Build sorted pool key.
411
- address token0;
412
- address token1;
413
- if (projectToken < WETH_ADDR) {
414
- token0 = projectToken;
415
- token1 = WETH_ADDR;
416
- } else {
417
- token0 = WETH_ADDR;
418
- token1 = projectToken;
419
- }
420
-
454
+ // Native ETH is represented as address(0) in V4 pool keys.
455
+ // address(0) is always less than any deployed token address.
421
456
  key = PoolKey({
422
- currency0: Currency.wrap(token0),
423
- currency1: Currency.wrap(token1),
457
+ currency0: Currency.wrap(address(0)),
458
+ currency1: Currency.wrap(projectToken),
424
459
  fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
425
460
  tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
426
461
  hooks: IHooks(address(0))
427
462
  });
428
463
 
429
- // Initialize pool at price = 1.0 (tick 0).
430
- uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(0);
431
- poolManager.initialize(key, sqrtPrice);
464
+ // Pool is already initialized at 1:1 price by REVDeployer during deployment.
465
+ // Just add liquidity and mock the oracle.
432
466
 
433
467
  // Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
434
468
  // deal() skips ERC20Votes checkpoints, causing underflow when tokens are burned.
435
469
  vm.prank(address(jbController()));
436
470
  jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
471
+ // Fund with ETH for the native currency side.
437
472
  vm.deal(address(liqHelper), liquidityTokenAmount);
438
- vm.prank(address(liqHelper));
439
- IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
440
473
 
441
- // Approve PoolManager to spend tokens from LiquidityHelper.
474
+ // Approve PoolManager to spend project tokens from LiquidityHelper.
442
475
  vm.startPrank(address(liqHelper));
443
476
  IERC20(projectToken).approve(address(poolManager), type(uint256).max);
444
- IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
445
477
  vm.stopPrank();
446
478
 
447
479
  // Add full-range liquidity.
448
480
  int256 liquidityDelta = int256(liquidityTokenAmount / 2);
449
481
  vm.prank(address(liqHelper));
450
- liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
482
+ liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
451
483
 
452
484
  // Mock the oracle at address(0) for hookless pools.
453
- // The buyback hook calls IGeomeanOracle(address(key.hooks)).observe() for TWAP.
454
- // Since hooks = address(0), we need code there + a mock response.
455
- // tick=0 means 1:1 price → TWAP says pool rate is ~1 token/WETH → minting wins.
456
485
  _mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
457
-
458
- // Cache immutables before prank (vm.prank only applies to the next call).
459
- uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
460
-
461
- // Register pool with buyback hook via split operator (multisig has SET_BUYBACK_POOL permission).
462
- vm.prank(multisig());
463
- BUYBACK_HOOK.setPoolFor({
464
- projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
465
- });
466
486
  }
467
487
 
468
488
  /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
@@ -478,11 +498,11 @@ contract TestSplitWeightFork is TestBaseWorkflow {
478
498
  // arithmeticMeanTick = (tickCumulatives[1] - tickCumulatives[0]) / twapWindow = tick
479
499
  tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
480
500
 
481
- uint160[] memory secondsPerLiquidityCumulativeX128s = new uint160[](2);
501
+ uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
482
502
  secondsPerLiquidityCumulativeX128s[0] = 0;
483
503
  uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
484
504
  if (liq == 0) liq = 1;
485
- secondsPerLiquidityCumulativeX128s[1] = uint160((uint256(twapWindow) << 128) / liq);
505
+ secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
486
506
 
487
507
  vm.mockCall(
488
508
  address(0),
@@ -523,7 +543,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
523
543
  }
524
544
 
525
545
  /// @notice Build payment metadata with only 721 tier selection (no quote → TWAP/spot fallback).
526
- function _buildPayMetadataNoQuote(address hookMetadataTarget) internal view returns (bytes memory) {
546
+ function _buildPayMetadataNoQuote(address hookMetadataTarget) internal pure returns (bytes memory) {
527
547
  uint16[] memory tierIds = new uint16[](1);
528
548
  tierIds[0] = 1;
529
549
  bytes memory tierData = abi.encode(true, tierIds);
@@ -543,122 +563,64 @@ contract TestSplitWeightFork is TestBaseWorkflow {
543
563
  /// @notice SWAP PATH: Pool offers good rate → buyback hook swaps on AMM instead of minting.
544
564
  /// With 30% tier split, the buyback should swap with 0.7 ETH worth.
545
565
  /// Terminal mints 0 tokens (weight=0), buyback hook mints via controller after swap.
546
- function test_fork_swapPath_splitWithBuyback() public onlyFork {
566
+ function test_fork_swapPath_splitWithBuyback() public {
547
567
  (uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
548
568
 
549
- // Set up pool with deep liquidity at 1:1 price (pool offers ~1 token per WETH).
550
- // The issuance rate is 1000 tokens/ETH, so the pool rate (~1 token/WETH) is much worse.
551
- // Wait — at 1:1 pool price, 1 WETH gets ~1 token. Minting gets 1000 tokens.
552
- // So minting is better → buyback will NOT swap.
553
- // To make swap win, we need the pool to offer MORE tokens per WETH than minting.
554
- // Minting rate = 1000 tokens/ETH (before split reduction, the buyback sees reduced weight).
555
- //
556
- // After REVDeployer scales weight: weight = 1000e18 * 0.7e18 / 1e18 = 700e18
557
- // The buyback hook receives weight=700e18 and amount=0.7 ETH.
558
- // tokenCountWithoutHook = mulDiv(0.7e18, 700e18, 1e18) = 490 tokens.
559
- //
560
- // Wait, that's wrong. Let me re-trace:
561
- // REVDeployer.beforePayRecordedWith:
562
- // 1. 721 hook returns splitAmount=0.3 ETH → projectAmount = 0.7 ETH
563
- // 2. buybackHookContext.amount.value = 0.7 ETH, weight = context.weight = 1000e18
564
- // 3. Buyback hook sees: amountToSwapWith = 0.7 ETH, weight = 1000e18
565
- // tokenCountWithoutHook = mulDiv(0.7e18, 1000e18, 1e18) = 700 tokens
566
- // 4. If pool offers > 700 tokens for 0.7 WETH → swap wins
567
- // 5. If pool offers < 700 tokens → mint wins
568
- //
569
- // At 1:1 pool price, 0.7 WETH gets ~0.7 tokens (after fees). That's way less than 700.
570
- // We need a pool priced so projectToken is CHEAP — e.g., 1 WETH = 2000 tokens.
571
- //
572
- // Let's create a pool at a tick where projectToken is very cheap.
573
- // tick = -69_000 gives approximately 1 WETH = 1000 tokens. We want more than 700 for 0.7 WETH.
574
- // Actually, let's just seed the pool with lots of project tokens and little WETH.
575
- // This naturally makes project tokens cheaper.
576
-
577
- // Instead of tick manipulation, let's just use a pool at tick 0 (1:1) but seed asymmetrically:
578
- // Lots of project tokens, little WETH → effective price favors the buyer.
579
- // Actually V4 pool price is set at initialization (sqrtPriceX96), seeding doesn't change the tick.
580
- //
581
- // Let's initialize at a tick where 1 WETH = many project tokens.
582
- // For swap to win: pool must give > 700 tokens for 0.7 WETH.
583
- // Rate needed: > 1000 tokens/WETH.
584
- // Use tick = -69082 which gives ~1:1000 ratio (1 WETH ≈ 1000 tokens).
585
- // With 0.3% fee and slippage, it might give ~997, which is still > 700. Swap wins.
569
+ // We need to initialize the pool and get the price to favor buying project tokens: > 1000 tokens/ETH.
570
+ // Strategy: initialize pool, add liquidity, then swap project tokens for ETH to move the tick.
586
571
 
587
572
  address projectToken = address(jbTokens().tokenOf(revnetId));
588
573
  require(projectToken != address(0), "project token not deployed");
589
574
 
590
- bool projectTokenIs0 = projectToken < WETH_ADDR;
591
-
592
- // Build sorted pool key.
593
- address token0 = projectTokenIs0 ? projectToken : WETH_ADDR;
594
- address token1 = projectTokenIs0 ? WETH_ADDR : projectToken;
595
-
575
+ // Native ETH is address(0), always less than any deployed token.
596
576
  PoolKey memory key = PoolKey({
597
- currency0: Currency.wrap(token0),
598
- currency1: Currency.wrap(token1),
577
+ currency0: Currency.wrap(address(0)),
578
+ currency1: Currency.wrap(projectToken),
599
579
  fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
600
580
  tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
601
581
  hooks: IHooks(address(0))
602
582
  });
603
583
 
604
- // Set initial tick so that 1 WETH = ~2000 project tokens.
605
- // If projectToken is token0: price = token1/token0 = WETH/projectToken.
606
- // We want projectToken cheap → WETH/projectToken high → tick positive.
607
- // tick ~= 76_000 → price ~= 2000.
608
- // If WETH is token0: price = token1/token0 = projectToken/WETH.
609
- // We want projectToken/WETH high → tick positive.
610
- // tick ~= 76_000 → price ~= 2000.
611
- // Either way: positive tick ≈ 2000 of token1 per token0.
612
- //
613
- // But we want "1 WETH = 2000 projectTokens".
614
- // If projectToken is token0: price = WETH per projectToken = 1/2000 → negative tick.
615
- // tick ≈ -76_000.
616
- // If WETH is token0: price = projectToken per WETH = 2000 → positive tick.
617
- // tick ≈ 76_000.
618
- int24 initTick;
619
- if (projectTokenIs0) {
620
- // price = WETH/projectToken = 0.0005 → tick ≈ -76_000
621
- initTick = -76_020; // Rounded to tickSpacing=60
622
- } else {
623
- // price = projectToken/WETH = 2000 → tick ≈ 76_000
624
- initTick = 76_020;
625
- }
626
-
627
- uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(initTick);
628
- poolManager.initialize(key, sqrtPrice);
584
+ // Pool is already initialized at 1:1 price by REVDeployer during deployment.
629
585
 
630
586
  // Seed liquidity. We need both tokens.
631
587
  // IMPORTANT: Use JBTokens.mintFor (not deal) so ERC20Votes checkpoints are updated.
632
- // deal() only sets balanceOf/totalSupply but skips Votes checkpoints, causing burn underflow.
633
- uint256 projectLiq = 10_000_000e18; // lots of project tokens
634
- uint256 wethLiq = 5000e18; // some WETH
588
+ uint256 projectLiq = 10_000_000e18;
589
+ uint256 ethLiq = 5000e18;
635
590
 
636
591
  vm.prank(address(jbController()));
637
592
  jbTokens().mintFor(address(liqHelper), revnetId, projectLiq);
638
- vm.deal(address(liqHelper), wethLiq);
639
- vm.prank(address(liqHelper));
640
- IWETH9(WETH_ADDR).deposit{value: wethLiq}();
593
+ vm.deal(address(liqHelper), ethLiq);
641
594
 
642
595
  vm.startPrank(address(liqHelper));
643
596
  IERC20(projectToken).approve(address(poolManager), type(uint256).max);
644
- IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
645
597
  vm.stopPrank();
646
598
 
647
- // Add full-range liquidity.
648
- int256 liquidityDelta = int256(wethLiq / 4); // Use fraction for liquidity units
599
+ // Add full-range liquidity at tick 0 (1:1 price).
600
+ int256 liquidityDelta = int256(ethLiq / 4);
649
601
  vm.prank(address(liqHelper));
650
- liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
602
+ liqHelper.addLiquidity{value: ethLiq}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
651
603
 
652
- // Mock the oracle at address(0) to report the actual pool price (initTick).
653
- // This makes the TWAP quote reflect ~2000 tokens/WETH, so the swap path wins.
654
- _mockOracle(liquidityDelta, initTick, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
604
+ // Swap a large amount of project tokens for ETH to move the price.
605
+ // This makes project tokens cheaper (more tokens per ETH) so the swap path wins.
606
+ uint256 swapAmount = 5_000_000e18;
607
+ vm.prank(address(jbController()));
608
+ jbTokens().mintFor(address(liqHelper), revnetId, swapAmount);
655
609
 
656
- // Register pool with buyback hook.
657
- uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
658
- vm.prank(multisig());
659
- BUYBACK_HOOK.setPoolFor({
660
- projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
661
- });
610
+ // currency0 is native ETH (address(0)), currency1 is projectToken.
611
+ // To sell projectToken for ETH (making project tokens cheaper), swap 1->0 (zeroForOne = false).
612
+ // zeroForOne=false pushes sqrtPrice up (more projectTokens per ETH).
613
+ bool zeroForOne = false;
614
+ uint160 sqrtPriceLimit = TickMath.getSqrtPriceAtTick(76_000);
615
+
616
+ vm.prank(address(liqHelper));
617
+ liqHelper.swap(key, zeroForOne, -int256(swapAmount), sqrtPriceLimit);
618
+
619
+ // Read the post-swap tick for the oracle mock.
620
+ (, int24 postSwapTick,,) = poolManager.getSlot0(key.toId());
621
+
622
+ // Mock the TWAP oracle to report the post-swap tick (so buyback hook sees the real price).
623
+ _mockOracle(liquidityDelta, postSwapTick, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
662
624
 
663
625
  // Build metadata: mint tier 1 + quote for swap.
664
626
  // The quote tells buyback to swap with the full amount, expecting at least 1 token out.
@@ -669,9 +631,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
669
631
  minimumSwapAmountOut: 1 // Accept any amount from swap
670
632
  });
671
633
 
672
- // Record payer balance before.
673
- uint256 payerBalBefore = jbTokens().totalBalanceOf(PAYER, revnetId);
674
-
675
634
  // Pay 1 ETH through the terminal.
676
635
  vm.prank(PAYER);
677
636
  uint256 terminalTokensReturned = jbMultiTerminal().pay{value: 1 ether}({
@@ -701,11 +660,11 @@ contract TestSplitWeightFork is TestBaseWorkflow {
701
660
  /// @notice MINT PATH: Pool offers bad rate → buyback decides minting is better.
702
661
  /// With 30% tier split, REVDeployer scales weight from 1000e18 to 700e18.
703
662
  /// Terminal mints 700 tokens.
704
- function test_fork_mintPath_splitWithBuyback() public onlyFork {
663
+ function test_fork_mintPath_splitWithBuyback() public {
705
664
  (uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
706
665
 
707
666
  // Set up pool with 1:1 price. At this price:
708
- // 0.7 WETH → ~0.7 tokens from pool (after fees).
667
+ // 0.7 ETH → ~0.7 tokens from pool (after fees).
709
668
  // Direct minting: 700 tokens.
710
669
  // Minting wins by a huge margin → buyback returns context.weight unchanged.
711
670
  _setupPool(revnetId, 10_000 ether);
@@ -713,7 +672,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
713
672
  // Build metadata: mint tier 1 + quote for "swap" with 0.7 ETH, but expect many tokens (forces mint path).
714
673
  // When minimumSwapAmountOut > actual pool output, the buyback hook falls back to minting.
715
674
  // Actually the buyback hook uses max(payerQuote, twapQuote). If we set minimumSwapAmountOut=0,
716
- // it'll use the TWAP/spot quote. At 1:1 pool price, spot says ~0.7 tokens for 0.7 WETH.
675
+ // it'll use the TWAP/spot quote. At 1:1 pool price, spot says ~0.7 tokens for 0.7 ETH.
717
676
  // tokenCountWithoutHook = 700 tokens. 700 > ~0.7 → mint wins.
718
677
  // We don't even need quote metadata — the spot fallback handles it.
719
678
  address metadataTarget = hook.METADATA_ID_TARGET();
@@ -742,7 +701,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
742
701
  }
743
702
 
744
703
  /// @notice MINT PATH without splits: baseline confirming 1000 tokens for 1 ETH.
745
- function test_fork_mintPath_noSplits_fullTokens() public onlyFork {
704
+ function test_fork_mintPath_noSplits_fullTokens() public {
746
705
  (uint256 revnetId,) = _deployRevnetWith721();
747
706
  _setupPool(revnetId, 10_000 ether);
748
707
 
@@ -764,7 +723,7 @@ contract TestSplitWeightFork is TestBaseWorkflow {
764
723
  }
765
724
 
766
725
  /// @notice Invariant: tokens / projectAmount rate is identical with and without splits.
767
- function test_fork_invariant_tokenPerEthConsistent() public onlyFork {
726
+ function test_fork_invariant_tokenPerEthConsistent() public {
768
727
  // --- Revnet 1: with 721 splits (30%) ---
769
728
  (uint256 revnetId1, IJB721TiersHook hook1) = _deployRevnetWith721();
770
729
  _setupPool(revnetId1, 10_000 ether);
@@ -30,7 +30,7 @@ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/
30
30
 
31
31
  /// @notice Tests for default operator permissions including SET_ROUTER_TERMINAL.
32
32
  /// Verifies that all default split operator permissions are granted correctly.
33
- contract TestPR29_SwapTerminalPermission is TestBaseWorkflow {
33
+ contract TestSwapTerminalPermission is TestBaseWorkflow {
34
34
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
35
35
 
36
36
  REVDeployer REV_DEPLOYER;
@@ -31,9 +31,9 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
31
31
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
32
32
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
33
33
 
34
- /// @title TestPR21_Uint112Overflow
34
+ /// @title TestUint112Overflow
35
35
  /// @notice Tests for uint112 truncation fix in REVLoans._adjust()
36
- contract TestPR21_Uint112Overflow is TestBaseWorkflow {
36
+ contract TestUint112Overflow is TestBaseWorkflow {
37
37
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
38
38
  bytes32 ERC20_SALT = "REV_TOKEN";
39
39
 
@@ -233,7 +233,7 @@ contract TestPR21_Uint112Overflow is TestBaseWorkflow {
233
233
  }
234
234
 
235
235
  /// @notice Verify that uint112.max exactly does NOT revert (boundary test).
236
- function test_boundaryValue_exactlyUint112Max() public {
236
+ function test_boundaryValue_exactlyUint112Max() public pure {
237
237
  // uint112.max is the boundary — the check is `>`, not `>=`
238
238
  // So exactly uint112.max should NOT revert
239
239
  uint256 maxVal = type(uint112).max;
@@ -243,7 +243,7 @@ contract TestPR21_Uint112Overflow is TestBaseWorkflow {
243
243
 
244
244
  /// @notice Verify the overflow check exists: values > uint112.max are rejected.
245
245
  /// @dev We verify this by checking the error selector exists on the contract.
246
- function test_overflowRevert_errorExists() public view {
246
+ function test_overflowRevert_errorExists() public pure {
247
247
  // The fix adds REVLoans_OverflowAlert error. Verify the error exists
248
248
  // by encoding it. If this compiles, the error exists.
249
249
  bytes4 selector = REVLoans.REVLoans_OverflowAlert.selector;
@@ -32,7 +32,7 @@ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressReg
32
32
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
33
33
 
34
34
  /// @notice Tests for PR #16: zero repayment prevention.
35
- contract TestPR16_ZeroRepayment is TestBaseWorkflow {
35
+ contract TestZeroRepayment is TestBaseWorkflow {
36
36
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
37
37
 
38
38
  REVDeployer REV_DEPLOYER;
@@ -212,8 +212,6 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
212
212
  (uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
213
213
  require(loanId != 0, "Loan setup failed");
214
214
 
215
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
216
-
217
215
  // After borrowing, fees reduced surplus. We need to restore it so newBorrowAmount >= loan.amount.
218
216
  // Donate enough surplus to compensate for all fees extracted during borrowing.
219
217
  // A large donation ensures newBorrowAmount > loan.amount, hitting
@@ -243,7 +241,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
243
241
  /// @notice Repaying full borrow amount should succeed (returning all collateral).
244
242
  function test_repayNonZeroAmount_succeeds() public {
245
243
  // Setup: borrow against 10 ETH
246
- (uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, 10e18, 25);
244
+ (uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
247
245
  require(loanId != 0, "Loan setup failed");
248
246
 
249
247
  REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
@@ -271,7 +269,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
271
269
  /// @notice Repaying some collateral (non-zero) should succeed.
272
270
  function test_repayNonZeroCollateral_succeeds() public {
273
271
  // Setup: borrow against 10 ETH
274
- (uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, 10e18, 25);
272
+ (uint256 loanId,,) = _setupLoan(USER, 10e18, 25);
275
273
  require(loanId != 0, "Loan setup failed");
276
274
 
277
275
  REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
@@ -283,7 +281,7 @@ contract TestPR16_ZeroRepayment is TestBaseWorkflow {
283
281
  JBSingleAllowance memory allowance;
284
282
 
285
283
  vm.prank(USER);
286
- (uint256 paidOffLoanId, REVLoan memory paidOffLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount}(
284
+ (, REVLoan memory paidOffLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount}(
287
285
  loanId,
288
286
  loan.amount, // maxRepayBorrowAmount — generous cap
289
287
  collateralToReturn,