@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,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
|
-
}
|