@rev-net/core-v6 0.0.37 → 0.0.40

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 (112) 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 +69 -67
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +26 -22
  11. package/src/REVOwner.sol +147 -29
  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/src/structs/REVAutoIssuance.sol +4 -2
  16. package/src/structs/REVConfig.sol +8 -5
  17. package/src/structs/REVDescription.sol +6 -5
  18. package/src/structs/REVLoan.sol +8 -5
  19. package/src/structs/REVStageConfig.sol +14 -16
  20. package/ADMINISTRATION.md +0 -73
  21. package/ARCHITECTURE.md +0 -116
  22. package/AUDIT_INSTRUCTIONS.md +0 -90
  23. package/RISKS.md +0 -107
  24. package/SKILLS.md +0 -46
  25. package/STYLE_GUIDE.md +0 -610
  26. package/USER_JOURNEYS.md +0 -195
  27. package/foundry.lock +0 -11
  28. package/slither-ci.config.json +0 -10
  29. package/sphinx.lock +0 -507
  30. package/test/REV.integrations.t.sol +0 -573
  31. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  32. package/test/REVDeployerRegressions.t.sol +0 -396
  33. package/test/REVInvincibility.t.sol +0 -1371
  34. package/test/REVInvincibilityHandler.sol +0 -387
  35. package/test/REVLifecycle.t.sol +0 -420
  36. package/test/REVLoans.invariants.t.sol +0 -724
  37. package/test/REVLoansAttacks.t.sol +0 -816
  38. package/test/REVLoansFeeRecovery.t.sol +0 -783
  39. package/test/REVLoansFindings.t.sol +0 -711
  40. package/test/REVLoansRegressions.t.sol +0 -364
  41. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  42. package/test/REVLoansSourced.t.sol +0 -1839
  43. package/test/REVLoansUnSourced.t.sol +0 -409
  44. package/test/TestAuditFixVerification.t.sol +0 -675
  45. package/test/TestBurnHeldTokens.t.sol +0 -394
  46. package/test/TestCEIPattern.t.sol +0 -508
  47. package/test/TestCashOutCallerValidation.t.sol +0 -452
  48. package/test/TestConversionDocumentation.t.sol +0 -365
  49. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  50. package/test/TestCrossSourceReallocation.t.sol +0 -361
  51. package/test/TestERC2771MetaTx.t.sol +0 -585
  52. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  53. package/test/TestFlashLoanSurplus.t.sol +0 -365
  54. package/test/TestHiddenTokens.t.sol +0 -474
  55. package/test/TestHookArrayOOB.t.sol +0 -278
  56. package/test/TestLiquidationBehavior.t.sol +0 -398
  57. package/test/TestLoanSourceRotation.t.sol +0 -553
  58. package/test/TestLoansCashOutDelay.t.sol +0 -493
  59. package/test/TestLongTailEconomics.t.sol +0 -677
  60. package/test/TestLowFindings.t.sol +0 -677
  61. package/test/TestMixedFixes.t.sol +0 -593
  62. package/test/TestPermit2Signatures.t.sol +0 -683
  63. package/test/TestReallocationSandwich.t.sol +0 -412
  64. package/test/TestRevnetRegressions.t.sol +0 -350
  65. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  66. package/test/TestSplitWeightE2E.t.sol +0 -605
  67. package/test/TestSplitWeightFork.t.sol +0 -855
  68. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  69. package/test/TestSwapTerminalPermission.t.sol +0 -262
  70. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  71. package/test/TestUint112Overflow.t.sol +0 -311
  72. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  73. package/test/TestZeroRepayment.t.sol +0 -354
  74. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  75. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  76. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  77. package/test/audit/NemesisVerification.t.sol +0 -97
  78. package/test/audit/OperatorDelegation.t.sol +0 -356
  79. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  80. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  81. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  82. package/test/audit/ReallocatePermission.t.sol +0 -363
  83. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  84. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  85. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  86. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  87. package/test/fork/ForkTestBase.sol +0 -727
  88. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  89. package/test/fork/TestCashOutFork.t.sol +0 -253
  90. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  91. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  92. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  93. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  94. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  95. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  96. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  97. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  98. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  99. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  100. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  101. package/test/helpers/MaliciousContracts.sol +0 -247
  102. package/test/helpers/REVEmpty721Config.sol +0 -45
  103. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  104. package/test/mock/MockBuybackDataHook.sol +0 -112
  105. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  106. package/test/mock/MockSuckerRegistry.sol +0 -17
  107. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  108. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  109. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  110. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  111. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  112. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,113 +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
-
7
- /// @notice Fork tests for REVLoans.reallocateCollateralFromLoan() with real Uniswap V4 buyback hook.
8
- ///
9
- /// Covers: basic reallocation and source mismatch revert.
10
- ///
11
- /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanReallocateFork -vvv
12
- contract TestLoanReallocateFork is ForkTestBase {
13
- uint256 revnetId;
14
-
15
- function setUp() public override {
16
- super.setUp();
17
-
18
- // Deploy fee project + revnet.
19
- _deployFeeProject(5000);
20
- revnetId = _deployRevnet(5000);
21
- _setupPool(revnetId, 10_000 ether);
22
-
23
- // Pay to create surplus.
24
- _payRevnet(revnetId, PAYER, 10 ether);
25
- _payRevnet(revnetId, BORROWER, 10 ether);
26
- }
27
-
28
- /// @notice Reallocate collateral to a new loan: original reduced, new loan created.
29
- function test_fork_reallocate_basic() public {
30
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
31
- (uint256 loanId, REVLoan memory loan) =
32
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
33
-
34
- // Add more surplus after the loan so the remaining collateral supports the existing borrow amount.
35
- // Without extra surplus, any collateral removal would make borrowable < loan.amount (bonding curve).
36
- address extraPayer = makeAddr("extraPayer");
37
- vm.deal(extraPayer, 20 ether);
38
- _payRevnet(revnetId, extraPayer, 20 ether);
39
-
40
- uint256 transferAmount = loan.collateral / 20;
41
- uint256 totalCollateralBefore = LOANS_CONTRACT.totalCollateralOf(revnetId);
42
-
43
- REVLoanSource memory source = _nativeLoanSource();
44
-
45
- // Grant burn permission again for the new loan's collateral.
46
- _grantBurnPermission(BORROWER, revnetId);
47
-
48
- // Cache before prank to avoid consuming the prank with a static call.
49
- uint256 minFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
50
-
51
- vm.prank(BORROWER);
52
- (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan, REVLoan memory newLoan) = LOANS_CONTRACT.reallocateCollateralFromLoan({
53
- loanId: loanId,
54
- collateralCountToTransfer: transferAmount,
55
- source: source,
56
- minBorrowAmount: 0,
57
- collateralCountToAdd: 0,
58
- beneficiary: payable(BORROWER),
59
- prepaidFeePercent: minFeePercent
60
- });
61
-
62
- // Original loan reduced.
63
- assertEq(
64
- reallocatedLoan.collateral,
65
- loan.collateral - transferAmount,
66
- "reallocated loan should have reduced collateral"
67
- );
68
-
69
- // New loan has the transferred collateral.
70
- assertEq(newLoan.collateral, transferAmount, "new loan should have transferred collateral");
71
-
72
- // Original loan burned, reallocated loan created.
73
- vm.expectRevert();
74
- _loanOwnerOf(loanId);
75
-
76
- assertEq(_loanOwnerOf(reallocatedLoanId), BORROWER, "reallocated loan owned by borrower");
77
- assertEq(_loanOwnerOf(newLoanId), BORROWER, "new loan owned by borrower");
78
-
79
- // Total collateral should remain approximately the same (moved, not destroyed).
80
- // It increases by halfCollateral because the new loan's borrowFrom also adds collateral.
81
- // But reallocateCollateralFromLoan transfers collateral from the existing loan (not burning new tokens),
82
- // so totalCollateralOf should stay the same (the half was already in the system).
83
- assertEq(
84
- LOANS_CONTRACT.totalCollateralOf(revnetId), totalCollateralBefore, "total collateral should be unchanged"
85
- );
86
- }
87
-
88
- /// @notice Reallocate with a different source terminal should revert.
89
- function test_fork_reallocate_sourceMismatchReverts() public {
90
- uint256 borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
91
- (uint256 loanId, REVLoan memory loan) =
92
- _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
93
-
94
- // Create a source with a different terminal address.
95
- REVLoanSource memory badSource =
96
- REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: IJBPayoutTerminal(address(0xdead))});
97
-
98
- // Cache before prank to avoid consuming the prank with a static call.
99
- uint256 minFeePercent = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
100
-
101
- vm.prank(BORROWER);
102
- vm.expectRevert();
103
- LOANS_CONTRACT.reallocateCollateralFromLoan({
104
- loanId: loanId,
105
- collateralCountToTransfer: loan.collateral / 2,
106
- source: badSource,
107
- minBorrowAmount: 0,
108
- collateralCountToAdd: 0,
109
- beneficiary: payable(BORROWER),
110
- prepaidFeePercent: minFeePercent
111
- });
112
- }
113
- }
@@ -1,188 +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
-
7
- /// @notice Fork tests for REVLoans.repayLoan() with real Uniswap V4 buyback hook.
8
- ///
9
- /// Covers: full repay, partial repay, source fee after prepaid, no-fee within prepaid, expired revert.
10
- ///
11
- /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanRepayFork -vvv
12
- contract TestLoanRepayFork is ForkTestBase {
13
- uint256 revnetId;
14
- uint256 loanId;
15
- REVLoan loan;
16
- uint256 borrowerTokens;
17
- uint256 feeTokensFromLoan; // Tokens minted to borrower from source fee payment back to revnet.
18
-
19
- function setUp() public override {
20
- super.setUp();
21
-
22
- // Deploy fee project + revnet.
23
- _deployFeeProject(5000);
24
- revnetId = _deployRevnet(5000);
25
- _setupPool(revnetId, 10_000 ether);
26
-
27
- // Pay to create surplus.
28
- _payRevnet(revnetId, PAYER, 10 ether);
29
- _payRevnet(revnetId, BORROWER, 5 ether);
30
-
31
- borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
32
-
33
- // Create a loan with min prepaid fee.
34
- (loanId, loan) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
35
-
36
- // Record fee tokens minted to borrower from source fee payment back to revnet.
37
- feeTokensFromLoan = jbTokens().totalBalanceOf(BORROWER, revnetId);
38
- }
39
-
40
- /// @notice Full repay: return all collateral, burn loan NFT.
41
- function test_fork_repay_full() public {
42
- uint256 totalCollateralBefore = LOANS_CONTRACT.totalCollateralOf(revnetId);
43
- uint256 totalBorrowedBefore =
44
- LOANS_CONTRACT.totalBorrowedFrom(revnetId, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
45
-
46
- // Fund borrower to repay (they need more ETH than they got from the loan due to fees).
47
- vm.deal(BORROWER, 100 ether);
48
-
49
- JBSingleAllowance memory allowance;
50
-
51
- vm.prank(BORROWER);
52
- LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
53
- loanId: loanId,
54
- maxRepayBorrowAmount: loan.amount * 2,
55
- collateralCountToReturn: loan.collateral,
56
- beneficiary: payable(BORROWER),
57
- allowance: allowance
58
- });
59
-
60
- // Collateral re-minted to borrower (plus fee tokens from loan creation).
61
- assertEq(
62
- jbTokens().totalBalanceOf(BORROWER, revnetId),
63
- borrowerTokens + feeTokensFromLoan,
64
- "collateral should be returned to borrower"
65
- );
66
-
67
- // Loan NFT burned.
68
- vm.expectRevert();
69
- _loanOwnerOf(loanId);
70
-
71
- // Tracking decreased.
72
- assertEq(
73
- LOANS_CONTRACT.totalCollateralOf(revnetId),
74
- totalCollateralBefore - loan.collateral,
75
- "totalCollateralOf should decrease"
76
- );
77
- assertLt(
78
- LOANS_CONTRACT.totalBorrowedFrom(revnetId, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
79
- totalBorrowedBefore,
80
- "totalBorrowedFrom should decrease"
81
- );
82
- }
83
-
84
- /// @notice Partial repay: return half the collateral, old loan burned, new loan minted.
85
- function test_fork_repay_partial() public {
86
- uint256 halfCollateral = loan.collateral / 2;
87
-
88
- vm.deal(BORROWER, 100 ether);
89
-
90
- JBSingleAllowance memory allowance;
91
-
92
- vm.prank(BORROWER);
93
- (uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
94
- loanId: loanId,
95
- maxRepayBorrowAmount: loan.amount * 2,
96
- collateralCountToReturn: halfCollateral,
97
- beneficiary: payable(BORROWER),
98
- allowance: allowance
99
- });
100
-
101
- // Some collateral re-minted (plus fee tokens from loan creation).
102
- uint256 returnedTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
103
- assertEq(returnedTokens, halfCollateral + feeTokensFromLoan, "half collateral should be returned");
104
-
105
- // Old loan burned.
106
- vm.expectRevert();
107
- _loanOwnerOf(loanId);
108
-
109
- // New loan created with reduced collateral.
110
- assertGt(newLoanId, 0, "new loan should be created");
111
- assertEq(newLoan.collateral, loan.collateral - halfCollateral, "new loan collateral should be reduced");
112
- assertLt(newLoan.amount, loan.amount, "new loan amount should be less");
113
-
114
- // New loan NFT owned by borrower.
115
- assertEq(_loanOwnerOf(newLoanId), BORROWER, "new loan NFT should be owned by borrower");
116
- }
117
-
118
- /// @notice After prepaid duration, source fee is charged on repayment.
119
- function test_fork_repay_withSourceFee() public {
120
- // Warp well past the prepaid duration to accrue a meaningful source fee.
121
- vm.warp(block.timestamp + loan.prepaidDuration + 365 days);
122
-
123
- vm.deal(BORROWER, 100 ether);
124
-
125
- // Record ETH spent for repayment.
126
- uint256 borrowerEthBefore = BORROWER.balance;
127
-
128
- JBSingleAllowance memory allowance;
129
-
130
- vm.prank(BORROWER);
131
- LOANS_CONTRACT.repayLoan{value: loan.amount * 3}({
132
- loanId: loanId,
133
- maxRepayBorrowAmount: loan.amount * 3,
134
- collateralCountToReturn: loan.collateral,
135
- beneficiary: payable(BORROWER),
136
- allowance: allowance
137
- });
138
-
139
- uint256 ethSpent = borrowerEthBefore - BORROWER.balance;
140
-
141
- // Total cost should be more than the loan principal (due to source fee).
142
- assertGt(ethSpent, loan.amount, "repay cost should exceed loan amount due to source fee");
143
- }
144
-
145
- /// @notice Repay immediately (within prepaid duration) -> no source fee.
146
- function test_fork_repay_withinPrepaidNofee() public {
147
- // Don't warp — we're within prepaid duration.
148
- vm.deal(BORROWER, 100 ether);
149
-
150
- uint256 borrowerEthBefore = BORROWER.balance;
151
-
152
- JBSingleAllowance memory allowance;
153
-
154
- vm.prank(BORROWER);
155
- LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
156
- loanId: loanId,
157
- maxRepayBorrowAmount: loan.amount * 2,
158
- collateralCountToReturn: loan.collateral,
159
- beneficiary: payable(BORROWER),
160
- allowance: allowance
161
- });
162
-
163
- uint256 ethSpent = borrowerEthBefore - BORROWER.balance;
164
-
165
- // Within prepaid period, cost should be exactly the loan amount (no additional source fee).
166
- assertEq(ethSpent, loan.amount, "repay within prepaid should cost exactly loan amount");
167
- }
168
-
169
- /// @notice Repay after 10 years should revert (loan expired).
170
- function test_fork_repay_expiredReverts() public {
171
- // Warp past the 10-year liquidation duration (strict > check, so need +1).
172
- vm.warp(block.timestamp + LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION() + 1);
173
-
174
- vm.deal(BORROWER, 100 ether);
175
-
176
- JBSingleAllowance memory allowance;
177
-
178
- vm.prank(BORROWER);
179
- vm.expectRevert();
180
- LOANS_CONTRACT.repayLoan{value: loan.amount * 3}({
181
- loanId: loanId,
182
- maxRepayBorrowAmount: loan.amount * 3,
183
- collateralCountToReturn: loan.collateral,
184
- beneficiary: payable(BORROWER),
185
- allowance: allowance
186
- });
187
- }
188
- }
@@ -1,143 +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 {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
7
- import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
8
-
9
- /// @notice Fork tests for transferring loan NFTs and repaying from the new owner.
10
- ///
11
- /// Covers: transfer + repay by new owner, original owner rejection after transfer, transfer + partial repay.
12
- ///
13
- /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestLoanTransferFork -vvv
14
- contract TestLoanTransferFork is ForkTestBase {
15
- uint256 revnetId;
16
- uint256 loanId;
17
- REVLoan loan;
18
- uint256 borrowerTokens;
19
-
20
- address newOwner = makeAddr("newOwner");
21
-
22
- function setUp() public override {
23
- super.setUp();
24
-
25
- // Deploy fee project + revnet.
26
- _deployFeeProject(5000);
27
- revnetId = _deployRevnet(5000);
28
- _setupPool(revnetId, 10_000 ether);
29
-
30
- // Pay to create surplus.
31
- _payRevnet(revnetId, PAYER, 10 ether);
32
- _payRevnet(revnetId, BORROWER, 5 ether);
33
-
34
- borrowerTokens = jbTokens().totalBalanceOf(BORROWER, revnetId);
35
-
36
- // Create a loan with min prepaid fee.
37
- (loanId, loan) = _createLoan(revnetId, BORROWER, borrowerTokens, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
38
- }
39
-
40
- /// @notice Transfer loan NFT to a new owner, who then fully repays the loan.
41
- function test_fork_transferLoan_newOwnerCanRepay() public {
42
- // Transfer the loan NFT from BORROWER to newOwner.
43
- vm.prank(BORROWER);
44
- REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
45
-
46
- // Verify newOwner is the loan NFT owner.
47
- assertEq(_loanOwnerOf(loanId), newOwner, "newOwner should own the loan NFT after transfer");
48
-
49
- // Fund newOwner with ETH for repayment.
50
- vm.deal(newOwner, 100 ether);
51
-
52
- JBSingleAllowance memory allowance;
53
-
54
- // newOwner repays the loan in full.
55
- vm.prank(newOwner);
56
- LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
57
- loanId: loanId,
58
- maxRepayBorrowAmount: loan.amount * 2,
59
- collateralCountToReturn: loan.collateral,
60
- beneficiary: payable(newOwner),
61
- allowance: allowance
62
- });
63
-
64
- // Loan NFT should be burned after full repay.
65
- vm.expectRevert();
66
- _loanOwnerOf(loanId);
67
-
68
- // Collateral tokens should be minted to newOwner (the beneficiary).
69
- uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
70
- assertEq(newOwnerTokens, borrowerTokens, "collateral should be returned to newOwner");
71
- }
72
-
73
- /// @notice After transferring the loan NFT, the original borrower cannot repay.
74
- function test_fork_transferLoan_originalBorrowerCannotRepay() public {
75
- // Transfer the loan NFT from BORROWER to newOwner.
76
- vm.prank(BORROWER);
77
- REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
78
-
79
- // Fund BORROWER with ETH for the attempted repayment.
80
- vm.deal(BORROWER, 100 ether);
81
-
82
- JBSingleAllowance memory allowance;
83
-
84
- // Original borrower tries to repay — should revert with JBPermissioned_Unauthorized.
85
- vm.prank(BORROWER);
86
- vm.expectRevert(
87
- abi.encodeWithSelector(
88
- JBPermissioned.JBPermissioned_Unauthorized.selector,
89
- newOwner,
90
- BORROWER,
91
- revnetId,
92
- JBPermissionIds.REPAY_LOAN
93
- )
94
- );
95
- LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
96
- loanId: loanId,
97
- maxRepayBorrowAmount: loan.amount * 2,
98
- collateralCountToReturn: loan.collateral,
99
- beneficiary: payable(BORROWER),
100
- allowance: allowance
101
- });
102
- }
103
-
104
- /// @notice Transfer loan NFT, new owner does a partial repay — old loan burned, new loan minted to new owner.
105
- function test_fork_transferLoan_newOwnerPartialRepay() public {
106
- // Transfer the loan NFT from BORROWER to newOwner.
107
- vm.prank(BORROWER);
108
- REVLoans(payable(address(LOANS_CONTRACT))).safeTransferFrom(BORROWER, newOwner, loanId);
109
-
110
- // Fund newOwner with ETH for repayment.
111
- vm.deal(newOwner, 100 ether);
112
-
113
- uint256 halfCollateral = loan.collateral / 2;
114
-
115
- JBSingleAllowance memory allowance;
116
-
117
- // newOwner partially repays the loan (return half the collateral).
118
- vm.prank(newOwner);
119
- (uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loan.amount * 2}({
120
- loanId: loanId,
121
- maxRepayBorrowAmount: loan.amount * 2,
122
- collateralCountToReturn: halfCollateral,
123
- beneficiary: payable(newOwner),
124
- allowance: allowance
125
- });
126
-
127
- // Original loan NFT should be burned.
128
- vm.expectRevert();
129
- _loanOwnerOf(loanId);
130
-
131
- // New loan should exist with reduced collateral.
132
- assertGt(newLoanId, 0, "new loan should be created");
133
- assertEq(newLoan.collateral, loan.collateral - halfCollateral, "new loan collateral should be reduced");
134
- assertLt(newLoan.amount, loan.amount, "new loan borrow amount should be less");
135
-
136
- // New loan NFT should be owned by newOwner.
137
- assertEq(_loanOwnerOf(newLoanId), newOwner, "new loan NFT should be owned by newOwner");
138
-
139
- // Half collateral should be returned to newOwner.
140
- uint256 newOwnerTokens = jbTokens().totalBalanceOf(newOwner, revnetId);
141
- assertEq(newOwnerTokens, halfCollateral, "half collateral should be returned to newOwner");
142
- }
143
- }