@rev-net/core-v6 0.0.14 → 0.0.16

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 (78) hide show
  1. package/ADMINISTRATION.md +5 -1
  2. package/ARCHITECTURE.md +69 -11
  3. package/AUDIT_INSTRUCTIONS.md +90 -7
  4. package/CHANGE_LOG.md +16 -3
  5. package/README.md +32 -7
  6. package/RISKS.md +26 -14
  7. package/SKILLS.md +168 -46
  8. package/STYLE_GUIDE.md +1 -1
  9. package/USER_JOURNEYS.md +20 -6
  10. package/foundry.toml +7 -0
  11. package/package.json +9 -10
  12. package/script/Deploy.s.sol +80 -16
  13. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  14. package/src/REVDeployer.sol +73 -21
  15. package/src/REVLoans.sol +27 -6
  16. package/test/REV.integrations.t.sol +1 -1
  17. package/test/REVAutoIssuanceFuzz.t.sol +1 -1
  18. package/test/REVDeployerRegressions.t.sol +7 -4
  19. package/test/REVInvincibility.t.sol +7 -19
  20. package/test/REVInvincibilityHandler.sol +1 -1
  21. package/test/REVLifecycle.t.sol +1 -1
  22. package/test/REVLoans.invariants.t.sol +1 -1
  23. package/test/REVLoansAttacks.t.sol +20 -12
  24. package/test/REVLoansFeeRecovery.t.sol +20 -12
  25. package/test/REVLoansFindings.t.sol +20 -12
  26. package/test/REVLoansRegressions.t.sol +20 -12
  27. package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
  28. package/test/REVLoansSourced.t.sol +1 -9
  29. package/test/REVLoansUnSourced.t.sol +1 -1
  30. package/test/TestBurnHeldTokens.t.sol +1 -1
  31. package/test/TestCEIPattern.t.sol +1 -1
  32. package/test/TestCashOutCallerValidation.t.sol +75 -1
  33. package/test/TestConversionDocumentation.t.sol +1 -1
  34. package/test/TestCrossCurrencyReclaim.t.sol +1 -1
  35. package/test/TestCrossSourceReallocation.t.sol +1 -1
  36. package/test/TestERC2771MetaTx.t.sol +1 -1
  37. package/test/TestEmptyBuybackSpecs.t.sol +1 -1
  38. package/test/TestFlashLoanSurplus.t.sol +1 -1
  39. package/test/TestHookArrayOOB.t.sol +1 -1
  40. package/test/TestLiquidationBehavior.t.sol +1 -1
  41. package/test/TestLoanSourceRotation.t.sol +1 -1
  42. package/test/TestLongTailEconomics.t.sol +1 -1
  43. package/test/TestLowFindings.t.sol +4 -2
  44. package/test/TestMixedFixes.t.sol +7 -5
  45. package/test/TestPermit2Signatures.t.sol +1 -1
  46. package/test/TestReallocationSandwich.t.sol +1 -1
  47. package/test/TestRevnetRegressions.t.sol +1 -1
  48. package/test/TestSplitWeightAdjustment.t.sol +11 -6
  49. package/test/TestSplitWeightE2E.t.sol +1 -1
  50. package/test/TestSplitWeightFork.t.sol +9 -10
  51. package/test/TestStageTransitionBorrowable.t.sol +1 -1
  52. package/test/TestSwapTerminalPermission.t.sol +1 -1
  53. package/test/TestUint112Overflow.t.sol +1 -1
  54. package/test/TestZeroRepayment.t.sol +1 -1
  55. package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
  56. package/test/fork/ForkTestBase.sol +8 -11
  57. package/test/fork/TestAutoIssuanceFork.t.sol +148 -0
  58. package/test/fork/TestCashOutFork.t.sol +23 -22
  59. package/test/fork/TestIssuanceDecayFork.t.sol +158 -0
  60. package/test/fork/TestLoanBorrowFork.t.sol +1 -1
  61. package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
  62. package/test/fork/TestLoanERC20Fork.t.sol +463 -0
  63. package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
  64. package/test/fork/TestLoanReallocateFork.t.sol +1 -1
  65. package/test/fork/TestLoanRepayFork.t.sol +3 -3
  66. package/test/fork/TestLoanTransferFork.t.sol +1 -1
  67. package/test/fork/TestPermit2PaymentFork.t.sol +299 -0
  68. package/test/fork/TestSplitWeightFork.t.sol +1 -1
  69. package/test/helpers/MaliciousContracts.sol +37 -23
  70. package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
  71. package/test/mock/MockBuybackDataHook.sol +51 -7
  72. package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
  73. package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
  74. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
  77. package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
  78. package/test/regression/TestZeroPriceFeed.t.sol +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -756,14 +756,6 @@ contract REVLoansSourcedTests is TestBaseWorkflow {
756
756
 
757
757
  uint256 balanceBefore = _balanceOf(token, USER);
758
758
 
759
- // Ensure that the hook was called.
760
- vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.beforeCashOutRecordedWith.selector));
761
-
762
- // It only adds itself as a `after` cashoutHook if there is a cashout tax rate.
763
- if (cashOutTaxRate > 0) {
764
- vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.afterCashOutRecordedWith.selector));
765
- }
766
-
767
759
  // Perform a cashout.
768
760
  vm.prank(USER);
769
761
  jbMultiTerminal().cashOutTokensOf(USER, revnetProjectId, tokensToCashout, token, 0, payable(USER), bytes(""));
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -22,8 +22,12 @@ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
22
22
  import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
23
23
  // forge-lint: disable-next-line(unaliased-plain-import)
24
24
  import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
25
+ import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
25
26
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
26
27
  import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
28
+ import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
29
+ import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
30
+ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
27
31
  import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
28
32
  import {REVLoans} from "../src/REVLoans.sol";
29
33
  import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
@@ -302,6 +306,76 @@ contract TestCashOutCallerValidation is TestBaseWorkflow {
302
306
  assertGt(feeBalanceAfter, feeBalanceBefore, "Fee project balance should increase from cash out fee");
303
307
  }
304
308
 
309
+ /// @notice Revnet cash-out fees and buyback sell-side specs are composed together.
310
+ function test_beforeCashOutRecordedWith_proxiesIntoBuybackAndAppendsFeeSpec() public {
311
+ bytes memory buybackMetadata = abi.encode(uint256(123));
312
+ MOCK_BUYBACK.configureCashOutResult({
313
+ cashOutTaxRate: JBConstants.MAX_CASH_OUT_TAX_RATE,
314
+ cashOutCount: 0,
315
+ totalSupply: 0,
316
+ hookAmount: 0,
317
+ hookMetadata: buybackMetadata
318
+ });
319
+
320
+ JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
321
+ terminal: address(jbMultiTerminal()),
322
+ holder: USER,
323
+ projectId: REVNET_ID,
324
+ rulesetId: 0,
325
+ cashOutCount: 1000,
326
+ totalSupply: 10_000,
327
+ surplus: JBTokenAmount({
328
+ token: JBConstants.NATIVE_TOKEN,
329
+ value: 100 ether,
330
+ decimals: 18,
331
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
332
+ }),
333
+ useTotalSurplus: true,
334
+ cashOutTaxRate: 3000,
335
+ beneficiaryIsFeeless: false,
336
+ metadata: ""
337
+ });
338
+
339
+ (
340
+ uint256 cashOutTaxRate,
341
+ uint256 cashOutCount,
342
+ uint256 totalSupply,
343
+ JBCashOutHookSpecification[] memory hookSpecifications
344
+ ) = REV_DEPLOYER.beforeCashOutRecordedWith(context);
345
+
346
+ uint256 feeCashOutCount = context.cashOutCount * REV_DEPLOYER.FEE() / JBConstants.MAX_FEE;
347
+ uint256 nonFeeCashOutCount = context.cashOutCount - feeCashOutCount;
348
+ uint256 postFeeReclaimedAmount = JBCashOuts.cashOutFrom({
349
+ surplus: context.surplus.value,
350
+ cashOutCount: nonFeeCashOutCount,
351
+ totalSupply: context.totalSupply,
352
+ cashOutTaxRate: context.cashOutTaxRate
353
+ });
354
+ uint256 feeAmount = JBCashOuts.cashOutFrom({
355
+ surplus: context.surplus.value - postFeeReclaimedAmount,
356
+ cashOutCount: feeCashOutCount,
357
+ totalSupply: context.totalSupply - nonFeeCashOutCount,
358
+ cashOutTaxRate: context.cashOutTaxRate
359
+ });
360
+
361
+ assertEq(cashOutTaxRate, JBConstants.MAX_CASH_OUT_TAX_RATE, "Buyback cash out tax rate should be forwarded");
362
+ assertEq(cashOutCount, nonFeeCashOutCount, "Buyback should receive the non-fee cash out count");
363
+ assertEq(totalSupply, context.totalSupply, "Total supply should pass through");
364
+ assertEq(hookSpecifications.length, 2, "Buyback spec and revnet fee spec should both be returned");
365
+
366
+ assertEq(address(hookSpecifications[0].hook), address(MOCK_BUYBACK), "First hook spec should come from buyback");
367
+ assertEq(hookSpecifications[0].amount, 0, "Buyback sell-side spec should preserve its forwarded amount");
368
+ assertEq(hookSpecifications[0].metadata, buybackMetadata, "Buyback metadata should be preserved");
369
+
370
+ assertEq(address(hookSpecifications[1].hook), address(REV_DEPLOYER), "Second hook spec should charge fee");
371
+ assertEq(hookSpecifications[1].amount, feeAmount, "Fee spec amount should match the revnet fee math");
372
+ assertEq(
373
+ hookSpecifications[1].metadata,
374
+ abi.encode(jbMultiTerminal()),
375
+ "Fee spec metadata should encode the fee terminal"
376
+ );
377
+ }
378
+
305
379
  /// @notice Test that afterCashOutRecordedWith has no access control — anyone can call it.
306
380
  /// A non-terminal caller would just be donating their own funds as fees.
307
381
  function test_nonTerminalCaller_justDonatesOwnFunds() public {
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -627,7 +627,9 @@ contract TestLowFindings is TestBaseWorkflow {
627
627
  assertEq(borrowable, 0, "Borrowable amount for 1 wei of collateral should be 0");
628
628
 
629
629
  // Mock the BURN permission (permission ID 11) for the loans contract.
630
- mockExpect(
630
+ // Use vm.mockCall only (not mockExpect which also adds vm.expectCall) because
631
+ // borrowFrom reverts with REVLoans_ZeroBorrowAmount before the permission check is reached.
632
+ vm.mockCall(
631
633
  address(jbPermissions()),
632
634
  abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
633
635
  abi.encode(true)
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -240,14 +240,16 @@ contract TestMixedFixes is TestBaseWorkflow {
240
240
 
241
241
  REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
242
242
 
243
- // Warp to exactly LOAN_LIQUIDATION_DURATION after creation
244
- vm.warp(loan.createdAt + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION());
243
+ // Warp to one second past LOAN_LIQUIDATION_DURATION after creation.
244
+ // The contract uses `>` (not `>=`) so the exact boundary is still repayable;
245
+ // we need to exceed the boundary by 1 second to trigger the revert.
246
+ vm.warp(loan.createdAt + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1);
245
247
 
246
- // With the >= fix, this should revert because timeSinceLoanCreated == LOAN_LIQUIDATION_DURATION
248
+ // timeSinceLoanCreated > LOAN_LIQUIDATION_DURATION revert
247
249
  vm.expectRevert(
248
250
  abi.encodeWithSelector(
249
251
  REVLoans.REVLoans_LoanExpired.selector,
250
- LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION(),
252
+ LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1,
251
253
  LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION()
252
254
  )
253
255
  );
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -225,7 +225,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
225
225
 
226
226
  // Mock 721 hook returning 0.3 ETH split on 1 ETH payment.
227
227
  JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
228
- hookSpecs[0] = JBPayHookSpecification({hook: IJBPayHook(mock721), amount: 0.3 ether, metadata: bytes("")});
228
+ hookSpecs[0] =
229
+ JBPayHookSpecification({hook: IJBPayHook(mock721), noop: false, amount: 0.3 ether, metadata: bytes("")});
229
230
  vm.mockCall(
230
231
  mock721,
231
232
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
@@ -267,7 +268,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
267
268
 
268
269
  // Mock 721 hook returning full 1 ETH split.
269
270
  JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
270
- hookSpecs[0] = JBPayHookSpecification({hook: IJBPayHook(mock721), amount: 1 ether, metadata: bytes("")});
271
+ hookSpecs[0] =
272
+ JBPayHookSpecification({hook: IJBPayHook(mock721), noop: false, amount: 1 ether, metadata: bytes("")});
271
273
  vm.mockCall(
272
274
  mock721,
273
275
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
@@ -351,7 +353,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
351
353
 
352
354
  // Mock 721 hook returning 0.4 ETH split on 1 ETH payment.
353
355
  JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
354
- hookSpecs[0] = JBPayHookSpecification({hook: IJBPayHook(mock721), amount: 0.4 ether, metadata: bytes("")});
356
+ hookSpecs[0] =
357
+ JBPayHookSpecification({hook: IJBPayHook(mock721), noop: false, amount: 0.4 ether, metadata: bytes("")});
355
358
  vm.mockCall(
356
359
  mock721,
357
360
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
@@ -400,7 +403,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
400
403
 
401
404
  // Mock 721 hook returning 0.2 ETH split.
402
405
  JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
403
- hookSpecs[0] = JBPayHookSpecification({hook: IJBPayHook(mock721), amount: 0.2 ether, metadata: bytes("")});
406
+ hookSpecs[0] =
407
+ JBPayHookSpecification({hook: IJBPayHook(mock721), noop: false, amount: 0.2 ether, metadata: bytes("")});
404
408
  vm.mockCall(
405
409
  mock721,
406
410
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
@@ -449,7 +453,8 @@ contract TestSplitWeightAdjustment is TestBaseWorkflow {
449
453
  // Mock 721 hook returning split amount with metadata.
450
454
  bytes memory splitMeta = abi.encode(uint256(42));
451
455
  JBPayHookSpecification[] memory hookSpecs = new JBPayHookSpecification[](1);
452
- hookSpecs[0] = JBPayHookSpecification({hook: IJBPayHook(mock721), amount: 0.5 ether, metadata: splitMeta});
456
+ hookSpecs[0] =
457
+ JBPayHookSpecification({hook: IJBPayHook(mock721), noop: false, amount: 0.5 ether, metadata: splitMeta});
453
458
  vm.mockCall(
454
459
  mock721,
455
460
  abi.encodeWithSelector(IJBRulesetDataHook.beforePayRecordedWith.selector),
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -495,13 +495,12 @@ contract TestSplitWeightFork is TestBaseWorkflow {
495
495
  hooks: IHooks(address(0))
496
496
  });
497
497
 
498
- // Pool is already initialized at 1:1 price by REVDeployer during deployment.
499
- // Just add liquidity and mock the oracle.
500
-
501
- // Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
502
- // deal() skips ERC20Votes checkpoints, causing underflow when tokens are burned.
498
+ // Pool is already initialized at fair issuance price by REVDeployer during deployment.
499
+ // At high tick (~69078 for 1000 tokens/ETH), full-range liquidity needs ~32x more project tokens than ETH.
500
+ // Mint 50x project tokens and use a smaller liquidity delta to stay within budget.
501
+ // Use mintFor (not deal) so ERC20Votes checkpoints are updated.
503
502
  vm.prank(address(jbController()));
504
- jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
503
+ jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount * 50);
505
504
  // Fund with ETH for the native currency side.
506
505
  vm.deal(address(liqHelper), liquidityTokenAmount);
507
506
 
@@ -512,12 +511,12 @@ contract TestSplitWeightFork is TestBaseWorkflow {
512
511
 
513
512
  // Add full-range liquidity.
514
513
  // forge-lint: disable-next-line(unsafe-typecast)
515
- int256 liquidityDelta = int256(liquidityTokenAmount / 2);
514
+ int256 liquidityDelta = int256(liquidityTokenAmount / 50);
516
515
  vm.prank(address(liqHelper));
517
516
  liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
518
517
 
519
- // Mock the oracle at address(0) for hookless pools.
520
- _mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
518
+ // Mock geomean oracle at tick 69078 (~1000 tokens/ETH, matching INITIAL_ISSUANCE).
519
+ _mockOracle(liquidityDelta, 69_078, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
521
520
  }
522
521
 
523
522
  /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";