@rev-net/core-v6 0.0.36 → 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 (101) 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 +134 -90
  11. package/src/REVOwner.sol +124 -17
  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 -97
  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 -368
  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/CodexCrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/CodexPhantomSurplusTerminal.t.sol +0 -367
  71. package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -142
  72. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  73. package/test/audit/NemesisOperatorDelegation.t.sol +0 -356
  74. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  75. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  76. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  77. package/test/fork/ForkTestBase.sol +0 -727
  78. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  79. package/test/fork/TestCashOutFork.t.sol +0 -253
  80. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  81. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  82. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  83. package/test/fork/TestLoanERC20Fork.t.sol +0 -465
  84. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  85. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  86. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  87. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  88. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  89. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  90. package/test/helpers/MaliciousContracts.sol +0 -247
  91. package/test/helpers/REVEmpty721Config.sol +0 -45
  92. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  93. package/test/mock/MockBuybackDataHook.sol +0 -112
  94. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  95. package/test/mock/MockSuckerRegistry.sol +0 -17
  96. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  97. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  98. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  99. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  100. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  101. 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
- }