@rev-net/core-v6 0.0.9 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -42,7 +42,6 @@ import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol
42
42
  import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
43
43
  import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
44
44
  import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
45
- import {IWETH9} from "@bananapus/buyback-hook-v6/src/interfaces/external/IWETH9.sol";
46
45
  import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
47
46
 
48
47
  // Uniswap V4
@@ -53,7 +52,7 @@ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
53
52
  import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
54
53
  import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
55
54
  import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
56
- import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
55
+ import {ModifyLiquidityParams, SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
57
56
  import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
58
57
  import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
59
58
 
@@ -63,6 +62,11 @@ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
63
62
  contract LiquidityHelper is IUnlockCallback {
64
63
  IPoolManager public immutable poolManager;
65
64
 
65
+ enum Action {
66
+ ADD_LIQUIDITY,
67
+ SWAP
68
+ }
69
+
66
70
  struct AddLiqParams {
67
71
  PoolKey key;
68
72
  int24 tickLower;
@@ -70,6 +74,13 @@ contract LiquidityHelper is IUnlockCallback {
70
74
  int256 liquidityDelta;
71
75
  }
72
76
 
77
+ struct DoSwapParams {
78
+ PoolKey key;
79
+ bool zeroForOne;
80
+ int256 amountSpecified;
81
+ uint160 sqrtPriceLimitX96;
82
+ }
83
+
73
84
  constructor(IPoolManager _poolManager) {
74
85
  poolManager = _poolManager;
75
86
  }
@@ -83,13 +94,38 @@ contract LiquidityHelper is IUnlockCallback {
83
94
  external
84
95
  payable
85
96
  {
86
- bytes memory data = abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta));
97
+ bytes memory data =
98
+ abi.encode(Action.ADD_LIQUIDITY, abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta)));
99
+ poolManager.unlock(data);
100
+ }
101
+
102
+ function swap(
103
+ PoolKey calldata key,
104
+ bool zeroForOne,
105
+ int256 amountSpecified,
106
+ uint160 sqrtPriceLimitX96
107
+ )
108
+ external
109
+ payable
110
+ {
111
+ bytes memory data =
112
+ abi.encode(Action.SWAP, abi.encode(DoSwapParams(key, zeroForOne, amountSpecified, sqrtPriceLimitX96)));
87
113
  poolManager.unlock(data);
88
114
  }
89
115
 
90
116
  function unlockCallback(bytes calldata data) external override returns (bytes memory) {
91
117
  require(msg.sender == address(poolManager), "only PM");
92
118
 
119
+ (Action action, bytes memory inner) = abi.decode(data, (Action, bytes));
120
+
121
+ if (action == Action.ADD_LIQUIDITY) {
122
+ return _handleAddLiquidity(inner);
123
+ } else {
124
+ return _handleSwap(inner);
125
+ }
126
+ }
127
+
128
+ function _handleAddLiquidity(bytes memory data) internal returns (bytes memory) {
93
129
  AddLiqParams memory params = abi.decode(data, (AddLiqParams));
94
130
 
95
131
  (BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
@@ -111,6 +147,27 @@ contract LiquidityHelper is IUnlockCallback {
111
147
  return abi.encode(callerDelta);
112
148
  }
113
149
 
150
+ function _handleSwap(bytes memory data) internal returns (bytes memory) {
151
+ DoSwapParams memory params = abi.decode(data, (DoSwapParams));
152
+
153
+ BalanceDelta delta = poolManager.swap(
154
+ params.key, SwapParams(params.zeroForOne, params.amountSpecified, params.sqrtPriceLimitX96), ""
155
+ );
156
+
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
+
114
171
  function _settleIfNegative(Currency currency, int128 delta) internal {
115
172
  if (delta >= 0) return;
116
173
  uint256 amount = uint256(uint128(-delta));
@@ -146,11 +203,10 @@ abstract contract ForkTestBase is TestBaseWorkflow {
146
203
  // ─────────────────────────
147
204
 
148
205
  address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
149
- address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
150
206
 
151
207
  /// @notice Full-range tick bounds for tickSpacing = 60.
152
- int24 constant TICK_LOWER = -887_220;
153
- int24 constant TICK_UPPER = 887_220;
208
+ int24 constant TICK_LOWER = -887_200;
209
+ int24 constant TICK_UPPER = 887_200;
154
210
 
155
211
  // ───────────────────────── State
156
212
  // ─────────────────────────
@@ -166,7 +222,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
166
222
  IJBSuckerRegistry SUCKER_REGISTRY;
167
223
  CTPublisher PUBLISHER;
168
224
  IPoolManager poolManager;
169
- IWETH9 weth;
170
225
  LiquidityHelper liqHelper;
171
226
 
172
227
  uint256 FEE_PROJECT_ID;
@@ -185,13 +240,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
185
240
  // ─────────────────────────
186
241
 
187
242
  function setUp() public virtual override {
188
- // Fork mainnet first we need the real V4 PoolManager.
189
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
190
- if (bytes(rpcUrl).length == 0) {
191
- vm.skip(true);
192
- return;
193
- }
194
- vm.createSelectFork(rpcUrl);
243
+ // Fork mainnet at a stable block deterministic and post-V4 deployment.
244
+ vm.createSelectFork("ethereum", 21_700_000);
195
245
 
196
246
  // Verify V4 PoolManager is deployed.
197
247
  require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
@@ -200,7 +250,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
200
250
  super.setUp();
201
251
 
202
252
  poolManager = IPoolManager(POOL_MANAGER_ADDR);
203
- weth = IWETH9(WETH_ADDR);
204
253
  liqHelper = new LiquidityHelper(poolManager);
205
254
 
206
255
  FEE_PROJECT_ID = jbProjects().createFor(multisig());
@@ -220,8 +269,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
220
269
  jbPrices(),
221
270
  jbProjects(),
222
271
  jbTokens(),
223
- weth,
224
272
  poolManager,
273
+ IHooks(address(0)), // oracleHook
225
274
  address(0) // trustedForwarder
226
275
  );
227
276
 
@@ -262,12 +311,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
262
311
  vm.deal(BORROWER, 100 ether);
263
312
  }
264
313
 
265
- modifier onlyFork() {
266
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
267
- if (bytes(rpcUrl).length == 0) return;
268
- _;
269
- }
270
-
271
314
  // ───────────────────────── Config Helpers
272
315
  // ─────────────────────────
273
316
 
@@ -403,6 +446,8 @@ abstract contract ForkTestBase is TestBaseWorkflow {
403
446
  function _deployRevnetWith721(uint16 cashOutTaxRate) internal returns (uint256 revnetId, IJB721TiersHook hook) {
404
447
  (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
405
448
  _buildMinimalConfig(cashOutTaxRate);
449
+ // Use a different salt to avoid CREATE2 collision with _deployRevnet's ERC-20.
450
+ cfg.description.salt = "FORK_721_SALT";
406
451
  REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
407
452
 
408
453
  (revnetId, hook) = REV_DEPLOYER.deployWith721sFor({
@@ -418,55 +463,42 @@ abstract contract ForkTestBase is TestBaseWorkflow {
418
463
  // ───────────────────────── Pool Helpers
419
464
  // ─────────────────────────
420
465
 
421
- /// @notice Set up a V4 pool for the revnet's project token / WETH pair at 1:1 price.
466
+ /// @notice Set up a V4 pool for the revnet's project token / native ETH pair at 1:1 price.
422
467
  function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
423
468
  address projectToken = address(jbTokens().tokenOf(revnetId));
424
469
  require(projectToken != address(0), "project token not deployed");
425
470
 
426
- address token0;
427
- address token1;
428
- if (projectToken < WETH_ADDR) {
429
- token0 = projectToken;
430
- token1 = WETH_ADDR;
431
- } else {
432
- token0 = WETH_ADDR;
433
- token1 = projectToken;
434
- }
435
-
471
+ // Native ETH is represented as address(0) in V4 pool keys.
472
+ // address(0) is always less than any deployed token address.
436
473
  key = PoolKey({
437
- currency0: Currency.wrap(token0),
438
- currency1: Currency.wrap(token1),
474
+ currency0: Currency.wrap(address(0)),
475
+ currency1: Currency.wrap(projectToken),
439
476
  fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
440
477
  tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
441
478
  hooks: IHooks(address(0))
442
479
  });
443
480
 
444
- uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(0);
445
- poolManager.initialize(key, sqrtPrice);
481
+ // Pool is already initialized at 1:1 price by REVDeployer during deployment.
482
+ // Just add liquidity and mock the oracle.
483
+
484
+ // At 1:1 price, full-range liquidity needs equal amounts of both tokens.
485
+ uint256 projectTokenAmount = liquidityTokenAmount;
446
486
 
447
487
  // Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
448
488
  vm.prank(address(jbController()));
449
- jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
489
+ jbTokens().mintFor(address(liqHelper), revnetId, projectTokenAmount);
490
+ // Fund with ETH for the native currency side.
450
491
  vm.deal(address(liqHelper), liquidityTokenAmount);
451
- vm.prank(address(liqHelper));
452
- IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
453
492
 
454
493
  vm.startPrank(address(liqHelper));
455
494
  IERC20(projectToken).approve(address(poolManager), type(uint256).max);
456
- IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
457
495
  vm.stopPrank();
458
496
 
459
497
  int256 liquidityDelta = int256(liquidityTokenAmount / 2);
460
498
  vm.prank(address(liqHelper));
461
- liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
499
+ liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
462
500
 
463
501
  _mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
464
-
465
- uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
466
- vm.prank(multisig());
467
- BUYBACK_HOOK.setPoolFor({
468
- projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
469
- });
470
502
  }
471
503
 
472
504
  /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
@@ -477,11 +509,11 @@ abstract contract ForkTestBase is TestBaseWorkflow {
477
509
  tickCumulatives[0] = 0;
478
510
  tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
479
511
 
480
- uint160[] memory secondsPerLiquidityCumulativeX128s = new uint160[](2);
512
+ uint136[] memory secondsPerLiquidityCumulativeX128s = new uint136[](2);
481
513
  secondsPerLiquidityCumulativeX128s[0] = 0;
482
514
  uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
483
515
  if (liq == 0) liq = 1;
484
- secondsPerLiquidityCumulativeX128s[1] = uint160((uint256(twapWindow) << 128) / liq);
516
+ secondsPerLiquidityCumulativeX128s[1] = uint136((uint256(twapWindow) << 128) / liq);
485
517
 
486
518
  vm.mockCall(
487
519
  address(0),
@@ -547,7 +579,7 @@ abstract contract ForkTestBase is TestBaseWorkflow {
547
579
  }
548
580
 
549
581
  /// @notice Build payment metadata with only 721 tier selection (no quote -> TWAP/spot fallback).
550
- function _buildPayMetadataNoQuote(address hookMetadataTarget) internal view returns (bytes memory) {
582
+ function _buildPayMetadataNoQuote(address hookMetadataTarget) internal pure returns (bytes memory) {
551
583
  uint16[] memory tierIds = new uint16[](1);
552
584
  tierIds[0] = 1;
553
585
  bytes memory tierData = abi.encode(true, tierIds);
@@ -15,10 +15,6 @@ contract TestCashOutFork is ForkTestBase {
15
15
  function setUp() public override {
16
16
  super.setUp();
17
17
 
18
- // Skip if no fork available.
19
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
20
- if (bytes(rpcUrl).length == 0) return;
21
-
22
18
  // Deploy fee project + revnet with 50% cashOutTaxRate.
23
19
  _deployFeeProject(5000);
24
20
  revnetId = _deployRevnet(5000);
@@ -34,7 +30,7 @@ contract TestCashOutFork is ForkTestBase {
34
30
  }
35
31
 
36
32
  /// @notice Cash out tokens and verify fee deduction, token burn, and bonding curve reclaim.
37
- function test_fork_cashOut_normalWithFee() public onlyFork {
33
+ function test_fork_cashOut_normalWithFee() public {
38
34
  uint256 payerTokens = jbTokens().totalBalanceOf(PAYER, revnetId);
39
35
  uint256 cashOutCount = payerTokens / 2; // Cash out half.
40
36
 
@@ -78,7 +74,7 @@ contract TestCashOutFork is ForkTestBase {
78
74
  }
79
75
 
80
76
  /// @notice High tax rate (90%) produces small reclaim relative to pro-rata.
81
- function test_fork_cashOut_highTaxRate() public onlyFork {
77
+ function test_fork_cashOut_highTaxRate() public {
82
78
  // Deploy a separate revnet with 90% tax rate.
83
79
  uint256 highTaxRevnet = _deployRevnet(9000);
84
80
  _setupPool(highTaxRevnet, 10_000 ether);
@@ -111,7 +107,7 @@ contract TestCashOutFork is ForkTestBase {
111
107
  }
112
108
 
113
109
  /// @notice Sucker addresses get full pro-rata reclaim with 0% tax and no fee.
114
- function test_fork_cashOut_suckerExempt() public onlyFork {
110
+ function test_fork_cashOut_suckerExempt() public {
115
111
  address sucker = makeAddr("sucker");
116
112
  vm.deal(sucker, 100 ether);
117
113
 
@@ -153,7 +149,7 @@ contract TestCashOutFork is ForkTestBase {
153
149
  }
154
150
 
155
151
  /// @notice After a payment with 30% tier split, surplus accounting reflects actual terminal balance.
156
- function test_fork_cashOut_afterTierSplitPayment() public onlyFork {
152
+ function test_fork_cashOut_afterTierSplitPayment() public {
157
153
  // Deploy revnet with 721 hook.
158
154
  (uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
159
155
  _setupPool(splitRevnetId, 10_000 ether);
@@ -202,9 +198,14 @@ contract TestCashOutFork is ForkTestBase {
202
198
  }
203
199
 
204
200
  /// @notice Cash out before delay expires should revert.
205
- function test_fork_cashOut_delayEnforcement() public onlyFork {
206
- // Deploy a fresh revnet (delay starts from deploy time).
207
- uint256 delayRevnet = _deployRevnet(5000);
201
+ function test_fork_cashOut_delayEnforcement() public {
202
+ // Deploy a revnet whose first stage started in the past → triggers cash-out delay.
203
+ (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
204
+ _buildMinimalConfig(5000);
205
+ cfg.stageConfigurations[0].startsAtOrAfter = uint40(block.timestamp - 1);
206
+ uint256 delayRevnet = REV_DEPLOYER.deployFor({
207
+ revnetId: 0, configuration: cfg, terminalConfigurations: tc, suckerDeploymentConfiguration: sdc
208
+ });
208
209
  _setupPool(delayRevnet, 10_000 ether);
209
210
  _payRevnet(delayRevnet, PAYER, 1 ether);
210
211
 
@@ -15,9 +15,6 @@ contract TestLoanBorrowFork is ForkTestBase {
15
15
  function setUp() public override {
16
16
  super.setUp();
17
17
 
18
- string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
19
- if (bytes(rpcUrl).length == 0) return;
20
-
21
18
  // Deploy fee project + revnet with 50% cashOutTaxRate.
22
19
  _deployFeeProject(5000);
23
20
  revnetId = _deployRevnet(5000);
@@ -31,7 +28,7 @@ contract TestLoanBorrowFork is ForkTestBase {
31
28
  }
32
29
 
33
30
  /// @notice Basic borrow: collateralize all borrower tokens, verify loan state.
34
- function test_fork_borrow_basic() public onlyFork {
31
+ function test_fork_borrow_basic() public {
35
32
  uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
36
33
 
37
34
  uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
@@ -53,8 +50,11 @@ contract TestLoanBorrowFork is ForkTestBase {
53
50
  assertEq(loan.collateral, borrowerTokens, "loan collateral should match");
54
51
  assertEq(loan.createdAt, block.timestamp, "loan createdAt should be now");
55
52
 
56
- // Borrower tokens should be burned (collateral deposited).
57
- assertEq(jbTokens().totalBalanceOf(BORROWER, revnetId), 0, "borrower tokens should be burned");
53
+ // Borrower's original tokens are burned as collateral, but the source fee payment back to the revnet mints
54
+ // some tokens to the borrower.
55
+ uint256 feeTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
56
+ assertGt(feeTokens, 0, "borrower should have tokens from source fee payment");
57
+ assertLt(feeTokens, borrowerTokens, "fee tokens should be less than original collateral");
58
58
 
59
59
  // Borrower received ETH (net of fees).
60
60
  assertGt(BORROWER.balance, borrowerEthBefore, "borrower should receive ETH");
@@ -76,7 +76,7 @@ contract TestLoanBorrowFork is ForkTestBase {
76
76
  }
77
77
 
78
78
  /// @notice Verify fee distribution: source fee (2.5%) + REV fee (1%) deducted correctly.
79
- function test_fork_borrow_feeDistribution() public onlyFork {
79
+ function test_fork_borrow_feeDistribution() public {
80
80
  uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
81
81
  uint256 prepaidFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT(); // 25 = 2.5%
82
82
 
@@ -86,8 +86,6 @@ contract TestLoanBorrowFork is ForkTestBase {
86
86
 
87
87
  // Record balances before.
88
88
  uint256 borrowerEthBefore = BORROWER.balance;
89
- uint256 revnetTerminalBefore = _terminalBalance(revnetId, JBConstants.NATIVE_TOKEN);
90
-
91
89
  _grantBurnPermission(BORROWER, revnetId);
92
90
 
93
91
  REVLoanSource memory source = _nativeLoanSource();
@@ -122,7 +120,7 @@ contract TestLoanBorrowFork is ForkTestBase {
122
120
  }
123
121
 
124
122
  /// @notice Borrow after a payment with 30% tier splits.
125
- function test_fork_borrow_afterTierSplits() public onlyFork {
123
+ function test_fork_borrow_afterTierSplits() public {
126
124
  // Deploy revnet with 721 hook.
127
125
  (uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
128
126
  _setupPool(splitRevnetId, 10_000 ether);
@@ -142,8 +140,8 @@ contract TestLoanBorrowFork is ForkTestBase {
142
140
  metadata: metadata
143
141
  });
144
142
 
145
- // With 30% split and 1000 tokens/ETH issuance, borrower gets 700 tokens/ETH * 5 = 3500 tokens.
146
- assertEq(borrowerTokens, 3500e18, "should get 3500 tokens after 30% split");
143
+ // Tier 1 costs 1 ETH with 30% split 0.3 ETH to splits, 4.7 ETH minted at 1000 tokens/ETH = 4700 tokens.
144
+ assertEq(borrowerTokens, 4700e18, "should get 4700 tokens after tier split");
147
145
 
148
146
  // Surplus should reflect actual terminal balance.
149
147
  uint256 surplus = _terminalBalance(splitRevnetId, JBConstants.NATIVE_TOKEN);