@rev-net/core-v6 0.0.37 → 0.0.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +60 -65
- package/src/REVHiddenTokens.sol +2 -2
- package/src/REVLoans.sol +17 -10
- package/src/REVOwner.sol +121 -14
- package/src/interfaces/IREVDeployer.sol +2 -1
- package/src/interfaces/IREVHiddenTokens.sol +4 -1
- package/src/interfaces/IREVOwner.sol +5 -0
- 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,576 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
pragma solidity 0.8.28;
|
|
3
|
-
|
|
4
|
-
/// @title TestLoansAndDeployerFixes
|
|
5
|
-
/// @notice Regression tests for approval cleanup, stale source skip, and stage ordering.
|
|
6
|
-
|
|
7
|
-
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
8
|
-
import {IJBDirectory} from "@bananapus/core-v6/src/interfaces/IJBDirectory.sol";
|
|
9
|
-
import {IJBTerminal} from "@bananapus/core-v6/src/interfaces/IJBTerminal.sol";
|
|
10
|
-
import {IJBPayoutTerminal} from "@bananapus/core-v6/src/interfaces/IJBPayoutTerminal.sol";
|
|
11
|
-
import {IJBPermissions} from "@bananapus/core-v6/src/interfaces/IJBPermissions.sol";
|
|
12
|
-
import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
|
|
13
|
-
import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
|
|
14
|
-
import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
|
|
15
|
-
import {JBSplit} from "@bananapus/core-v6/src/structs/JBSplit.sol";
|
|
16
|
-
import {JBTerminalConfig} from "@bananapus/core-v6/src/structs/JBTerminalConfig.sol";
|
|
17
|
-
import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
|
|
18
|
-
import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
|
|
19
|
-
import {REVLoan} from "../../src/structs/REVLoan.sol";
|
|
20
|
-
import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
|
|
21
|
-
import {REVConfig} from "../../src/structs/REVConfig.sol";
|
|
22
|
-
import {REVDescription} from "../../src/structs/REVDescription.sol";
|
|
23
|
-
import {REVSuckerDeploymentConfig} from "../../src/structs/REVSuckerDeploymentConfig.sol";
|
|
24
|
-
import {REVDeployer} from "../../src/REVDeployer.sol";
|
|
25
|
-
import {REVLoans} from "../../src/REVLoans.sol";
|
|
26
|
-
import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
|
|
27
|
-
import {REVLoansFeeRecovery, FeeRecoveryProjectConfig} from "../REVLoansFeeRecovery.t.sol";
|
|
28
|
-
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// Main test contract — extends REVLoansFeeRecovery to reuse full setup.
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
|
-
contract TestLoansAndDeployerFixes is REVLoansFeeRecovery {
|
|
34
|
-
// ========================================================================
|
|
35
|
-
// Helpers
|
|
36
|
-
// ========================================================================
|
|
37
|
-
|
|
38
|
-
/// @notice Mock the burn-tokens permission without requiring it to be called.
|
|
39
|
-
/// Use this instead of _mockLoanPermission when the borrow may revert before
|
|
40
|
-
/// the burn is reached, or in other cases where vm.expectCall would cause a
|
|
41
|
-
/// spurious failure.
|
|
42
|
-
function _mockLoanPermissionNoExpect(address user) internal {
|
|
43
|
-
vm.mockCall(
|
|
44
|
-
address(jbPermissions()),
|
|
45
|
-
abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
|
|
46
|
-
abi.encode(true)
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/// @notice Mock the repay-loan permission without requiring it to be called.
|
|
51
|
-
/// repayLoan calls _requirePermissionFrom(loanOwner, ..., REPAY_LOAN), but when
|
|
52
|
-
/// sender == loanOwner the hasPermission call is skipped entirely. We still mock
|
|
53
|
-
/// the call in case the path changes, but do not set expectCall.
|
|
54
|
-
function _mockRepayPermissionNoExpect(address user) internal {
|
|
55
|
-
vm.mockCall(
|
|
56
|
-
address(jbPermissions()),
|
|
57
|
-
abi.encodeCall(IJBPermissions.hasPermission, (user, user, REVNET_ID, 12, true, true)),
|
|
58
|
-
abi.encode(true)
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// ========================================================================
|
|
63
|
-
// Stale ERC20 Approval Cleanup
|
|
64
|
-
// ========================================================================
|
|
65
|
-
// After _tryPayFee (in _addTo) or _removeFrom, the ERC20 allowance from
|
|
66
|
-
// the LOANS_CONTRACT to the terminal must be zero. The _afterTransferTo
|
|
67
|
-
// call now force-approves to 0 after successful transfers, and the catch
|
|
68
|
-
// block in _tryPayFee uses safeDecreaseAllowance on failure.
|
|
69
|
-
// ========================================================================
|
|
70
|
-
|
|
71
|
-
/// @notice After an ERC20 borrow, the allowance from LOANS_CONTRACT to the terminal is 0.
|
|
72
|
-
function test_erc20BorrowLeavesZeroAllowanceToTerminal() public {
|
|
73
|
-
uint256 payAmount = 1_000_000; // 6 decimals for TOKEN
|
|
74
|
-
deal(address(TOKEN), USER, payAmount);
|
|
75
|
-
|
|
76
|
-
// Pay into revnet with ERC-20.
|
|
77
|
-
vm.startPrank(USER);
|
|
78
|
-
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
79
|
-
uint256 tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
80
|
-
vm.stopPrank();
|
|
81
|
-
|
|
82
|
-
_mockLoanPermission(USER);
|
|
83
|
-
REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
84
|
-
|
|
85
|
-
vm.prank(USER);
|
|
86
|
-
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
|
|
87
|
-
|
|
88
|
-
// Allowance to the terminal must be zero after successful borrow.
|
|
89
|
-
assertEq(
|
|
90
|
-
IERC20(address(TOKEN)).allowance(address(LOANS_CONTRACT), address(jbMultiTerminal())),
|
|
91
|
-
0,
|
|
92
|
-
"Stale allowance to terminal after ERC20 borrow"
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/// @notice After a native ETH borrow, verify no stale state (native token has no allowance concept, but
|
|
97
|
-
/// the loans contract balance must be zero).
|
|
98
|
-
function test_nativeBorrowLeavesNoFundsStuck() public {
|
|
99
|
-
(, uint256 balanceBefore, uint256 balanceAfter) = _borrowNative(USER, 10e18, 25);
|
|
100
|
-
|
|
101
|
-
uint256 received = balanceAfter - balanceBefore;
|
|
102
|
-
assertGt(received, 0, "Borrower should receive ETH");
|
|
103
|
-
|
|
104
|
-
// No ETH stuck in the loans contract.
|
|
105
|
-
assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract after native borrow");
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// @notice After an ERC20 borrow where the fee terminal reverts, the allowance from
|
|
109
|
-
/// LOANS_CONTRACT to the fee terminal is also 0 (catch block cleans it up).
|
|
110
|
-
function test_erc20BorrowWithRevertingFeeTerminalCleansAllowance() public {
|
|
111
|
-
// Mock the fee terminal to revert for TOKEN.
|
|
112
|
-
_mockRevertingFeeTerminal(address(TOKEN));
|
|
113
|
-
|
|
114
|
-
uint256 payAmount = 1_000_000;
|
|
115
|
-
deal(address(TOKEN), USER, payAmount);
|
|
116
|
-
|
|
117
|
-
vm.startPrank(USER);
|
|
118
|
-
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
119
|
-
uint256 tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
120
|
-
vm.stopPrank();
|
|
121
|
-
|
|
122
|
-
_mockLoanPermission(USER);
|
|
123
|
-
REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
124
|
-
|
|
125
|
-
vm.prank(USER);
|
|
126
|
-
LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
|
|
127
|
-
|
|
128
|
-
// Allowance to the REVERTING terminal must be 0 (catch block cleaned it up).
|
|
129
|
-
assertEq(
|
|
130
|
-
IERC20(address(TOKEN)).allowance(address(LOANS_CONTRACT), address(REVERTING_TERMINAL)),
|
|
131
|
-
0,
|
|
132
|
-
"Stale allowance to reverting fee terminal after borrow"
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
// Allowance to the regular terminal must also be 0.
|
|
136
|
-
assertEq(
|
|
137
|
-
IERC20(address(TOKEN)).allowance(address(LOANS_CONTRACT), address(jbMultiTerminal())),
|
|
138
|
-
0,
|
|
139
|
-
"Stale allowance to terminal after borrow with reverting fee terminal"
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
// No tokens stuck.
|
|
143
|
-
assertEq(IERC20(address(TOKEN)).balanceOf(address(LOANS_CONTRACT)), 0, "No ERC20 stuck in loans contract");
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/// @notice After a full ERC20 loan repayment (_removeFrom path), the allowance to the terminal is 0.
|
|
147
|
-
function test_erc20RepaymentLeavesZeroAllowance() public {
|
|
148
|
-
uint256 payAmount = 1_000_000;
|
|
149
|
-
deal(address(TOKEN), USER, payAmount * 2); // Extra for repayment
|
|
150
|
-
|
|
151
|
-
// Pay into revnet.
|
|
152
|
-
vm.startPrank(USER);
|
|
153
|
-
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
154
|
-
uint256 tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
155
|
-
vm.stopPrank();
|
|
156
|
-
|
|
157
|
-
_mockLoanPermission(USER);
|
|
158
|
-
REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
159
|
-
|
|
160
|
-
// Borrow.
|
|
161
|
-
vm.prank(USER);
|
|
162
|
-
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
|
|
163
|
-
|
|
164
|
-
// Read loan details to get repay amount.
|
|
165
|
-
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
166
|
-
|
|
167
|
-
// Mock repay permission (no expectCall — sender == loanOwner so hasPermission is skipped).
|
|
168
|
-
_mockRepayPermissionNoExpect(USER);
|
|
169
|
-
|
|
170
|
-
// Approve the loans contract to pull tokens for repayment via permit2 or direct transfer.
|
|
171
|
-
uint256 maxRepay = loan.amount * 2; // Generous max to cover fees.
|
|
172
|
-
deal(address(TOKEN), USER, maxRepay);
|
|
173
|
-
vm.startPrank(USER);
|
|
174
|
-
TOKEN.approve(address(LOANS_CONTRACT), maxRepay);
|
|
175
|
-
LOANS_CONTRACT.repayLoan({
|
|
176
|
-
loanId: loanId,
|
|
177
|
-
maxRepayBorrowAmount: maxRepay,
|
|
178
|
-
collateralCountToReturn: loan.collateral,
|
|
179
|
-
beneficiary: payable(USER),
|
|
180
|
-
allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
181
|
-
});
|
|
182
|
-
vm.stopPrank();
|
|
183
|
-
|
|
184
|
-
// After repayment (_removeFrom), allowance to terminal must be 0.
|
|
185
|
-
assertEq(
|
|
186
|
-
IERC20(address(TOKEN)).allowance(address(LOANS_CONTRACT), address(jbMultiTerminal())),
|
|
187
|
-
0,
|
|
188
|
-
"Stale allowance to terminal after ERC20 repayment"
|
|
189
|
-
);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// ========================================================================
|
|
193
|
-
// Stale Loan Source DoS Prevention
|
|
194
|
-
// ========================================================================
|
|
195
|
-
// _totalBorrowedFrom must skip sources with zero balance (totalBorrowedFrom == 0)
|
|
196
|
-
// BEFORE calling accountingContextForTokenOf on the terminal. This prevents DoS
|
|
197
|
-
// when a stale terminal starts reverting.
|
|
198
|
-
// ========================================================================
|
|
199
|
-
|
|
200
|
-
/// @notice After fully repaying a loan, if the source terminal starts reverting on
|
|
201
|
-
/// accountingContextForTokenOf, subsequent borrows from other sources still work.
|
|
202
|
-
function test_staleLoanSourceDoesNotBlockNewBorrows() public {
|
|
203
|
-
// Step 1: Borrow from native ETH source.
|
|
204
|
-
vm.prank(USER);
|
|
205
|
-
uint256 nativeTokens =
|
|
206
|
-
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
207
|
-
|
|
208
|
-
_mockLoanPermission(USER);
|
|
209
|
-
REVLoanSource memory nativeSource =
|
|
210
|
-
REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
211
|
-
|
|
212
|
-
vm.prank(USER);
|
|
213
|
-
(uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, nativeSource, 0, nativeTokens, payable(USER), 25, USER);
|
|
214
|
-
|
|
215
|
-
// Step 2: Fully repay the native loan so totalBorrowedFrom goes to 0.
|
|
216
|
-
REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
|
|
217
|
-
|
|
218
|
-
// Mock repay permission (no expectCall — sender == loanOwner so hasPermission is skipped).
|
|
219
|
-
_mockRepayPermissionNoExpect(USER);
|
|
220
|
-
|
|
221
|
-
uint256 maxRepay = loan.amount * 2;
|
|
222
|
-
vm.deal(USER, USER.balance + maxRepay);
|
|
223
|
-
vm.prank(USER);
|
|
224
|
-
LOANS_CONTRACT.repayLoan{value: maxRepay}({
|
|
225
|
-
loanId: loanId,
|
|
226
|
-
maxRepayBorrowAmount: maxRepay,
|
|
227
|
-
collateralCountToReturn: loan.collateral,
|
|
228
|
-
beneficiary: payable(USER),
|
|
229
|
-
allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
|
|
230
|
-
});
|
|
231
|
-
|
|
232
|
-
// Confirm the totalBorrowedFrom for native source is now 0.
|
|
233
|
-
assertEq(
|
|
234
|
-
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
|
|
235
|
-
0,
|
|
236
|
-
"totalBorrowedFrom should be 0 after full repay"
|
|
237
|
-
);
|
|
238
|
-
|
|
239
|
-
// Step 3: Mock the terminal to revert on accountingContextForTokenOf for native token.
|
|
240
|
-
// This simulates a stale terminal that has been removed or broken.
|
|
241
|
-
vm.mockCallRevert(
|
|
242
|
-
address(jbMultiTerminal()),
|
|
243
|
-
abi.encodeWithSelector(
|
|
244
|
-
IJBTerminal.accountingContextForTokenOf.selector, REVNET_ID, JBConstants.NATIVE_TOKEN
|
|
245
|
-
),
|
|
246
|
-
"terminal removed"
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
// Step 4: Pay into revnet with ERC20 and borrow from ERC20 source.
|
|
250
|
-
// The _totalBorrowedFrom loop should skip the native source (balance is 0)
|
|
251
|
-
// without calling accountingContextForTokenOf on it.
|
|
252
|
-
uint256 payAmount = 1_000_000;
|
|
253
|
-
deal(address(TOKEN), USER, payAmount);
|
|
254
|
-
|
|
255
|
-
// We need to clear the mock for ERC20-related calls on the terminal.
|
|
256
|
-
// The mock only targets native token, so ERC20 calls should still work.
|
|
257
|
-
vm.startPrank(USER);
|
|
258
|
-
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
259
|
-
uint256 erc20Tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
260
|
-
vm.stopPrank();
|
|
261
|
-
|
|
262
|
-
_mockLoanPermission(USER);
|
|
263
|
-
REVLoanSource memory erc20Source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
264
|
-
|
|
265
|
-
// This should NOT revert despite the native source terminal reverting on accountingContextForTokenOf.
|
|
266
|
-
vm.prank(USER);
|
|
267
|
-
LOANS_CONTRACT.borrowFrom(REVNET_ID, erc20Source, 0, erc20Tokens, payable(USER), 25, USER);
|
|
268
|
-
|
|
269
|
-
// If we got here, the zero-balance source was successfully skipped.
|
|
270
|
-
assertGt(
|
|
271
|
-
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), address(TOKEN)),
|
|
272
|
-
0,
|
|
273
|
-
"ERC20 borrow should succeed despite stale native source"
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/// @notice Verify that _totalBorrowedFrom correctly counts non-zero sources.
|
|
278
|
-
function test_nonZeroSourcesStillCounted() public {
|
|
279
|
-
// Borrow from native source (leave it outstanding).
|
|
280
|
-
vm.prank(USER);
|
|
281
|
-
uint256 nativeTokens =
|
|
282
|
-
jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
|
|
283
|
-
|
|
284
|
-
_mockLoanPermission(USER);
|
|
285
|
-
REVLoanSource memory nativeSource =
|
|
286
|
-
REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
|
|
287
|
-
|
|
288
|
-
vm.prank(USER);
|
|
289
|
-
LOANS_CONTRACT.borrowFrom(REVNET_ID, nativeSource, 0, nativeTokens, payable(USER), 25, USER);
|
|
290
|
-
|
|
291
|
-
// Confirm totalBorrowedFrom is non-zero.
|
|
292
|
-
assertGt(
|
|
293
|
-
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
|
|
294
|
-
0,
|
|
295
|
-
"totalBorrowedFrom should be non-zero for outstanding loan"
|
|
296
|
-
);
|
|
297
|
-
|
|
298
|
-
// Now borrow from ERC20 source as well — _totalBorrowedFrom should read both.
|
|
299
|
-
uint256 payAmount = 1_000_000;
|
|
300
|
-
deal(address(TOKEN), USER, payAmount);
|
|
301
|
-
vm.startPrank(USER);
|
|
302
|
-
TOKEN.approve(address(jbMultiTerminal()), payAmount);
|
|
303
|
-
uint256 erc20Tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
|
|
304
|
-
vm.stopPrank();
|
|
305
|
-
|
|
306
|
-
_mockLoanPermission(USER);
|
|
307
|
-
REVLoanSource memory erc20Source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
|
|
308
|
-
|
|
309
|
-
// This call internally invokes _totalBorrowedFrom which reads both sources.
|
|
310
|
-
// If it incorrectly skips non-zero sources, the borrowable amount calculation would be wrong.
|
|
311
|
-
vm.prank(USER);
|
|
312
|
-
LOANS_CONTRACT.borrowFrom(REVNET_ID, erc20Source, 0, erc20Tokens, payable(USER), 25, USER);
|
|
313
|
-
|
|
314
|
-
assertGt(
|
|
315
|
-
LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), address(TOKEN)),
|
|
316
|
-
0,
|
|
317
|
-
"ERC20 borrow should record totalBorrowedFrom"
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// ========================================================================
|
|
322
|
-
// Cross-Chain startsAtOrAfter Normalization
|
|
323
|
-
// ========================================================================
|
|
324
|
-
// When stage 0 has startsAtOrAfter=0, it is normalized to block.timestamp.
|
|
325
|
-
// Stage 1 must have startsAtOrAfter > block.timestamp (the normalized value),
|
|
326
|
-
// otherwise REVDeployer_StageTimesMustIncrease is reverted.
|
|
327
|
-
// ========================================================================
|
|
328
|
-
|
|
329
|
-
/// @notice Deploying a revnet where stage 0 startsAtOrAfter=0 and stage 1 startsAtOrAfter=1
|
|
330
|
-
/// (less than block.timestamp) must revert with REVDeployer_StageTimesMustIncrease.
|
|
331
|
-
function test_stageTimesRevertWhenStage1BeforeBlockTimestamp() public {
|
|
332
|
-
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
|
|
333
|
-
uint256 decimalMultiplier = 10 ** 18;
|
|
334
|
-
|
|
335
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
336
|
-
splits[0].beneficiary = payable(multisig());
|
|
337
|
-
splits[0].percent = 10_000;
|
|
338
|
-
|
|
339
|
-
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
340
|
-
issuanceConfs[0] = REVAutoIssuance({
|
|
341
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
342
|
-
chainId: uint32(block.chainid),
|
|
343
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
344
|
-
count: uint104(70_000 * decimalMultiplier),
|
|
345
|
-
beneficiary: multisig()
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
// Stage 0: startsAtOrAfter = 0 (normalized to block.timestamp).
|
|
349
|
-
stageConfigurations[0] = REVStageConfig({
|
|
350
|
-
startsAtOrAfter: 0, // Normalized to block.timestamp
|
|
351
|
-
autoIssuances: issuanceConfs,
|
|
352
|
-
splitPercent: 2000,
|
|
353
|
-
splits: splits,
|
|
354
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
355
|
-
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
356
|
-
issuanceCutFrequency: 90 days,
|
|
357
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
358
|
-
cashOutTaxRate: 6000,
|
|
359
|
-
extraMetadata: 0
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
// Stage 1: startsAtOrAfter = 1 (definitely < block.timestamp).
|
|
363
|
-
stageConfigurations[1] = REVStageConfig({
|
|
364
|
-
startsAtOrAfter: 1, // 1 < block.timestamp, should fail
|
|
365
|
-
autoIssuances: issuanceConfs,
|
|
366
|
-
splitPercent: 2000,
|
|
367
|
-
splits: splits,
|
|
368
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
369
|
-
initialIssuance: uint112(500 * decimalMultiplier),
|
|
370
|
-
issuanceCutFrequency: 90 days,
|
|
371
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
372
|
-
cashOutTaxRate: 6000,
|
|
373
|
-
extraMetadata: 0
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
377
|
-
accountingContextsToAccept[0] = JBAccountingContext({
|
|
378
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
382
|
-
terminalConfigurations[0] =
|
|
383
|
-
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
384
|
-
|
|
385
|
-
REVConfig memory config = REVConfig({
|
|
386
|
-
description: REVDescription({
|
|
387
|
-
name: "StageOrderTest", ticker: "$SOT", uri: "ipfs://test", salt: "STAGE_ORDER_SALT_REVERT"
|
|
388
|
-
}),
|
|
389
|
-
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
390
|
-
splitOperator: multisig(),
|
|
391
|
-
stageConfigurations: stageConfigurations
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
REVSuckerDeploymentConfig memory suckerConfig = REVSuckerDeploymentConfig({
|
|
395
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0),
|
|
396
|
-
salt: keccak256(abi.encodePacked("STAGE_ORDER_REVERT"))
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
// This should revert because stage 1 start (1) < block.timestamp (the normalized stage 0 start).
|
|
400
|
-
vm.expectRevert(abi.encodeWithSelector(REVDeployer.REVDeployer_StageTimesMustIncrease.selector));
|
|
401
|
-
REV_DEPLOYER.deployFor({
|
|
402
|
-
revnetId: 0,
|
|
403
|
-
configuration: config,
|
|
404
|
-
terminalConfigurations: terminalConfigurations,
|
|
405
|
-
suckerDeploymentConfiguration: suckerConfig,
|
|
406
|
-
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
407
|
-
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
/// @notice Deploying with stage 0 startsAtOrAfter=0 and stage 1 startsAtOrAfter > block.timestamp
|
|
412
|
-
/// should succeed.
|
|
413
|
-
function test_stageTimesSucceedWhenStage1AfterBlockTimestamp() public {
|
|
414
|
-
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
|
|
415
|
-
uint256 decimalMultiplier = 10 ** 18;
|
|
416
|
-
|
|
417
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
418
|
-
splits[0].beneficiary = payable(multisig());
|
|
419
|
-
splits[0].percent = 10_000;
|
|
420
|
-
|
|
421
|
-
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
422
|
-
issuanceConfs[0] = REVAutoIssuance({
|
|
423
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
424
|
-
chainId: uint32(block.chainid),
|
|
425
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
426
|
-
count: uint104(70_000 * decimalMultiplier),
|
|
427
|
-
beneficiary: multisig()
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
// Stage 0: startsAtOrAfter = 0 (normalized to block.timestamp).
|
|
431
|
-
stageConfigurations[0] = REVStageConfig({
|
|
432
|
-
startsAtOrAfter: 0,
|
|
433
|
-
autoIssuances: issuanceConfs,
|
|
434
|
-
splitPercent: 2000,
|
|
435
|
-
splits: splits,
|
|
436
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
437
|
-
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
438
|
-
issuanceCutFrequency: 90 days,
|
|
439
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
440
|
-
cashOutTaxRate: 6000,
|
|
441
|
-
extraMetadata: 0
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Stage 1: startsAtOrAfter = block.timestamp + 100 (after normalized stage 0 start).
|
|
445
|
-
stageConfigurations[1] = REVStageConfig({
|
|
446
|
-
startsAtOrAfter: uint40(block.timestamp + 100),
|
|
447
|
-
autoIssuances: issuanceConfs,
|
|
448
|
-
splitPercent: 2000,
|
|
449
|
-
splits: splits,
|
|
450
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
451
|
-
initialIssuance: uint112(500 * decimalMultiplier),
|
|
452
|
-
issuanceCutFrequency: 90 days,
|
|
453
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
454
|
-
cashOutTaxRate: 6000,
|
|
455
|
-
extraMetadata: 0
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
459
|
-
accountingContextsToAccept[0] = JBAccountingContext({
|
|
460
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
464
|
-
terminalConfigurations[0] =
|
|
465
|
-
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
466
|
-
|
|
467
|
-
REVConfig memory config = REVConfig({
|
|
468
|
-
description: REVDescription({
|
|
469
|
-
name: "StageOrderTestOK", ticker: "$SOTOK", uri: "ipfs://test", salt: "STAGE_ORDER_SALT_SUCCESS"
|
|
470
|
-
}),
|
|
471
|
-
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
472
|
-
splitOperator: multisig(),
|
|
473
|
-
stageConfigurations: stageConfigurations
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
REVSuckerDeploymentConfig memory suckerConfig = REVSuckerDeploymentConfig({
|
|
477
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0),
|
|
478
|
-
salt: keccak256(abi.encodePacked("STAGE_ORDER_SUCCESS"))
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
// This should succeed because stage 1 start > block.timestamp (the normalized stage 0 start).
|
|
482
|
-
(uint256 newRevnetId,) = REV_DEPLOYER.deployFor({
|
|
483
|
-
revnetId: 0,
|
|
484
|
-
configuration: config,
|
|
485
|
-
terminalConfigurations: terminalConfigurations,
|
|
486
|
-
suckerDeploymentConfiguration: suckerConfig,
|
|
487
|
-
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
488
|
-
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
assertGt(newRevnetId, 0, "Deployment should succeed and return a valid revnet ID");
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/// @notice Stage 1 startsAtOrAfter == block.timestamp (equal to normalized stage 0) must also revert
|
|
495
|
-
/// because the check is strictly greater-than.
|
|
496
|
-
function test_stageTimesRevertWhenStage1EqualsBlockTimestamp() public {
|
|
497
|
-
REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
|
|
498
|
-
uint256 decimalMultiplier = 10 ** 18;
|
|
499
|
-
|
|
500
|
-
JBSplit[] memory splits = new JBSplit[](1);
|
|
501
|
-
splits[0].beneficiary = payable(multisig());
|
|
502
|
-
splits[0].percent = 10_000;
|
|
503
|
-
|
|
504
|
-
REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
|
|
505
|
-
issuanceConfs[0] = REVAutoIssuance({
|
|
506
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
507
|
-
chainId: uint32(block.chainid),
|
|
508
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
509
|
-
count: uint104(70_000 * decimalMultiplier),
|
|
510
|
-
beneficiary: multisig()
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
// Stage 0: startsAtOrAfter = 0 (normalized to block.timestamp).
|
|
514
|
-
stageConfigurations[0] = REVStageConfig({
|
|
515
|
-
startsAtOrAfter: 0,
|
|
516
|
-
autoIssuances: issuanceConfs,
|
|
517
|
-
splitPercent: 2000,
|
|
518
|
-
splits: splits,
|
|
519
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
520
|
-
initialIssuance: uint112(1000 * decimalMultiplier),
|
|
521
|
-
issuanceCutFrequency: 90 days,
|
|
522
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
523
|
-
cashOutTaxRate: 6000,
|
|
524
|
-
extraMetadata: 0
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// Stage 1: startsAtOrAfter = block.timestamp (same as normalized stage 0, must fail).
|
|
528
|
-
stageConfigurations[1] = REVStageConfig({
|
|
529
|
-
startsAtOrAfter: uint40(block.timestamp),
|
|
530
|
-
autoIssuances: issuanceConfs,
|
|
531
|
-
splitPercent: 2000,
|
|
532
|
-
splits: splits,
|
|
533
|
-
// forge-lint: disable-next-line(unsafe-typecast)
|
|
534
|
-
initialIssuance: uint112(500 * decimalMultiplier),
|
|
535
|
-
issuanceCutFrequency: 90 days,
|
|
536
|
-
issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
|
|
537
|
-
cashOutTaxRate: 6000,
|
|
538
|
-
extraMetadata: 0
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
|
|
542
|
-
accountingContextsToAccept[0] = JBAccountingContext({
|
|
543
|
-
token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
|
|
544
|
-
});
|
|
545
|
-
|
|
546
|
-
JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
|
|
547
|
-
terminalConfigurations[0] =
|
|
548
|
-
JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
|
|
549
|
-
|
|
550
|
-
REVConfig memory config = REVConfig({
|
|
551
|
-
description: REVDescription({
|
|
552
|
-
name: "StageOrderEqual", ticker: "$SOTEQ", uri: "ipfs://test", salt: "STAGE_ORDER_SALT_EQUAL"
|
|
553
|
-
}),
|
|
554
|
-
baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
|
|
555
|
-
splitOperator: multisig(),
|
|
556
|
-
stageConfigurations: stageConfigurations
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
REVSuckerDeploymentConfig memory suckerConfig = REVSuckerDeploymentConfig({
|
|
560
|
-
deployerConfigurations: new JBSuckerDeployerConfig[](0),
|
|
561
|
-
salt: keccak256(abi.encodePacked("STAGE_ORDER_EQUAL"))
|
|
562
|
-
});
|
|
563
|
-
|
|
564
|
-
// This should revert because stage 1 start == block.timestamp == normalized stage 0 start.
|
|
565
|
-
// The check is `effectiveStart <= previousStageStart`, so equality triggers revert.
|
|
566
|
-
vm.expectRevert(abi.encodeWithSelector(REVDeployer.REVDeployer_StageTimesMustIncrease.selector));
|
|
567
|
-
REV_DEPLOYER.deployFor({
|
|
568
|
-
revnetId: 0,
|
|
569
|
-
configuration: config,
|
|
570
|
-
terminalConfigurations: terminalConfigurations,
|
|
571
|
-
suckerDeploymentConfiguration: suckerConfig,
|
|
572
|
-
tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
|
|
573
|
-
allowedPosts: REVEmpty721Config.emptyAllowedPosts()
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
}
|