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