@rev-net/core-v6 0.0.36 → 0.0.37

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 (28) hide show
  1. package/RISKS.md +11 -1
  2. package/package.json +9 -9
  3. package/src/REVLoans.sol +120 -83
  4. package/src/REVOwner.sol +3 -3
  5. package/test/REV.integrations.t.sol +14 -14
  6. package/test/REVInvincibility.t.sol +16 -16
  7. package/test/REVLifecycle.t.sol +32 -32
  8. package/test/REVLoansSourced.t.sol +15 -15
  9. package/test/TestCashOutCallerValidation.t.sol +8 -8
  10. package/test/TestConversionDocumentation.t.sol +2 -5
  11. package/test/TestCrossCurrencyReclaim.t.sol +72 -72
  12. package/test/TestLongTailEconomics.t.sol +56 -56
  13. package/test/TestSwapTerminalPermission.t.sol +21 -21
  14. package/test/audit/HiddenSupplyCashout.t.sol +61 -0
  15. package/test/audit/NemesisVerification.t.sol +97 -0
  16. package/test/audit/REVOwnerCurrencyMismatch.t.sol +188 -0
  17. package/test/audit/{CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol → REVOwnerRemoteSurplusCurrencyMismatch.t.sol} +4 -6
  18. package/test/audit/ReallocatePermission.t.sol +363 -0
  19. package/test/audit/RemoteLoanAccountingGap.t.sol +74 -0
  20. package/test/fork/TestCashOutFork.t.sol +48 -48
  21. package/test/fork/TestLoanAdversarialFork.t.sol +744 -0
  22. package/test/fork/TestLoanERC20Fork.t.sol +2 -8
  23. package/test/fork/TestPermit2PaymentFork.t.sol +32 -32
  24. package/test/regression/TestBurnPermissionRequired.t.sol +5 -5
  25. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +8 -8
  26. /package/test/audit/{CodexCrossChainBuybackRouteMismatch.t.sol → CrossChainBuybackRouteMismatch.t.sol} +0 -0
  27. /package/test/audit/{NemesisOperatorDelegation.t.sol → OperatorDelegation.t.sol} +0 -0
  28. /package/test/audit/{CodexPhantomSurplusTerminal.t.sol → PhantomSurplusTerminal.t.sol} +0 -0
@@ -268,14 +268,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
268
268
  vm.prank(USER1);
269
269
  uint256 reclaimed = jbMultiTerminal()
270
270
  .cashOutTokensOf({
271
- holder: USER1,
272
- projectId: REVNET_ID,
273
- cashOutCount: tokens,
274
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
275
- minTokensReclaimed: 0,
276
- beneficiary: payable(USER1),
277
- metadata: ""
278
- });
271
+ holder: USER1,
272
+ projectId: REVNET_ID,
273
+ cashOutCount: tokens,
274
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
275
+ minTokensReclaimed: 0,
276
+ beneficiary: payable(USER1),
277
+ metadata: ""
278
+ });
279
279
 
280
280
  assertGt(reclaimed, 0, "should reclaim some ETH");
281
281
  // With 50% tax and single holder cashing out everything, the bonding curve returns the full surplus.
@@ -310,14 +310,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
310
310
  vm.prank(USER1);
311
311
  uint256 reclaimedToken = jbMultiTerminal()
312
312
  .cashOutTokensOf({
313
- holder: USER1,
314
- projectId: REVNET_ID,
315
- cashOutCount: revTokens,
316
- tokenToReclaim: address(TOKEN),
317
- minTokensReclaimed: 0,
318
- beneficiary: payable(USER1),
319
- metadata: ""
320
- });
313
+ holder: USER1,
314
+ projectId: REVNET_ID,
315
+ cashOutCount: revTokens,
316
+ tokenToReclaim: address(TOKEN),
317
+ minTokensReclaimed: 0,
318
+ beneficiary: payable(USER1),
319
+ metadata: ""
320
+ });
321
321
 
322
322
  assertGt(reclaimedToken, 0, "should reclaim some TOKEN");
323
323
  assertLe(reclaimedToken, tokenAmount, "should not exceed total TOKEN paid in");
@@ -358,14 +358,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
358
358
  vm.expectRevert();
359
359
  jbMultiTerminal()
360
360
  .cashOutTokensOf({
361
- holder: USER1,
362
- projectId: REVNET_ID,
363
- cashOutCount: revTokens,
364
- tokenToReclaim: address(TOKEN),
365
- minTokensReclaimed: 0,
366
- beneficiary: payable(USER1),
367
- metadata: ""
368
- });
361
+ holder: USER1,
362
+ projectId: REVNET_ID,
363
+ cashOutCount: revTokens,
364
+ tokenToReclaim: address(TOKEN),
365
+ minTokensReclaimed: 0,
366
+ beneficiary: payable(USER1),
367
+ metadata: ""
368
+ });
369
369
  }
370
370
 
371
371
  /// @notice Pay with TOKEN (so the surplus is in TOKEN), then pay with ETH too.
@@ -395,14 +395,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
395
395
  vm.prank(USER1);
396
396
  uint256 reclaimedToken = jbMultiTerminal()
397
397
  .cashOutTokensOf({
398
- holder: USER1,
399
- projectId: REVNET_ID,
400
- cashOutCount: cashOutCount,
401
- tokenToReclaim: address(TOKEN),
402
- minTokensReclaimed: 0,
403
- beneficiary: payable(USER1),
404
- metadata: ""
405
- });
398
+ holder: USER1,
399
+ projectId: REVNET_ID,
400
+ cashOutCount: cashOutCount,
401
+ tokenToReclaim: address(TOKEN),
402
+ minTokensReclaimed: 0,
403
+ beneficiary: payable(USER1),
404
+ metadata: ""
405
+ });
406
406
 
407
407
  assertGt(reclaimedToken, 0, "should reclaim some TOKEN");
408
408
  assertLe(reclaimedToken, tokenPayment, "should not exceed total TOKEN paid in");
@@ -442,14 +442,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
442
442
  vm.prank(USER1);
443
443
  uint256 reclaimed = jbMultiTerminal()
444
444
  .cashOutTokensOf({
445
- holder: USER1,
446
- projectId: REVNET_ID,
447
- cashOutCount: tokens1,
448
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
449
- minTokensReclaimed: 0,
450
- beneficiary: payable(USER1),
451
- metadata: ""
452
- });
445
+ holder: USER1,
446
+ projectId: REVNET_ID,
447
+ cashOutCount: tokens1,
448
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
449
+ minTokensReclaimed: 0,
450
+ beneficiary: payable(USER1),
451
+ metadata: ""
452
+ });
453
453
  assertGt(reclaimed, 0, "should reclaim ETH from mixed-currency surplus");
454
454
 
455
455
  // The reclaimed amount should be less than the original payment (due to bonding curve tax).
@@ -480,14 +480,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
480
480
  vm.prank(USER1);
481
481
  uint256 reclaimed = jbMultiTerminal()
482
482
  .cashOutTokensOf({
483
- holder: USER1,
484
- projectId: REVNET_ID,
485
- cashOutCount: tokens,
486
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
487
- minTokensReclaimed: 0,
488
- beneficiary: payable(USER1),
489
- metadata: ""
490
- });
483
+ holder: USER1,
484
+ projectId: REVNET_ID,
485
+ cashOutCount: tokens,
486
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
487
+ minTokensReclaimed: 0,
488
+ beneficiary: payable(USER1),
489
+ metadata: ""
490
+ });
491
491
 
492
492
  assertLe(reclaimed, 1, "tiny payment should not yield more than original amount");
493
493
  }
@@ -517,14 +517,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
517
517
  vm.prank(USER1);
518
518
  uint256 reclaimed = jbMultiTerminal()
519
519
  .cashOutTokensOf({
520
- holder: USER1,
521
- projectId: REVNET_ID,
522
- cashOutCount: revTokens,
523
- tokenToReclaim: address(TOKEN),
524
- minTokensReclaimed: 0,
525
- beneficiary: payable(USER1),
526
- metadata: ""
527
- });
520
+ holder: USER1,
521
+ projectId: REVNET_ID,
522
+ cashOutCount: revTokens,
523
+ tokenToReclaim: address(TOKEN),
524
+ minTokensReclaimed: 0,
525
+ beneficiary: payable(USER1),
526
+ metadata: ""
527
+ });
528
528
 
529
529
  // Should reclaim some TOKEN (bounded by the original payment amount).
530
530
  assertLe(reclaimed, tokenAmount, "should not exceed original TOKEN payment");
@@ -559,14 +559,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
559
559
  vm.prank(USER1);
560
560
  uint256 reclaimed = jbMultiTerminal()
561
561
  .cashOutTokensOf({
562
- holder: USER1,
563
- projectId: REVNET_ID,
564
- cashOutCount: revTokens,
565
- tokenToReclaim: address(TOKEN),
566
- minTokensReclaimed: 0,
567
- beneficiary: payable(USER1),
568
- metadata: ""
569
- });
562
+ holder: USER1,
563
+ projectId: REVNET_ID,
564
+ cashOutCount: revTokens,
565
+ tokenToReclaim: address(TOKEN),
566
+ minTokensReclaimed: 0,
567
+ beneficiary: payable(USER1),
568
+ metadata: ""
569
+ });
570
570
 
571
571
  assertLe(reclaimed, tokenAmount, "should not exceed total TOKEN paid");
572
572
  }
@@ -595,14 +595,14 @@ contract TestCrossCurrencyReclaim is TestBaseWorkflow {
595
595
  vm.prank(USER1);
596
596
  uint256 reclaimed = jbMultiTerminal()
597
597
  .cashOutTokensOf({
598
- holder: USER1,
599
- projectId: REVNET_ID,
600
- cashOutCount: tokens,
601
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
602
- minTokensReclaimed: 0,
603
- beneficiary: payable(USER1),
604
- metadata: ""
605
- });
598
+ holder: USER1,
599
+ projectId: REVNET_ID,
600
+ cashOutCount: tokens,
601
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
602
+ minTokensReclaimed: 0,
603
+ beneficiary: payable(USER1),
604
+ metadata: ""
605
+ });
606
606
 
607
607
  // With 0% cash out tax, no fee is charged on cash outs (per REVDeployer.beforeCashOutRecordedWith).
608
608
  assertEq(reclaimed, balanceBefore, "0% tax, single holder should reclaim full balance");
@@ -363,14 +363,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
363
363
  vm.prank(users[i]);
364
364
  uint256 reclaimed = jbMultiTerminal()
365
365
  .cashOutTokensOf({
366
- holder: users[i],
367
- projectId: REVNET_ID,
368
- cashOutCount: userTokens[i],
369
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
370
- minTokensReclaimed: 0,
371
- beneficiary: payable(users[i]),
372
- metadata: ""
373
- });
366
+ holder: users[i],
367
+ projectId: REVNET_ID,
368
+ cashOutCount: userTokens[i],
369
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
370
+ minTokensReclaimed: 0,
371
+ beneficiary: payable(users[i]),
372
+ metadata: ""
373
+ });
374
374
  totalReclaimed += reclaimed;
375
375
  }
376
376
 
@@ -412,14 +412,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
412
412
  vm.prank(payer);
413
413
  uint256 reclaimedHalf = jbMultiTerminal()
414
414
  .cashOutTokensOf({
415
- holder: payer,
416
- projectId: REVNET_ID,
417
- cashOutCount: halfTokens,
418
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
419
- minTokensReclaimed: 0,
420
- beneficiary: payable(payer),
421
- metadata: ""
422
- });
415
+ holder: payer,
416
+ projectId: REVNET_ID,
417
+ cashOutCount: halfTokens,
418
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
419
+ minTokensReclaimed: 0,
420
+ beneficiary: payable(payer),
421
+ metadata: ""
422
+ });
423
423
 
424
424
  // With a 50% tax rate and being the only holder, cashing out half the tokens
425
425
  // should return less than half the surplus (bonding curve subproportional behavior).
@@ -452,14 +452,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
452
452
  vm.prank(users[i]);
453
453
  jbMultiTerminal()
454
454
  .cashOutTokensOf({
455
- holder: users[i],
456
- projectId: REVNET_ID,
457
- cashOutCount: userBalance / 2,
458
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
459
- minTokensReclaimed: 0,
460
- beneficiary: payable(users[i]),
461
- metadata: ""
462
- });
455
+ holder: users[i],
456
+ projectId: REVNET_ID,
457
+ cashOutCount: userBalance / 2,
458
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
459
+ minTokensReclaimed: 0,
460
+ beneficiary: payable(users[i]),
461
+ metadata: ""
462
+ });
463
463
  }
464
464
  }
465
465
 
@@ -479,14 +479,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
479
479
  vm.prank(users[3]);
480
480
  jbMultiTerminal()
481
481
  .cashOutTokensOf({
482
- holder: users[3],
483
- projectId: REVNET_ID,
484
- cashOutCount: user3Balance,
485
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
486
- minTokensReclaimed: 0,
487
- beneficiary: payable(users[3]),
488
- metadata: ""
489
- });
482
+ holder: users[3],
483
+ projectId: REVNET_ID,
484
+ cashOutCount: user3Balance,
485
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
486
+ minTokensReclaimed: 0,
487
+ beneficiary: payable(users[3]),
488
+ metadata: ""
489
+ });
490
490
  }
491
491
  }
492
492
 
@@ -556,14 +556,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
556
556
  vm.prank(lateUser);
557
557
  uint256 reclaimed = jbMultiTerminal()
558
558
  .cashOutTokensOf({
559
- holder: lateUser,
560
- projectId: REVNET_ID,
561
- cashOutCount: lateTokens,
562
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
563
- minTokensReclaimed: 0,
564
- beneficiary: payable(lateUser),
565
- metadata: ""
566
- });
559
+ holder: lateUser,
560
+ projectId: REVNET_ID,
561
+ cashOutCount: lateTokens,
562
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
563
+ minTokensReclaimed: 0,
564
+ beneficiary: payable(lateUser),
565
+ metadata: ""
566
+ });
567
567
 
568
568
  // The late entrant should not extract more than they put in.
569
569
  assertLe(reclaimed, 10e18, "late entrant should not extract more than they paid");
@@ -599,14 +599,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
599
599
  vm.prank(users[0]);
600
600
  jbMultiTerminal()
601
601
  .cashOutTokensOf({
602
- holder: users[0],
603
- projectId: REVNET_ID,
604
- cashOutCount: balance0 / 3,
605
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
606
- minTokensReclaimed: 0,
607
- beneficiary: payable(users[0]),
608
- metadata: ""
609
- });
602
+ holder: users[0],
603
+ projectId: REVNET_ID,
604
+ cashOutCount: balance0 / 3,
605
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
606
+ minTokensReclaimed: 0,
607
+ beneficiary: payable(users[0]),
608
+ metadata: ""
609
+ });
610
610
  }
611
611
  }
612
612
 
@@ -654,14 +654,14 @@ contract TestLongTailEconomics is TestBaseWorkflow {
654
654
  vm.prank(user);
655
655
  jbMultiTerminal()
656
656
  .cashOutTokensOf({
657
- holder: user,
658
- projectId: REVNET_ID,
659
- cashOutCount: portion,
660
- tokenToReclaim: JBConstants.NATIVE_TOKEN,
661
- minTokensReclaimed: 0,
662
- beneficiary: payable(user),
663
- metadata: ""
664
- });
657
+ holder: user,
658
+ projectId: REVNET_ID,
659
+ cashOutCount: portion,
660
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
661
+ minTokensReclaimed: 0,
662
+ beneficiary: payable(user),
663
+ metadata: ""
664
+ });
665
665
 
666
666
  uint256 feeBalanceAfterRound =
667
667
  jbTerminalStore().balanceOf(address(jbMultiTerminal()), FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN);
@@ -206,24 +206,24 @@ contract TestSwapTerminalPermission is TestBaseWorkflow {
206
206
  function test_splitOperator_hasRegistryPermissions() public view {
207
207
  bool hasBuybackHook = jbPermissions()
208
208
  .hasPermission({
209
- operator: multisig(),
210
- account: address(REV_DEPLOYER),
211
- projectId: TEST_REVNET_ID,
212
- permissionId: JBPermissionIds.SET_BUYBACK_HOOK,
213
- includeRoot: false,
214
- includeWildcardProjectId: false
215
- });
209
+ operator: multisig(),
210
+ account: address(REV_DEPLOYER),
211
+ projectId: TEST_REVNET_ID,
212
+ permissionId: JBPermissionIds.SET_BUYBACK_HOOK,
213
+ includeRoot: false,
214
+ includeWildcardProjectId: false
215
+ });
216
216
  assertTrue(hasBuybackHook, "Split operator should have SET_BUYBACK_HOOK permission");
217
217
 
218
218
  bool hasRouterTerminal = jbPermissions()
219
219
  .hasPermission({
220
- operator: multisig(),
221
- account: address(REV_DEPLOYER),
222
- projectId: TEST_REVNET_ID,
223
- permissionId: JBPermissionIds.SET_ROUTER_TERMINAL,
224
- includeRoot: false,
225
- includeWildcardProjectId: false
226
- });
220
+ operator: multisig(),
221
+ account: address(REV_DEPLOYER),
222
+ projectId: TEST_REVNET_ID,
223
+ permissionId: JBPermissionIds.SET_ROUTER_TERMINAL,
224
+ includeRoot: false,
225
+ includeWildcardProjectId: false
226
+ });
227
227
  assertTrue(hasRouterTerminal, "Split operator should have SET_ROUTER_TERMINAL permission");
228
228
  }
229
229
 
@@ -244,13 +244,13 @@ contract TestSwapTerminalPermission is TestBaseWorkflow {
244
244
  for (uint256 i = 0; i < expectedPermissions.length; i++) {
245
245
  bool hasPermission = jbPermissions()
246
246
  .hasPermission({
247
- operator: multisig(),
248
- account: address(REV_DEPLOYER),
249
- projectId: TEST_REVNET_ID,
250
- permissionId: expectedPermissions[i],
251
- includeRoot: false,
252
- includeWildcardProjectId: false
253
- });
247
+ operator: multisig(),
248
+ account: address(REV_DEPLOYER),
249
+ projectId: TEST_REVNET_ID,
250
+ permissionId: expectedPermissions[i],
251
+ includeRoot: false,
252
+ includeWildcardProjectId: false
253
+ });
254
254
  assertTrue(
255
255
  hasPermission,
256
256
  string.concat(
@@ -0,0 +1,61 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
5
+
6
+ import {TestHiddenTokens} from "../TestHiddenTokens.t.sol";
7
+
8
+ contract CodexNemesisHiddenSupplyCashoutTest is TestHiddenTokens {
9
+ function test_hiddenSupplyCanDrainCashoutAndThenBeRevealed() public {
10
+ uint256 payAmount = 10 ether;
11
+
12
+ vm.prank(USER);
13
+ uint256 minted = jbMultiTerminal().pay{value: payAmount}({
14
+ projectId: REVNET_ID,
15
+ token: JBConstants.NATIVE_TOKEN,
16
+ amount: payAmount,
17
+ beneficiary: USER,
18
+ minReturnedTokens: 0,
19
+ memo: "",
20
+ metadata: ""
21
+ });
22
+
23
+ uint256 terminalBalanceBefore =
24
+ jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
25
+ assertEq(terminalBalanceBefore, payAmount, "setup: revnet terminal balance");
26
+
27
+ _allowHolderToHide(USER, REVNET_ID);
28
+
29
+ uint256 hiddenCount = minted / 2;
30
+ vm.prank(USER);
31
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hiddenCount, USER);
32
+
33
+ uint256 visibleSupply = jbController().TOKENS().totalSupplyOf(REVNET_ID);
34
+ assertEq(visibleSupply, minted - hiddenCount, "hidden tokens left the cash-out denominator");
35
+
36
+ vm.prank(USER);
37
+ jbMultiTerminal()
38
+ .cashOutTokensOf({
39
+ holder: USER,
40
+ projectId: REVNET_ID,
41
+ cashOutCount: visibleSupply,
42
+ tokenToReclaim: JBConstants.NATIVE_TOKEN,
43
+ minTokensReclaimed: 0,
44
+ beneficiary: payable(USER),
45
+ metadata: ""
46
+ });
47
+
48
+ uint256 terminalBalanceAfter =
49
+ jbTerminalStore().balanceOf(address(jbMultiTerminal()), REVNET_ID, JBConstants.NATIVE_TOKEN);
50
+ assertEq(terminalBalanceAfter, 0, "visible tranche drained the revnet balance");
51
+
52
+ vm.prank(USER);
53
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hiddenCount, USER);
54
+
55
+ assertEq(
56
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
57
+ hiddenCount,
58
+ "hidden tranche was restored after the cash out"
59
+ );
60
+ }
61
+ }
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import {Test} from "forge-std/Test.sol";
5
+
6
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
7
+ import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
8
+
9
+ contract CodexNemesisVerificationTest is Test {
10
+ function testConfigurationHashDoesNotCommitSplitOperatorSplitsOrExtraMetadata() public pure {
11
+ bytes32 hashA = _revDeployerEncodedConfigurationHash();
12
+ bytes32 hashB = _revDeployerEncodedConfigurationHash();
13
+
14
+ assertEq(hashA, hashB, "actual REVDeployer hash collides");
15
+
16
+ bytes32 fullCommitmentA = keccak256(
17
+ abi.encode(hashA, address(0x1111), uint32(7000), address(0xAAAA), uint48(30 days), uint16(0x0004))
18
+ );
19
+ bytes32 fullCommitmentB = keccak256(
20
+ abi.encode(hashB, address(0x2222), uint32(7000), address(0xBBBB), uint48(90 days), uint16(0x0000))
21
+ );
22
+
23
+ assertNotEq(fullCommitmentA, fullCommitmentB, "omitted fields are economically distinct");
24
+ }
25
+
26
+ function testRemoteLoanStateOmissionOverstatesCrossChainCashOutValue() public pure {
27
+ uint256 localSupply = 100 ether;
28
+ uint256 localSurplus = 100 ether;
29
+ uint256 remoteVisibleSupply = 1 ether;
30
+ uint256 remoteVisibleSurplus = 1 ether;
31
+ uint256 omittedRemoteLoanCollateral = 99 ether;
32
+ uint256 omittedRemoteLoanDebt = 99 ether;
33
+ uint256 cashOutCount = 100 ether;
34
+ uint256 cashOutTaxRate = 1000;
35
+
36
+ uint256 current = JBCashOuts.cashOutFrom({
37
+ surplus: localSurplus + remoteVisibleSurplus,
38
+ cashOutCount: cashOutCount,
39
+ totalSupply: localSupply + remoteVisibleSupply,
40
+ cashOutTaxRate: cashOutTaxRate
41
+ });
42
+
43
+ uint256 withRemoteLoans = JBCashOuts.cashOutFrom({
44
+ surplus: localSurplus + remoteVisibleSurplus + omittedRemoteLoanDebt,
45
+ cashOutCount: cashOutCount,
46
+ totalSupply: localSupply + remoteVisibleSupply + omittedRemoteLoanCollateral,
47
+ cashOutTaxRate: cashOutTaxRate
48
+ });
49
+
50
+ assertGt(current, withRemoteLoans, "omitting remote loan state should not increase cash-out value");
51
+ assertGt(current - withRemoteLoans, 4 ether, "drift is material");
52
+ }
53
+
54
+ function testRemoteLoanStateOmissionCanHitFullSupplyBranch() public pure {
55
+ uint256 localSupply = 100 ether;
56
+ uint256 localSurplus = 100 ether;
57
+ uint256 omittedRemoteLoanCollateral = 100 ether;
58
+ uint256 omittedRemoteLoanDebt = 100 ether;
59
+ uint256 cashOutCount = 100 ether;
60
+ uint256 cashOutTaxRate = 1000;
61
+
62
+ uint256 current = JBCashOuts.cashOutFrom({
63
+ surplus: localSurplus, cashOutCount: cashOutCount, totalSupply: localSupply, cashOutTaxRate: cashOutTaxRate
64
+ });
65
+
66
+ uint256 withRemoteLoans = JBCashOuts.cashOutFrom({
67
+ surplus: localSurplus + omittedRemoteLoanDebt,
68
+ cashOutCount: cashOutCount,
69
+ totalSupply: localSupply + omittedRemoteLoanCollateral,
70
+ cashOutTaxRate: cashOutTaxRate
71
+ });
72
+
73
+ assertEq(current, localSurplus, "current path enters full-supply branch");
74
+ assertEq(withRemoteLoans, 95 ether, "true omnichain curve should remain partial");
75
+ assertGt(current, withRemoteLoans, "full-supply branch overstates cash-out value");
76
+ }
77
+
78
+ function _revDeployerEncodedConfigurationHash() internal pure returns (bytes32) {
79
+ bytes memory encodedConfiguration = abi.encode(uint32(1), "REV", "REV", bytes32("codex-nemesis"));
80
+
81
+ encodedConfiguration = abi.encode(encodedConfiguration, address(0x1234));
82
+
83
+ encodedConfiguration = abi.encode(
84
+ encodedConfiguration,
85
+ uint48(1_740_089_444),
86
+ uint16(7000),
87
+ uint112(1_000_000 ether),
88
+ uint32(30 days),
89
+ uint32(0),
90
+ uint16(JBConstants.MAX_CASH_OUT_TAX_RATE / 2)
91
+ );
92
+
93
+ encodedConfiguration = abi.encode(encodedConfiguration, uint32(1), address(0xBEEF), uint104(100_000 ether));
94
+
95
+ return keccak256(encodedConfiguration);
96
+ }
97
+ }