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