@rev-net/core-v6 0.0.37 → 0.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +69 -67
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +26 -22
  11. package/src/REVOwner.sol +147 -29
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/src/structs/REVAutoIssuance.sol +4 -2
  16. package/src/structs/REVConfig.sol +8 -5
  17. package/src/structs/REVDescription.sol +6 -5
  18. package/src/structs/REVLoan.sol +8 -5
  19. package/src/structs/REVStageConfig.sol +14 -16
  20. package/ADMINISTRATION.md +0 -73
  21. package/ARCHITECTURE.md +0 -116
  22. package/AUDIT_INSTRUCTIONS.md +0 -90
  23. package/RISKS.md +0 -107
  24. package/SKILLS.md +0 -46
  25. package/STYLE_GUIDE.md +0 -610
  26. package/USER_JOURNEYS.md +0 -195
  27. package/foundry.lock +0 -11
  28. package/slither-ci.config.json +0 -10
  29. package/sphinx.lock +0 -507
  30. package/test/REV.integrations.t.sol +0 -573
  31. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  32. package/test/REVDeployerRegressions.t.sol +0 -396
  33. package/test/REVInvincibility.t.sol +0 -1371
  34. package/test/REVInvincibilityHandler.sol +0 -387
  35. package/test/REVLifecycle.t.sol +0 -420
  36. package/test/REVLoans.invariants.t.sol +0 -724
  37. package/test/REVLoansAttacks.t.sol +0 -816
  38. package/test/REVLoansFeeRecovery.t.sol +0 -783
  39. package/test/REVLoansFindings.t.sol +0 -711
  40. package/test/REVLoansRegressions.t.sol +0 -364
  41. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  42. package/test/REVLoansSourced.t.sol +0 -1839
  43. package/test/REVLoansUnSourced.t.sol +0 -409
  44. package/test/TestAuditFixVerification.t.sol +0 -675
  45. package/test/TestBurnHeldTokens.t.sol +0 -394
  46. package/test/TestCEIPattern.t.sol +0 -508
  47. package/test/TestCashOutCallerValidation.t.sol +0 -452
  48. package/test/TestConversionDocumentation.t.sol +0 -365
  49. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  50. package/test/TestCrossSourceReallocation.t.sol +0 -361
  51. package/test/TestERC2771MetaTx.t.sol +0 -585
  52. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  53. package/test/TestFlashLoanSurplus.t.sol +0 -365
  54. package/test/TestHiddenTokens.t.sol +0 -474
  55. package/test/TestHookArrayOOB.t.sol +0 -278
  56. package/test/TestLiquidationBehavior.t.sol +0 -398
  57. package/test/TestLoanSourceRotation.t.sol +0 -553
  58. package/test/TestLoansCashOutDelay.t.sol +0 -493
  59. package/test/TestLongTailEconomics.t.sol +0 -677
  60. package/test/TestLowFindings.t.sol +0 -677
  61. package/test/TestMixedFixes.t.sol +0 -593
  62. package/test/TestPermit2Signatures.t.sol +0 -683
  63. package/test/TestReallocationSandwich.t.sol +0 -412
  64. package/test/TestRevnetRegressions.t.sol +0 -350
  65. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  66. package/test/TestSplitWeightE2E.t.sol +0 -605
  67. package/test/TestSplitWeightFork.t.sol +0 -855
  68. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  69. package/test/TestSwapTerminalPermission.t.sol +0 -262
  70. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  71. package/test/TestUint112Overflow.t.sol +0 -311
  72. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  73. package/test/TestZeroRepayment.t.sol +0 -354
  74. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  75. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  76. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  77. package/test/audit/NemesisVerification.t.sol +0 -97
  78. package/test/audit/OperatorDelegation.t.sol +0 -356
  79. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  80. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  81. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  82. package/test/audit/ReallocatePermission.t.sol +0 -363
  83. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  84. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  85. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  86. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  87. package/test/fork/ForkTestBase.sol +0 -727
  88. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  89. package/test/fork/TestCashOutFork.t.sol +0 -253
  90. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  91. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  92. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  93. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  94. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  95. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  96. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  97. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  98. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  99. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  100. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  101. package/test/helpers/MaliciousContracts.sol +0 -247
  102. package/test/helpers/REVEmpty721Config.sol +0 -45
  103. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  104. package/test/mock/MockBuybackDataHook.sol +0 -112
  105. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  106. package/test/mock/MockSuckerRegistry.sol +0 -17
  107. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  108. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  109. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  110. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  111. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  112. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,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
- }