@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.
- package/CHANGELOG.md +2 -2
- package/README.md +6 -7
- package/foundry.toml +1 -1
- package/package.json +23 -16
- package/references/operations.md +1 -1
- package/references/runtime.md +1 -1
- package/script/Deploy.s.sol +12 -9
- package/src/REVDeployer.sol +69 -67
- package/src/REVHiddenTokens.sol +2 -2
- package/src/REVLoans.sol +26 -22
- package/src/REVOwner.sol +147 -29
- package/src/interfaces/IREVDeployer.sol +2 -1
- package/src/interfaces/IREVHiddenTokens.sol +4 -1
- package/src/interfaces/IREVOwner.sol +5 -0
- package/src/structs/REVAutoIssuance.sol +4 -2
- package/src/structs/REVConfig.sol +8 -5
- package/src/structs/REVDescription.sol +6 -5
- package/src/structs/REVLoan.sol +8 -5
- package/src/structs/REVStageConfig.sol +14 -16
- package/ADMINISTRATION.md +0 -73
- package/ARCHITECTURE.md +0 -116
- package/AUDIT_INSTRUCTIONS.md +0 -90
- package/RISKS.md +0 -107
- package/SKILLS.md +0 -46
- package/STYLE_GUIDE.md +0 -610
- package/USER_JOURNEYS.md +0 -195
- package/foundry.lock +0 -11
- package/slither-ci.config.json +0 -10
- package/sphinx.lock +0 -507
- package/test/REV.integrations.t.sol +0 -573
- package/test/REVAutoIssuanceFuzz.t.sol +0 -328
- package/test/REVDeployerRegressions.t.sol +0 -396
- package/test/REVInvincibility.t.sol +0 -1371
- package/test/REVInvincibilityHandler.sol +0 -387
- package/test/REVLifecycle.t.sol +0 -420
- package/test/REVLoans.invariants.t.sol +0 -724
- package/test/REVLoansAttacks.t.sol +0 -816
- package/test/REVLoansFeeRecovery.t.sol +0 -783
- package/test/REVLoansFindings.t.sol +0 -711
- package/test/REVLoansRegressions.t.sol +0 -364
- package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
- package/test/REVLoansSourced.t.sol +0 -1839
- package/test/REVLoansUnSourced.t.sol +0 -409
- package/test/TestAuditFixVerification.t.sol +0 -675
- package/test/TestBurnHeldTokens.t.sol +0 -394
- package/test/TestCEIPattern.t.sol +0 -508
- package/test/TestCashOutCallerValidation.t.sol +0 -452
- package/test/TestConversionDocumentation.t.sol +0 -365
- package/test/TestCrossCurrencyReclaim.t.sol +0 -610
- package/test/TestCrossSourceReallocation.t.sol +0 -361
- package/test/TestERC2771MetaTx.t.sol +0 -585
- package/test/TestEmptyBuybackSpecs.t.sol +0 -300
- package/test/TestFlashLoanSurplus.t.sol +0 -365
- package/test/TestHiddenTokens.t.sol +0 -474
- package/test/TestHookArrayOOB.t.sol +0 -278
- package/test/TestLiquidationBehavior.t.sol +0 -398
- package/test/TestLoanSourceRotation.t.sol +0 -553
- package/test/TestLoansCashOutDelay.t.sol +0 -493
- package/test/TestLongTailEconomics.t.sol +0 -677
- package/test/TestLowFindings.t.sol +0 -677
- package/test/TestMixedFixes.t.sol +0 -593
- package/test/TestPermit2Signatures.t.sol +0 -683
- package/test/TestReallocationSandwich.t.sol +0 -412
- package/test/TestRevnetRegressions.t.sol +0 -350
- package/test/TestSplitWeightAdjustment.t.sol +0 -527
- package/test/TestSplitWeightE2E.t.sol +0 -605
- package/test/TestSplitWeightFork.t.sol +0 -855
- package/test/TestStageTransitionBorrowable.t.sol +0 -301
- package/test/TestSwapTerminalPermission.t.sol +0 -262
- package/test/TestTerminalEncodingInHash.t.sol +0 -326
- package/test/TestUint112Overflow.t.sol +0 -311
- package/test/TestZeroAmountLoanGuard.t.sol +0 -378
- package/test/TestZeroRepayment.t.sol +0 -354
- package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
- package/test/audit/HiddenSupplyCashout.t.sol +0 -61
- package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
- package/test/audit/NemesisVerification.t.sol +0 -97
- package/test/audit/OperatorDelegation.t.sol +0 -356
- package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
- package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
- package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
- package/test/audit/ReallocatePermission.t.sol +0 -363
- package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
- package/test/audit/SupportsInterfaceTest.t.sol +0 -51
- package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
- package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
- package/test/fork/ForkTestBase.sol +0 -727
- package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
- package/test/fork/TestCashOutFork.t.sol +0 -253
- package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
- package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
- package/test/fork/TestLoanBorrowFork.t.sol +0 -163
- package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
- package/test/fork/TestLoanERC20Fork.t.sol +0 -459
- package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
- package/test/fork/TestLoanReallocateFork.t.sol +0 -113
- package/test/fork/TestLoanRepayFork.t.sol +0 -188
- package/test/fork/TestLoanTransferFork.t.sol +0 -143
- package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
- package/test/fork/TestSplitWeightFork.t.sol +0 -189
- package/test/helpers/MaliciousContracts.sol +0 -247
- package/test/helpers/REVEmpty721Config.sol +0 -45
- package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
- package/test/mock/MockBuybackDataHook.sol +0 -112
- package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
- package/test/mock/MockSuckerRegistry.sol +0 -17
- package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
- package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
- package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
- package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
- package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
- 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
|
-
}
|