@rev-net/core-v6 0.0.37 → 0.0.39

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 (107) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +17 -10
  11. package/src/REVOwner.sol +121 -14
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -107
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -365
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  71. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  72. package/test/audit/NemesisVerification.t.sol +0 -97
  73. package/test/audit/OperatorDelegation.t.sol +0 -356
  74. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  75. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  76. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  77. package/test/audit/ReallocatePermission.t.sol +0 -363
  78. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  79. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  80. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  81. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  82. package/test/fork/ForkTestBase.sol +0 -727
  83. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  84. package/test/fork/TestCashOutFork.t.sol +0 -253
  85. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  86. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  87. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  88. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  89. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  90. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  91. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  92. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  93. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  94. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  95. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  96. package/test/helpers/MaliciousContracts.sol +0 -247
  97. package/test/helpers/REVEmpty721Config.sol +0 -45
  98. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  99. package/test/mock/MockBuybackDataHook.sol +0 -112
  100. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  101. package/test/mock/MockSuckerRegistry.sol +0 -17
  102. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  103. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  104. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  105. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  106. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  107. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,163 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "./ForkTestBase.sol";
6
- import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
7
-
8
- /// @notice Fork tests for REVLoans.borrowFrom() with real Uniswap V4 buyback hook.
9
- ///
10
- /// Covers: basic borrow, fee distribution, and borrow after tier splits.
11
- ///
12
- /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanBorrowFork -vvv
13
- contract TestLoanBorrowFork is ForkTestBase {
14
- uint256 revnetId;
15
-
16
- function setUp() public override {
17
- super.setUp();
18
-
19
- // Deploy fee project + revnet with 50% cashOutTaxRate.
20
- _deployFeeProject(5000);
21
- revnetId = _deployRevnet(5000);
22
-
23
- // Set up pool at 1:1 (mint path wins).
24
- _setupPool(revnetId, 10_000 ether);
25
-
26
- // Pay to create surplus. PAYER gets tokens and BORROWER gets tokens.
27
- _payRevnet(revnetId, PAYER, 10 ether);
28
- _payRevnet(revnetId, BORROWER, 5 ether);
29
- }
30
-
31
- /// @notice Basic borrow: collateralize all borrower tokens, verify loan state.
32
- function test_fork_borrow_basic() public {
33
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
34
-
35
- uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
36
- revnetId, borrowerTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
37
- );
38
- assertGt(borrowable, 0, "should have borrowable amount");
39
-
40
- uint256 totalCollateralBefore = LOANS_CONTRACT.totalCollateralOf(revnetId);
41
- uint256 totalBorrowedBefore =
42
- LOANS_CONTRACT.totalBorrowedFrom(revnetId, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
43
-
44
- uint256 borrowerEthBefore = BORROWER.balance;
45
-
46
- // Create the loan.
47
- (uint256 loanId, REVLoan memory loan) =
48
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
49
-
50
- // Verify loan state.
51
- assertEq(loan.collateral, borrowerTokens, "loan collateral should match");
52
- assertEq(loan.createdAt, block.timestamp, "loan createdAt should be now");
53
-
54
- // Borrower's original tokens are burned as collateral, but the source fee payment back to the revnet mints
55
- // some tokens to the borrower.
56
- uint256 feeTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
57
- assertGt(feeTokens, 0, "borrower should have tokens from source fee payment");
58
- assertLt(feeTokens, borrowerTokens, "fee tokens should be less than original collateral");
59
-
60
- // Borrower received ETH (net of fees).
61
- assertGt(BORROWER.balance, borrowerEthBefore, "borrower should receive ETH");
62
-
63
- // Tracking updated.
64
- assertEq(
65
- LOANS_CONTRACT.totalCollateralOf(revnetId),
66
- totalCollateralBefore + borrowerTokens,
67
- "totalCollateralOf should increase"
68
- );
69
- assertGt(
70
- LOANS_CONTRACT.totalBorrowedFrom(revnetId, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
71
- totalBorrowedBefore,
72
- "totalBorrowedFrom should increase"
73
- );
74
-
75
- // Loan NFT owned by borrower.
76
- assertEq(_loanOwnerOf(loanId), BORROWER, "loan NFT should be owned by borrower");
77
- }
78
-
79
- /// @notice Verify fee distribution: source fee (2.5%) + REV fee (1%) deducted correctly.
80
- function test_fork_borrow_feeDistribution() public {
81
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
82
- uint256 prepaidFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT(); // 25 = 2.5%
83
-
84
- uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
85
- revnetId, borrowerTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
86
- );
87
-
88
- // Record balances before.
89
- uint256 borrowerEthBefore = BORROWER.balance;
90
- _grantBurnPermission(BORROWER, revnetId);
91
-
92
- REVLoanSource memory source = _nativeLoanSource();
93
- vm.prank(BORROWER);
94
- LOANS_CONTRACT.borrowFrom({
95
- revnetId: revnetId,
96
- source: source,
97
- minBorrowAmount: 0,
98
- collateralCount: borrowerTokens,
99
- beneficiary: payable(BORROWER),
100
- prepaidFeePercent: prepaidFeePercent,
101
- holder: BORROWER
102
- });
103
-
104
- uint256 borrowerReceived = BORROWER.balance - borrowerEthBefore;
105
-
106
- // Calculate expected fees.
107
- // The allowance fee is taken by the terminal's useAllowanceOf (2.5% JB protocol fee).
108
- uint256 allowanceFee = JBFees.feeAmountFrom({amountBeforeFee: borrowable, feePercent: jbMultiTerminal().FEE()});
109
- // REV fee (1%).
110
- uint256 revFee =
111
- JBFees.feeAmountFrom({amountBeforeFee: borrowable, feePercent: LOANS_CONTRACT.REV_PREPAID_FEE_PERCENT()});
112
- // Source fee (prepaid).
113
- uint256 sourceFee = JBFees.feeAmountFrom({amountBeforeFee: borrowable, feePercent: prepaidFeePercent});
114
-
115
- uint256 totalFees = allowanceFee + revFee + sourceFee;
116
-
117
- // Borrower should receive borrowable - totalFees.
118
- assertApproxEqAbs(borrowerReceived, borrowable - totalFees, 10, "borrower net should match expected");
119
-
120
- // Loans contract should not hold any ETH.
121
- assertEq(address(LOANS_CONTRACT).balance, 0, "loans contract should not hold ETH");
122
- }
123
-
124
- /// @notice Borrow after a payment with 30% tier splits.
125
- function test_fork_borrow_afterTierSplits() public {
126
- // Deploy revnet with 721 hook.
127
- (uint256 splitRevnetId, IJB721TiersHook hook) = _deployRevnetWith721(5000);
128
- _setupPool(splitRevnetId, 10_000 ether);
129
-
130
- // Pay with tier metadata (30% split).
131
- address metadataTarget = hook.METADATA_ID_TARGET();
132
- bytes memory metadata = _buildPayMetadataNoQuote(metadataTarget);
133
-
134
- vm.prank(BORROWER);
135
- uint256 borrowerTokens = jbMultiTerminal().pay{value: 5 ether}({
136
- projectId: splitRevnetId,
137
- token: JBConstants.NATIVE_TOKEN,
138
- amount: 5 ether,
139
- beneficiary: BORROWER,
140
- minReturnedTokens: 0,
141
- memo: "",
142
- metadata: metadata
143
- });
144
-
145
- // Tier 1 costs 1 ETH with 30% split → 0.3 ETH to splits, 4.7 ETH minted at 1000 tokens/ETH = 4700 tokens.
146
- assertEq(borrowerTokens, 4700e18, "should get 4700 tokens after tier split");
147
-
148
- // Surplus should reflect actual terminal balance.
149
- uint256 surplus = _terminalBalance(splitRevnetId, JBConstants.NATIVE_TOKEN);
150
- assertGt(surplus, 0, "should have surplus");
151
-
152
- // Borrowable amount should be based on actual surplus, not full payment.
153
- uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
154
- splitRevnetId, borrowerTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
155
- );
156
-
157
- if (borrowable > 0) {
158
- (uint256 loanId,) =
159
- _createLoan(splitRevnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
160
- assertGt(loanId, 0, "loan should be created");
161
- }
162
- }
163
- }
@@ -1,308 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "./ForkTestBase.sol";
6
- import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
7
-
8
- /// @notice Fork tests for loan lifecycle spanning multiple revnet stages (rulesets).
9
- ///
10
- /// Verifies that loans created in one stage (high cashOutTaxRate) can be correctly repaid
11
- /// or liquidated after transitioning to a different stage (low cashOutTaxRate). This is
12
- /// critical because the bonding curve parameters change between stages, affecting borrowable
13
- /// amounts, collateral value, and fee calculations.
14
- ///
15
- /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanCrossRulesetFork -vvv
16
- contract TestLoanCrossRulesetFork is ForkTestBase {
17
- uint256 revnetId;
18
- uint256 constant STAGE_DURATION = 30 days;
19
-
20
- /// @notice Build a two-stage config: stage 1 (high tax), stage 2 (low tax).
21
- function _buildTwoStageConfig(
22
- uint16 stage1TaxRate,
23
- uint16 stage2TaxRate
24
- )
25
- internal
26
- view
27
- returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
28
- {
29
- JBAccountingContext[] memory acc = new JBAccountingContext[](1);
30
- acc[0] = JBAccountingContext({
31
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
32
- });
33
- tc = new JBTerminalConfig[](1);
34
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
35
-
36
- REVStageConfig[] memory stages = new REVStageConfig[](2);
37
- JBSplit[] memory splits = new JBSplit[](1);
38
- splits[0].beneficiary = payable(multisig());
39
- splits[0].percent = 10_000;
40
-
41
- // Stage 1: high tax — starts immediately.
42
- stages[0] = REVStageConfig({
43
- startsAtOrAfter: uint40(block.timestamp),
44
- autoIssuances: new REVAutoIssuance[](0),
45
- splitPercent: 0,
46
- splits: splits,
47
- initialIssuance: INITIAL_ISSUANCE,
48
- issuanceCutFrequency: 0,
49
- issuanceCutPercent: 0,
50
- cashOutTaxRate: stage1TaxRate,
51
- extraMetadata: 0
52
- });
53
-
54
- // Stage 2: low tax — starts after STAGE_DURATION.
55
- stages[1] = REVStageConfig({
56
- // forge-lint: disable-next-line(unsafe-typecast)
57
- startsAtOrAfter: uint40(block.timestamp + STAGE_DURATION),
58
- autoIssuances: new REVAutoIssuance[](0),
59
- splitPercent: 0,
60
- splits: splits,
61
- initialIssuance: INITIAL_ISSUANCE,
62
- issuanceCutFrequency: 0,
63
- issuanceCutPercent: 0,
64
- cashOutTaxRate: stage2TaxRate,
65
- extraMetadata: 0
66
- });
67
-
68
- cfg = REVConfig({
69
- // forge-lint: disable-next-line(named-struct-fields)
70
- description: REVDescription("CrossStage", "XSTG", "ipfs://xstage", "XSTG_SALT"),
71
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
72
- splitOperator: multisig(),
73
- stageConfigurations: stages
74
- });
75
-
76
- sdc = REVSuckerDeploymentConfig({
77
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("XSTG"))
78
- });
79
- }
80
-
81
- function setUp() public override {
82
- super.setUp();
83
-
84
- // Deploy fee project with 50% tax.
85
- _deployFeeProject(5000);
86
-
87
- // Deploy two-stage revnet: 70% tax → 20% tax after 30 days.
88
- (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
89
- _buildTwoStageConfig(7000, 2000);
90
-
91
- (revnetId,) = REV_DEPLOYER.deployFor({
92
- revnetId: 0,
93
- configuration: cfg,
94
- terminalConfigurations: tc,
95
- suckerDeploymentConfiguration: sdc,
96
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
97
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
98
- });
99
-
100
- // Set up pool at 1:1 (mint path wins).
101
- _setupPool(revnetId, 10_000 ether);
102
-
103
- // Create surplus with multiple payers so bonding curve tax has visible effect.
104
- _payRevnet(revnetId, PAYER, 10 ether);
105
- _payRevnet(revnetId, BORROWER, 5 ether);
106
-
107
- address otherPayer = makeAddr("otherPayer");
108
- vm.deal(otherPayer, 10 ether);
109
- _payRevnet(revnetId, otherPayer, 5 ether);
110
- }
111
-
112
- /// @notice Borrow in stage 1 (70% tax), repay in stage 2 (20% tax). Repayment should succeed
113
- /// and return full collateral regardless of the tax rate change.
114
- function test_fork_crossStage_borrowStage1_repayStage2() public {
115
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
116
- assertGt(borrowerTokens, 0, "borrower should have tokens");
117
-
118
- // Record borrowable in stage 1.
119
- uint256 borrowableStage1 = LOANS_CONTRACT.borrowableAmountFrom(
120
- revnetId, borrowerTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
121
- );
122
- assertGt(borrowableStage1, 0, "should have borrowable amount in stage 1");
123
-
124
- // Create loan in stage 1.
125
- (uint256 loanId, REVLoan memory loan) =
126
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
127
- assertGt(loanId, 0, "loan should be created");
128
- assertEq(loan.collateral, borrowerTokens, "collateral should match");
129
-
130
- // Record fee tokens minted to borrower from source fee payment back to revnet.
131
- uint256 feeTokensFromLoan = jbTokens().totalBalanceOf(BORROWER, revnetId);
132
-
133
- // Warp past stage 1 into stage 2.
134
- vm.warp(block.timestamp + STAGE_DURATION + 1);
135
-
136
- // Verify borrowable amount changed (should be higher with lower tax).
137
- uint256 borrowableStage2 = LOANS_CONTRACT.borrowableAmountFrom(
138
- revnetId, borrowerTokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
139
- );
140
- assertGt(borrowableStage2, borrowableStage1, "borrowable should increase with lower tax");
141
-
142
- // Repay the loan in stage 2: return all collateral.
143
- vm.deal(BORROWER, 100 ether);
144
- JBSingleAllowance memory allowance;
145
-
146
- vm.prank(BORROWER);
147
- LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
148
- loanId: loanId,
149
- maxRepayBorrowAmount: loan.amount * 2,
150
- collateralCountToReturn: loan.collateral,
151
- beneficiary: payable(BORROWER),
152
- allowance: allowance
153
- });
154
-
155
- // After repayment, borrower gets collateral back (plus fee tokens from loan creation).
156
- uint256 borrowerTokensAfter = jbTokens().totalBalanceOf(BORROWER, revnetId);
157
- assertEq(borrowerTokensAfter, borrowerTokens + feeTokensFromLoan, "borrower should recover full collateral");
158
-
159
- // Loan NFT should be burned.
160
- vm.expectRevert();
161
- _loanOwnerOf(loanId);
162
- }
163
-
164
- /// @notice Borrow in stage 1, attempt to liquidate in stage 2 before expiry. Should skip.
165
- function test_fork_crossStage_borrowStage1_liquidateStage2_notExpired() public {
166
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
167
-
168
- // Create loan in stage 1.
169
- (uint256 loanId,) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
170
-
171
- // Warp to stage 2 (but NOT past 10-year expiry).
172
- vm.warp(block.timestamp + STAGE_DURATION + 1);
173
-
174
- // Attempt liquidation — should skip this loan since it's not expired.
175
- // Loan number is 1 (first loan for this revnet), count = 1.
176
- LOANS_CONTRACT.liquidateExpiredLoansFrom(revnetId, 1, 1);
177
-
178
- // Loan should still exist.
179
- assertEq(_loanOwnerOf(loanId), BORROWER, "loan should not be liquidated");
180
- }
181
-
182
- /// @notice Borrow in stage 1, liquidate after 10-year expiry (spans far beyond both stages).
183
- function test_fork_crossStage_borrowStage1_liquidateAfterExpiry() public {
184
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
185
-
186
- // Create loan in stage 1.
187
- (uint256 loanId,) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
188
-
189
- uint256 totalCollateralBefore = LOANS_CONTRACT.totalCollateralOf(revnetId);
190
-
191
- // Warp past 10-year expiry (well beyond both stages).
192
- vm.warp(block.timestamp + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1);
193
-
194
- // Liquidate starting from loan number 1, count = 1.
195
- LOANS_CONTRACT.liquidateExpiredLoansFrom(revnetId, 1, 1);
196
-
197
- // Loan NFT should be burned.
198
- vm.expectRevert();
199
- _loanOwnerOf(loanId);
200
-
201
- // Collateral is permanently lost (burned during borrow, not returned on liquidation).
202
- uint256 totalCollateralAfter = LOANS_CONTRACT.totalCollateralOf(revnetId);
203
- assertEq(totalCollateralAfter, totalCollateralBefore - borrowerTokens, "total collateral should decrease");
204
- }
205
-
206
- /// @notice Partial repay in stage 1, complete repay in stage 2.
207
- function test_fork_crossStage_partialRepayStage1_completeRepayStage2() public {
208
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
209
-
210
- // Create loan in stage 1.
211
- (uint256 loanId, REVLoan memory loan) =
212
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
213
-
214
- // Record fee tokens minted to borrower from source fee payment back to revnet.
215
- uint256 feeTokensFromLoan = jbTokens().totalBalanceOf(BORROWER, revnetId);
216
-
217
- // Partial repay in stage 1: return half the collateral.
218
- uint256 halfCollateral = loan.collateral / 2;
219
-
220
- vm.deal(BORROWER, 100 ether);
221
- JBSingleAllowance memory allowance;
222
-
223
- vm.prank(BORROWER);
224
- (uint256 newLoanId,) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
225
- loanId: loanId,
226
- maxRepayBorrowAmount: loan.amount * 2,
227
- collateralCountToReturn: halfCollateral,
228
- beneficiary: payable(BORROWER),
229
- allowance: allowance
230
- });
231
-
232
- // Old loan should be replaced, new loan created for remainder.
233
- assertGt(newLoanId, 0, "new loan should be created for remainder");
234
-
235
- uint256 borrowerTokensMid = jbTokens().totalBalanceOf(BORROWER, revnetId);
236
- assertGt(borrowerTokensMid, 0, "borrower should get partial collateral back");
237
-
238
- // Warp to stage 2.
239
- vm.warp(block.timestamp + STAGE_DURATION + 1);
240
-
241
- // Complete repay in stage 2.
242
- REVLoan memory remainingLoan = LOANS_CONTRACT.loanOf(newLoanId);
243
-
244
- vm.prank(BORROWER);
245
- LOANS_CONTRACT.repayLoan{value: remainingLoan.amount * 2}({
246
- loanId: newLoanId,
247
- maxRepayBorrowAmount: remainingLoan.amount * 2,
248
- collateralCountToReturn: remainingLoan.collateral,
249
- beneficiary: payable(BORROWER),
250
- allowance: allowance
251
- });
252
-
253
- // All collateral should be recovered (plus fee tokens from loan creation).
254
- uint256 borrowerTokensFinal = jbTokens().totalBalanceOf(BORROWER, revnetId);
255
- assertEq(
256
- borrowerTokensFinal,
257
- borrowerTokens + feeTokensFromLoan,
258
- "should recover full collateral after two repayments"
259
- );
260
- }
261
-
262
- /// @notice Reallocate a loan created in stage 1 while in stage 2.
263
- function test_fork_crossStage_reallocateInStage2() public {
264
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
265
-
266
- // Create loan in stage 1.
267
- (uint256 loanId, REVLoan memory loan) =
268
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
269
-
270
- // Warp to stage 2.
271
- vm.warp(block.timestamp + STAGE_DURATION + 1);
272
-
273
- // Reallocate a small fraction (5%) to a new loan. Using a small fraction ensures the remaining
274
- // collateral still supports the existing borrow amount (bonding curve non-linearity).
275
- REVLoanSource memory source = _nativeLoanSource();
276
- uint256 transferAmount = loan.collateral / 20;
277
-
278
- // Grant burn permission for the new loan.
279
- _grantBurnPermission(BORROWER, revnetId);
280
-
281
- // Cache before prank to avoid consuming the prank with a static call.
282
- uint256 minFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
283
-
284
- vm.prank(BORROWER);
285
- (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan,) = LOANS_CONTRACT.reallocateCollateralFromLoan({
286
- loanId: loanId,
287
- collateralCountToTransfer: transferAmount,
288
- source: source,
289
- minBorrowAmount: 0,
290
- collateralCountToAdd: 0,
291
- beneficiary: payable(BORROWER),
292
- prepaidFeePercent: minFeePercent
293
- });
294
-
295
- // Original loan burned, reallocated loan created.
296
- vm.expectRevert();
297
- _loanOwnerOf(loanId);
298
-
299
- // Both new loans should exist.
300
- assertEq(_loanOwnerOf(reallocatedLoanId), BORROWER, "reallocated loan should exist");
301
- assertEq(_loanOwnerOf(newLoanId), BORROWER, "new loan should exist");
302
-
303
- // Reallocated loan should have reduced collateral.
304
- assertEq(
305
- reallocatedLoan.collateral, loan.collateral - transferAmount, "reallocated collateral should be reduced"
306
- );
307
- }
308
- }