@rev-net/core-v6 0.0.16 → 0.0.18

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 (79) hide show
  1. package/ARCHITECTURE.md +1 -0
  2. package/AUDIT_INSTRUCTIONS.md +3 -1
  3. package/CHANGE_LOG.md +12 -3
  4. package/RISKS.md +4 -0
  5. package/SKILLS.md +5 -4
  6. package/STYLE_GUIDE.md +2 -2
  7. package/USER_JOURNEYS.md +3 -0
  8. package/foundry.toml +1 -1
  9. package/package.json +9 -9
  10. package/script/Deploy.s.sol +20 -17
  11. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  12. package/src/REVDeployer.sol +5 -2
  13. package/src/REVLoans.sol +37 -1
  14. package/src/structs/REVBaseline721HookConfig.sol +0 -2
  15. package/test/REV.integrations.t.sol +1 -1
  16. package/test/REVAutoIssuanceFuzz.t.sol +1 -1
  17. package/test/REVDeployerRegressions.t.sol +1 -1
  18. package/test/REVInvincibility.t.sol +1 -1
  19. package/test/REVInvincibilityHandler.sol +1 -1
  20. package/test/REVLifecycle.t.sol +1 -1
  21. package/test/REVLoans.invariants.t.sol +1 -1
  22. package/test/REVLoansAttacks.t.sol +1 -1
  23. package/test/REVLoansFeeRecovery.t.sol +1 -1
  24. package/test/REVLoansFindings.t.sol +1 -1
  25. package/test/REVLoansRegressions.t.sol +1 -1
  26. package/test/REVLoansSourceFeeRecovery.t.sol +1 -1
  27. package/test/REVLoansSourced.t.sol +1 -1
  28. package/test/REVLoansUnSourced.t.sol +1 -1
  29. package/test/TestBurnHeldTokens.t.sol +1 -1
  30. package/test/TestCEIPattern.t.sol +1 -1
  31. package/test/TestCashOutCallerValidation.t.sol +1 -1
  32. package/test/TestConversionDocumentation.t.sol +1 -1
  33. package/test/TestCrossCurrencyReclaim.t.sol +1 -1
  34. package/test/TestCrossSourceReallocation.t.sol +1 -1
  35. package/test/TestERC2771MetaTx.t.sol +1 -1
  36. package/test/TestEmptyBuybackSpecs.t.sol +1 -1
  37. package/test/TestFlashLoanSurplus.t.sol +1 -1
  38. package/test/TestHookArrayOOB.t.sol +1 -1
  39. package/test/TestLiquidationBehavior.t.sol +1 -1
  40. package/test/TestLoanSourceRotation.t.sol +1 -1
  41. package/test/TestLoansCashOutDelay.t.sol +467 -0
  42. package/test/TestLongTailEconomics.t.sol +1 -1
  43. package/test/TestLowFindings.t.sol +1 -1
  44. package/test/TestMixedFixes.t.sol +1 -1
  45. package/test/TestPermit2Signatures.t.sol +1 -1
  46. package/test/TestReallocationSandwich.t.sol +1 -1
  47. package/test/TestRevnetRegressions.t.sol +1 -1
  48. package/test/TestSplitWeightAdjustment.t.sol +1 -1
  49. package/test/TestSplitWeightE2E.t.sol +1 -2
  50. package/test/TestSplitWeightFork.t.sol +1 -2
  51. package/test/TestStageTransitionBorrowable.t.sol +1 -1
  52. package/test/TestSwapTerminalPermission.t.sol +1 -1
  53. package/test/TestUint112Overflow.t.sol +1 -1
  54. package/test/TestZeroRepayment.t.sol +1 -1
  55. package/test/audit/LoanIdOverflowGuard.t.sol +1 -1
  56. package/test/fork/ForkTestBase.sol +1 -2
  57. package/test/fork/TestAutoIssuanceFork.t.sol +1 -1
  58. package/test/fork/TestCashOutFork.t.sol +1 -1
  59. package/test/fork/TestIssuanceDecayFork.t.sol +1 -1
  60. package/test/fork/TestLoanBorrowFork.t.sol +1 -1
  61. package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
  62. package/test/fork/TestLoanERC20Fork.t.sol +1 -1
  63. package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
  64. package/test/fork/TestLoanReallocateFork.t.sol +1 -1
  65. package/test/fork/TestLoanRepayFork.t.sol +1 -1
  66. package/test/fork/TestLoanTransferFork.t.sol +1 -1
  67. package/test/fork/TestPermit2PaymentFork.t.sol +1 -1
  68. package/test/fork/TestSplitWeightFork.t.sol +1 -1
  69. package/test/helpers/MaliciousContracts.sol +1 -1
  70. package/test/helpers/REVEmpty721Config.sol +0 -1
  71. package/test/mock/MockBuybackCashOutRecorder.sol +1 -1
  72. package/test/mock/MockBuybackDataHook.sol +1 -1
  73. package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
  74. package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
  75. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +1 -1
  76. package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
  77. package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
  78. package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
  79. package/test/regression/TestZeroPriceFeed.t.sol +1 -1
@@ -0,0 +1,467 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
+ import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
+ // forge-lint: disable-next-line(unaliased-plain-import)
9
+ import /* {*} from */ "./../src/REVDeployer.sol";
10
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
+ import "@croptop/core-v6/src/CTPublisher.sol";
12
+ import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
+
14
+ // forge-lint: disable-next-line(unaliased-plain-import)
15
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
16
+ // forge-lint: disable-next-line(unaliased-plain-import)
17
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
18
+ // forge-lint: disable-next-line(unaliased-plain-import)
19
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
20
+ // forge-lint: disable-next-line(unaliased-plain-import)
21
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
22
+ // forge-lint: disable-next-line(unaliased-plain-import)
23
+ import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
24
+
25
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
26
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
+ import {REVLoans} from "../src/REVLoans.sol";
28
+ import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
29
+ import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
30
+ import {REVDescription} from "../src/structs/REVDescription.sol";
31
+ import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
32
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
33
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
34
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
35
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
36
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
37
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
38
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
39
+ import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
40
+
41
+ struct FeeProjectConfig {
42
+ REVConfig configuration;
43
+ JBTerminalConfig[] terminalConfigurations;
44
+ REVSuckerDeploymentConfig suckerDeploymentConfiguration;
45
+ }
46
+
47
+ /// @notice Tests that REVLoans enforces the cash out delay set by REVDeployer for cross-chain deployments.
48
+ contract TestLoansCashOutDelay is TestBaseWorkflow {
49
+ // forge-lint: disable-next-line(mixed-case-variable)
50
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
51
+ // forge-lint: disable-next-line(mixed-case-variable)
52
+ bytes32 ERC20_SALT = "REV_TOKEN";
53
+
54
+ // forge-lint: disable-next-line(mixed-case-variable)
55
+ REVDeployer REV_DEPLOYER;
56
+ // forge-lint: disable-next-line(mixed-case-variable)
57
+ JB721TiersHook EXAMPLE_HOOK;
58
+ // forge-lint: disable-next-line(mixed-case-variable)
59
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
60
+ // forge-lint: disable-next-line(mixed-case-variable)
61
+ IJB721TiersHookStore HOOK_STORE;
62
+ // forge-lint: disable-next-line(mixed-case-variable)
63
+ IJBAddressRegistry ADDRESS_REGISTRY;
64
+ // forge-lint: disable-next-line(mixed-case-variable)
65
+ IREVLoans LOANS_CONTRACT;
66
+ // forge-lint: disable-next-line(mixed-case-variable)
67
+ IJBSuckerRegistry SUCKER_REGISTRY;
68
+ // forge-lint: disable-next-line(mixed-case-variable)
69
+ CTPublisher PUBLISHER;
70
+ // forge-lint: disable-next-line(mixed-case-variable)
71
+ MockBuybackDataHook MOCK_BUYBACK;
72
+
73
+ // forge-lint: disable-next-line(mixed-case-variable)
74
+ uint256 FEE_PROJECT_ID;
75
+
76
+ /// @notice Revnet deployed with startsAtOrAfter in the past (triggers cash out delay).
77
+ // forge-lint: disable-next-line(mixed-case-variable)
78
+ uint256 DELAYED_REVNET_ID;
79
+
80
+ /// @notice Revnet deployed with startsAtOrAfter == block.timestamp (no delay).
81
+ // forge-lint: disable-next-line(mixed-case-variable)
82
+ uint256 NORMAL_REVNET_ID;
83
+
84
+ // forge-lint: disable-next-line(mixed-case-variable)
85
+ address USER = makeAddr("user");
86
+
87
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
88
+
89
+ function getFeeProjectConfig() internal view returns (FeeProjectConfig memory) {
90
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
91
+ accountingContextsToAccept[0] = JBAccountingContext({
92
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
93
+ });
94
+
95
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
96
+ terminalConfigurations[0] =
97
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
98
+
99
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
100
+ JBSplit[] memory splits = new JBSplit[](1);
101
+ splits[0].beneficiary = payable(multisig());
102
+ splits[0].percent = 10_000;
103
+
104
+ stageConfigurations[0] = REVStageConfig({
105
+ startsAtOrAfter: uint40(block.timestamp),
106
+ autoIssuances: new REVAutoIssuance[](0),
107
+ splitPercent: 2000,
108
+ splits: splits,
109
+ // forge-lint: disable-next-line(unsafe-typecast)
110
+ initialIssuance: uint112(1000e18),
111
+ issuanceCutFrequency: 90 days,
112
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
113
+ cashOutTaxRate: 6000,
114
+ extraMetadata: 0
115
+ });
116
+
117
+ return FeeProjectConfig({
118
+ configuration: REVConfig({
119
+ // forge-lint: disable-next-line(named-struct-fields)
120
+ description: REVDescription("Revnet", "$REV", "ipfs://fee", ERC20_SALT),
121
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
122
+ splitOperator: multisig(),
123
+ stageConfigurations: stageConfigurations
124
+ }),
125
+ terminalConfigurations: terminalConfigurations,
126
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
127
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
128
+ })
129
+ });
130
+ }
131
+
132
+ /// @notice Returns a revnet config. When `pastStart` is true, `startsAtOrAfter` is set to 1 second ago,
133
+ /// triggering the 30-day cash out delay in REVDeployer._setCashOutDelayIfNeeded.
134
+ function _getRevnetConfig(
135
+ bool pastStart,
136
+ string memory name,
137
+ string memory symbol,
138
+ bytes32 salt
139
+ )
140
+ internal
141
+ view
142
+ returns (FeeProjectConfig memory)
143
+ {
144
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
145
+ accountingContextsToAccept[0] = JBAccountingContext({
146
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
147
+ });
148
+
149
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
150
+ terminalConfigurations[0] =
151
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
152
+
153
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
154
+ JBSplit[] memory splits = new JBSplit[](1);
155
+ splits[0].beneficiary = payable(multisig());
156
+ splits[0].percent = 10_000;
157
+
158
+ // If pastStart, set startsAtOrAfter to 1 second ago — simulates cross-chain deployment
159
+ // where the stage is already active on another chain.
160
+ uint40 startsAt = pastStart ? uint40(block.timestamp - 1) : uint40(block.timestamp);
161
+
162
+ stageConfigurations[0] = REVStageConfig({
163
+ startsAtOrAfter: startsAt,
164
+ autoIssuances: new REVAutoIssuance[](0),
165
+ splitPercent: 2000,
166
+ splits: splits,
167
+ // forge-lint: disable-next-line(unsafe-typecast)
168
+ initialIssuance: uint112(1000e18),
169
+ issuanceCutFrequency: 90 days,
170
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
171
+ cashOutTaxRate: 6000,
172
+ extraMetadata: 0
173
+ });
174
+
175
+ return FeeProjectConfig({
176
+ configuration: REVConfig({
177
+ // forge-lint: disable-next-line(named-struct-fields)
178
+ description: REVDescription(name, symbol, "ipfs://test", salt),
179
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
180
+ splitOperator: multisig(),
181
+ stageConfigurations: stageConfigurations
182
+ }),
183
+ terminalConfigurations: terminalConfigurations,
184
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
185
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: salt
186
+ })
187
+ });
188
+ }
189
+
190
+ function setUp() public override {
191
+ super.setUp();
192
+
193
+ // Warp to a realistic timestamp so startsAtOrAfter - 1 doesn't underflow.
194
+ vm.warp(1_700_000_000);
195
+
196
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
197
+
198
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
199
+ HOOK_STORE = new JB721TiersHookStore();
200
+ EXAMPLE_HOOK = new JB721TiersHook(
201
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
202
+ );
203
+ ADDRESS_REGISTRY = new JBAddressRegistry();
204
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
205
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
206
+ MOCK_BUYBACK = new MockBuybackDataHook();
207
+
208
+ LOANS_CONTRACT = new REVLoans({
209
+ controller: jbController(),
210
+ projects: jbProjects(),
211
+ revId: FEE_PROJECT_ID,
212
+ owner: address(this),
213
+ permit2: permit2(),
214
+ trustedForwarder: TRUSTED_FORWARDER
215
+ });
216
+
217
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
218
+ jbController(),
219
+ SUCKER_REGISTRY,
220
+ FEE_PROJECT_ID,
221
+ HOOK_DEPLOYER,
222
+ PUBLISHER,
223
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
224
+ address(LOANS_CONTRACT),
225
+ TRUSTED_FORWARDER
226
+ );
227
+
228
+ // Approve the deployer to configure the fee project.
229
+ vm.prank(multisig());
230
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
231
+
232
+ // Deploy the fee project.
233
+ FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
234
+ vm.prank(multisig());
235
+ REV_DEPLOYER.deployFor({
236
+ revnetId: FEE_PROJECT_ID,
237
+ configuration: feeProjectConfig.configuration,
238
+ terminalConfigurations: feeProjectConfig.terminalConfigurations,
239
+ suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
240
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
241
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
242
+ });
243
+
244
+ // Deploy a revnet with startsAtOrAfter in the past (triggers 30-day cash out delay).
245
+ FeeProjectConfig memory delayedConfig =
246
+ _getRevnetConfig(true, "Delayed", "$DLY", keccak256(abi.encodePacked("DELAYED")));
247
+ (DELAYED_REVNET_ID,) = REV_DEPLOYER.deployFor({
248
+ revnetId: 0,
249
+ configuration: delayedConfig.configuration,
250
+ terminalConfigurations: delayedConfig.terminalConfigurations,
251
+ suckerDeploymentConfiguration: delayedConfig.suckerDeploymentConfiguration,
252
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
253
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
254
+ });
255
+
256
+ // Deploy a normal revnet with no delay.
257
+ FeeProjectConfig memory normalConfig =
258
+ _getRevnetConfig(false, "Normal", "$NRM", keccak256(abi.encodePacked("NORMAL")));
259
+ (NORMAL_REVNET_ID,) = REV_DEPLOYER.deployFor({
260
+ revnetId: 0,
261
+ configuration: normalConfig.configuration,
262
+ terminalConfigurations: normalConfig.terminalConfigurations,
263
+ suckerDeploymentConfiguration: normalConfig.suckerDeploymentConfiguration,
264
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
265
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
266
+ });
267
+
268
+ vm.deal(USER, 100 ether);
269
+ }
270
+
271
+ // ------------------------------------------------------------------
272
+ // Helpers
273
+ // ------------------------------------------------------------------
274
+
275
+ /// @notice Pay ETH into a revnet and return the number of project tokens received.
276
+ function _payAndGetTokens(uint256 revnetId, uint256 amount) internal returns (uint256 tokenCount) {
277
+ vm.prank(USER);
278
+ tokenCount = jbMultiTerminal().pay{value: amount}(revnetId, JBConstants.NATIVE_TOKEN, amount, USER, 0, "", "");
279
+ }
280
+
281
+ /// @notice Mock the permissions check so LOANS_CONTRACT can burn tokens on behalf of USER.
282
+ function _mockBorrowPermission(uint256 projectId) internal {
283
+ mockExpect(
284
+ address(jbPermissions()),
285
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, projectId, 11, true, true)),
286
+ abi.encode(true)
287
+ );
288
+ }
289
+
290
+ // ------------------------------------------------------------------
291
+ // Tests: delayed revnet (startsAtOrAfter in the past → 30-day delay)
292
+ // ------------------------------------------------------------------
293
+
294
+ /// @notice Verify the deployer actually set a cash out delay for the delayed revnet.
295
+ function test_delayedRevnet_hasCashOutDelay() public view {
296
+ uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
297
+ assertGt(cashOutDelay, block.timestamp, "Cash out delay should be in the future");
298
+ }
299
+
300
+ /// @notice Verify the normal revnet has no cash out delay.
301
+ function test_normalRevnet_noCashOutDelay() public view {
302
+ uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(NORMAL_REVNET_ID);
303
+ assertEq(cashOutDelay, 0, "Normal revnet should have no cash out delay");
304
+ }
305
+
306
+ /// @notice borrowableAmountFrom should return 0 during the delay period.
307
+ function test_borrowableAmountFrom_returnsZeroDuringDelay() public {
308
+ // Pay into the delayed revnet to get tokens.
309
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
310
+ assertGt(tokenCount, 0, "Should have tokens");
311
+
312
+ // Query borrowable amount — should be 0 during the delay.
313
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
314
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
315
+ );
316
+ assertEq(borrowable, 0, "Borrowable amount should be 0 during cash out delay");
317
+ }
318
+
319
+ /// @notice borrowFrom should revert during the delay period.
320
+ function test_borrowFrom_revertsDuringDelay() public {
321
+ // Pay into the delayed revnet to get tokens.
322
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
323
+ assertGt(tokenCount, 0, "Should have tokens");
324
+
325
+ // No permission mock needed — the function reverts before reaching the permission check.
326
+
327
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
328
+
329
+ // Attempt to borrow — should revert with CashOutDelayNotFinished.
330
+ uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
331
+ vm.expectRevert(
332
+ abi.encodeWithSelector(REVLoans.REVLoans_CashOutDelayNotFinished.selector, cashOutDelay, block.timestamp)
333
+ );
334
+ vm.prank(USER);
335
+ LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, 1, tokenCount, payable(USER), 25);
336
+ }
337
+
338
+ /// @notice After warping past the delay, borrowableAmountFrom should return a non-zero value.
339
+ function test_borrowableAmountFrom_nonZeroAfterDelay() public {
340
+ // Pay into the delayed revnet.
341
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
342
+
343
+ // Still in delay — should be 0.
344
+ uint256 borrowableBefore = LOANS_CONTRACT.borrowableAmountFrom(
345
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
346
+ );
347
+ assertEq(borrowableBefore, 0, "Should be 0 during delay");
348
+
349
+ // Warp past the delay.
350
+ vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
351
+
352
+ // Now should be > 0.
353
+ uint256 borrowableAfter = LOANS_CONTRACT.borrowableAmountFrom(
354
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
355
+ );
356
+ assertGt(borrowableAfter, 0, "Should be > 0 after delay expires");
357
+ }
358
+
359
+ /// @notice After warping past the delay, borrowFrom should succeed.
360
+ function test_borrowFrom_succeedsAfterDelay() public {
361
+ // Pay into the delayed revnet.
362
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
363
+
364
+ // Warp past the delay.
365
+ vm.warp(block.timestamp + REV_DEPLOYER.CASH_OUT_DELAY() + 1);
366
+
367
+ // Get the borrowable amount.
368
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
369
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
370
+ );
371
+ assertGt(borrowable, 0, "Should be borrowable after delay");
372
+
373
+ // Mock permission.
374
+ _mockBorrowPermission(DELAYED_REVNET_ID);
375
+
376
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
377
+
378
+ // Borrow — should succeed.
379
+ vm.prank(USER);
380
+ (uint256 loanId,) =
381
+ LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, borrowable, tokenCount, payable(USER), 25);
382
+ assertGt(loanId, 0, "Should have created a loan");
383
+ }
384
+
385
+ // ------------------------------------------------------------------
386
+ // Tests: normal revnet (no delay)
387
+ // ------------------------------------------------------------------
388
+
389
+ /// @notice A normal revnet (no delay) should allow borrowing immediately.
390
+ function test_normalRevnet_borrowableImmediately() public {
391
+ // Pay into the normal revnet.
392
+ uint256 tokenCount = _payAndGetTokens(NORMAL_REVNET_ID, 1 ether);
393
+
394
+ // Should have a non-zero borrowable amount immediately.
395
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
396
+ NORMAL_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
397
+ );
398
+ assertGt(borrowable, 0, "Normal revnet should be borrowable immediately");
399
+ }
400
+
401
+ /// @notice A normal revnet (no delay) should allow borrowFrom immediately.
402
+ function test_normalRevnet_borrowFromImmediately() public {
403
+ // Pay into the normal revnet.
404
+ uint256 tokenCount = _payAndGetTokens(NORMAL_REVNET_ID, 1 ether);
405
+
406
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
407
+ NORMAL_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
408
+ );
409
+ assertGt(borrowable, 0, "Should be borrowable");
410
+
411
+ // Mock permission.
412
+ _mockBorrowPermission(NORMAL_REVNET_ID);
413
+
414
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
415
+
416
+ // Borrow — should succeed without any delay.
417
+ vm.prank(USER);
418
+ (uint256 loanId,) =
419
+ LOANS_CONTRACT.borrowFrom(NORMAL_REVNET_ID, source, borrowable, tokenCount, payable(USER), 25);
420
+ assertGt(loanId, 0, "Should have created a loan");
421
+ }
422
+
423
+ // ------------------------------------------------------------------
424
+ // Tests: boundary conditions
425
+ // ------------------------------------------------------------------
426
+
427
+ /// @notice borrowFrom should revert at exactly the delay timestamp (not yet expired).
428
+ function test_borrowFrom_revertsAtExactDelayTimestamp() public {
429
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
430
+
431
+ // Warp to exactly the delay timestamp (not past it).
432
+ uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
433
+ vm.warp(cashOutDelay);
434
+
435
+ // borrowableAmountFrom should still return 0 (cashOutDelay > block.timestamp is false, but == is not >).
436
+ // Actually cashOutDelay == block.timestamp means cashOutDelay > block.timestamp is false → should pass.
437
+ // Let's verify: at exact boundary, the delay is NOT enforced (delay == timestamp passes).
438
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
439
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
440
+ );
441
+ assertGt(borrowable, 0, "At exact delay timestamp, borrowing should be allowed");
442
+ }
443
+
444
+ /// @notice borrowFrom should revert 1 second before the delay expires.
445
+ function test_borrowFrom_revertsOneSecondBeforeDelay() public {
446
+ uint256 tokenCount = _payAndGetTokens(DELAYED_REVNET_ID, 1 ether);
447
+
448
+ // Warp to 1 second before the delay expires.
449
+ uint256 cashOutDelay = REV_DEPLOYER.cashOutDelayOf(DELAYED_REVNET_ID);
450
+ vm.warp(cashOutDelay - 1);
451
+
452
+ // borrowableAmountFrom should return 0.
453
+ uint256 borrowable = LOANS_CONTRACT.borrowableAmountFrom(
454
+ DELAYED_REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
455
+ );
456
+ assertEq(borrowable, 0, "Should be 0 one second before delay expires");
457
+
458
+ // borrowFrom should revert before reaching the permission check.
459
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
460
+
461
+ vm.expectRevert(
462
+ abi.encodeWithSelector(REVLoans.REVLoans_CashOutDelayNotFinished.selector, cashOutDelay, block.timestamp)
463
+ );
464
+ vm.prank(USER);
465
+ LOANS_CONTRACT.borrowFrom(DELAYED_REVNET_ID, source, 1, tokenCount, payable(USER), 25);
466
+ }
467
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -218,7 +218,6 @@ contract TestSplitWeightE2E is TestBaseWorkflow {
218
218
  tiersConfig: JB721InitTiersConfig({
219
219
  tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
220
220
  }),
221
- reserveBeneficiary: address(0),
222
221
  flags: REV721TiersHookFlags({
223
222
  noNewTiersWithReserves: false,
224
223
  noNewTiersWithVotes: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -429,7 +429,6 @@ contract TestSplitWeightFork is TestBaseWorkflow {
429
429
  tiersConfig: JB721InitTiersConfig({
430
430
  tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
431
431
  }),
432
- reserveBeneficiary: address(0),
433
432
  flags: REV721TiersHookFlags({
434
433
  noNewTiersWithReserves: false,
435
434
  noNewTiersWithVotes: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -435,7 +435,6 @@ abstract contract ForkTestBase is TestBaseWorkflow {
435
435
  tiersConfig: JB721InitTiersConfig({
436
436
  tiers: tiers, currency: uint32(uint160(JBConstants.NATIVE_TOKEN)), decimals: 18
437
437
  }),
438
- reserveBeneficiary: address(0),
439
438
  flags: REV721TiersHookFlags({
440
439
  noNewTiersWithReserves: false,
441
440
  noNewTiersWithVotes: false,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity ^0.8.26;
2
+ pragma solidity 0.8.28;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "./ForkTestBase.sol";