@rev-net/core-v6 0.0.15 → 0.0.16

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 (77) hide show
  1. package/ADMINISTRATION.md +5 -1
  2. package/ARCHITECTURE.md +69 -11
  3. package/AUDIT_INSTRUCTIONS.md +90 -7
  4. package/CHANGE_LOG.md +16 -3
  5. package/README.md +32 -7
  6. package/RISKS.md +26 -14
  7. package/SKILLS.md +167 -45
  8. package/STYLE_GUIDE.md +1 -1
  9. package/USER_JOURNEYS.md +20 -6
  10. package/package.json +9 -9
  11. package/script/Deploy.s.sol +80 -16
  12. package/script/helpers/RevnetCoreDeploymentLib.sol +1 -1
  13. package/src/REVDeployer.sol +39 -9
  14. package/src/REVLoans.sol +26 -1
  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/TestLongTailEconomics.t.sol +1 -1
  42. package/test/TestLowFindings.t.sol +1 -1
  43. package/test/TestMixedFixes.t.sol +1 -1
  44. package/test/TestPermit2Signatures.t.sol +1 -1
  45. package/test/TestReallocationSandwich.t.sol +1 -1
  46. package/test/TestRevnetRegressions.t.sol +1 -1
  47. package/test/TestSplitWeightAdjustment.t.sol +1 -1
  48. package/test/TestSplitWeightE2E.t.sol +1 -1
  49. package/test/TestSplitWeightFork.t.sol +9 -10
  50. package/test/TestStageTransitionBorrowable.t.sol +1 -1
  51. package/test/TestSwapTerminalPermission.t.sol +1 -1
  52. package/test/TestUint112Overflow.t.sol +1 -1
  53. package/test/TestZeroRepayment.t.sol +1 -1
  54. package/test/audit/LoanIdOverflowGuard.t.sol +497 -0
  55. package/test/fork/ForkTestBase.sol +8 -11
  56. package/test/fork/TestAutoIssuanceFork.t.sol +1 -1
  57. package/test/fork/TestCashOutFork.t.sol +1 -1
  58. package/test/fork/TestIssuanceDecayFork.t.sol +1 -1
  59. package/test/fork/TestLoanBorrowFork.t.sol +1 -1
  60. package/test/fork/TestLoanCrossRulesetFork.t.sol +1 -1
  61. package/test/fork/TestLoanERC20Fork.t.sol +1 -1
  62. package/test/fork/TestLoanLiquidationFork.t.sol +1 -1
  63. package/test/fork/TestLoanReallocateFork.t.sol +1 -1
  64. package/test/fork/TestLoanRepayFork.t.sol +1 -1
  65. package/test/fork/TestLoanTransferFork.t.sol +1 -1
  66. package/test/fork/TestPermit2PaymentFork.t.sol +1 -1
  67. package/test/fork/TestSplitWeightFork.t.sol +1 -1
  68. package/test/helpers/MaliciousContracts.sol +1 -1
  69. package/test/mock/MockBuybackCashOutRecorder.sol +82 -0
  70. package/test/mock/MockBuybackDataHook.sol +1 -1
  71. package/test/mock/MockBuybackDataHookMintPath.sol +1 -1
  72. package/test/regression/TestBurnPermissionRequired.t.sol +1 -1
  73. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +205 -0
  74. package/test/regression/TestCrossRevnetLiquidation.t.sol +1 -1
  75. package/test/regression/TestCumulativeLoanCounter.t.sol +1 -1
  76. package/test/regression/TestLiquidateGapHandling.t.sol +1 -1
  77. package/test/regression/TestZeroPriceFeed.t.sol +1 -1
@@ -0,0 +1,497 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.26;
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
+ // forge-lint: disable-next-line(unaliased-plain-import)
14
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
15
+ // forge-lint: disable-next-line(unaliased-plain-import)
16
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
17
+ // forge-lint: disable-next-line(unaliased-plain-import)
18
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
19
+ // forge-lint: disable-next-line(unaliased-plain-import)
20
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
21
+ // forge-lint: disable-next-line(unaliased-plain-import)
22
+ import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
23
+ // Core constants and structs used throughout the test.
24
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
25
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
+ import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
27
+ // Price feed mock for native-token-to-native-token identity pricing.
28
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
29
+ // REVLoans contract and its supporting types.
30
+ import {REVLoans} from "../../src/REVLoans.sol";
31
+ import {REVLoan} from "../../src/structs/REVLoan.sol";
32
+ import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
33
+ import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
34
+ import {REVDescription} from "../../src/structs/REVDescription.sol";
35
+ // Deployment dependencies for suckers, 721 hooks, and address registry.
36
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
37
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
38
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
39
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
40
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
41
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
42
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
43
+ // Helper that provides empty 721 tier configs for revnet deployment.
44
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
45
+
46
+ /// @notice Regression tests for the loan ID overflow guard in REVLoans.
47
+ /// @dev The totalLoansBorrowedFor counter must never exceed _ONE_TRILLION (1e12).
48
+ /// When it reaches that limit, borrowFrom, _reallocateCollateralFromLoan, and
49
+ /// the partial-repay branch of repayLoan must all revert with REVLoans_LoanIdOverflow().
50
+ /// These tests use vm.store to set the counter to the limit, then verify the revert.
51
+ contract LoanIdOverflowGuard is TestBaseWorkflow {
52
+ // ---------------------------------------------------------------
53
+ // Constants
54
+ // ---------------------------------------------------------------
55
+
56
+ /// @dev Salt for deterministic REVDeployer deployment.
57
+ // forge-lint: disable-next-line(mixed-case-variable)
58
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
59
+
60
+ /// @dev The overflow boundary -- must match _ONE_TRILLION in REVLoans.sol.
61
+ uint256 private constant _ONE_TRILLION = 1_000_000_000_000;
62
+
63
+ /// @dev Storage slot of the totalLoansBorrowedFor mapping in REVLoans (slot 8).
64
+ /// Determined via `forge inspect REVLoans storage-layout`.
65
+ uint256 private constant TOTAL_LOANS_BORROWED_FOR_SLOT = 8;
66
+
67
+ /// @dev The address that is allowed to forward meta-transactions.
68
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
69
+
70
+ // ---------------------------------------------------------------
71
+ // State variables
72
+ // ---------------------------------------------------------------
73
+
74
+ // forge-lint: disable-next-line(mixed-case-variable)
75
+ REVDeployer REV_DEPLOYER;
76
+ // forge-lint: disable-next-line(mixed-case-variable)
77
+ JB721TiersHook EXAMPLE_HOOK;
78
+ // forge-lint: disable-next-line(mixed-case-variable)
79
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
80
+ // forge-lint: disable-next-line(mixed-case-variable)
81
+ IJB721TiersHookStore HOOK_STORE;
82
+ // forge-lint: disable-next-line(mixed-case-variable)
83
+ IJBAddressRegistry ADDRESS_REGISTRY;
84
+ // forge-lint: disable-next-line(mixed-case-variable)
85
+ REVLoans LOANS_CONTRACT;
86
+ // forge-lint: disable-next-line(mixed-case-variable)
87
+ IJBSuckerRegistry SUCKER_REGISTRY;
88
+ // forge-lint: disable-next-line(mixed-case-variable)
89
+ CTPublisher PUBLISHER;
90
+ // forge-lint: disable-next-line(mixed-case-variable)
91
+ MockBuybackDataHook MOCK_BUYBACK;
92
+
93
+ /// @dev The fee project ID (project 1).
94
+ // forge-lint: disable-next-line(mixed-case-variable)
95
+ uint256 FEE_PROJECT_ID;
96
+
97
+ /// @dev The revnet project ID used by all tests.
98
+ // forge-lint: disable-next-line(mixed-case-variable)
99
+ uint256 REVNET_ID;
100
+
101
+ /// @dev Test user address with ETH for paying into the revnet.
102
+ // forge-lint: disable-next-line(mixed-case-variable)
103
+ address USER = makeAddr("user");
104
+
105
+ /// @dev Second test user used to increase revnet surplus between loan creation and reallocation.
106
+ // forge-lint: disable-next-line(mixed-case-variable)
107
+ address USER2 = makeAddr("user2");
108
+
109
+ // ---------------------------------------------------------------
110
+ // Setup
111
+ // ---------------------------------------------------------------
112
+
113
+ function setUp() public override {
114
+ // Initialize the base test workflow (deploys core contracts).
115
+ super.setUp();
116
+
117
+ // Create the fee project owned by multisig.
118
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
119
+
120
+ // Deploy the sucker registry (no deployers, no initial suckers).
121
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
122
+
123
+ // Deploy the 721 hook store.
124
+ HOOK_STORE = new JB721TiersHookStore();
125
+
126
+ // Deploy the example 721 hook (needed as the implementation for the deployer).
127
+ EXAMPLE_HOOK = new JB721TiersHook(
128
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
129
+ );
130
+
131
+ // Deploy the address registry (used by the hook deployer).
132
+ ADDRESS_REGISTRY = new JBAddressRegistry();
133
+
134
+ // Deploy the 721 hook deployer.
135
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
136
+
137
+ // Deploy the croptop publisher.
138
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
139
+
140
+ // Deploy the mock buyback data hook (satisfies the IJBBuybackHookRegistry interface).
141
+ MOCK_BUYBACK = new MockBuybackDataHook();
142
+
143
+ // Add a 1:1 native token price feed so bonding curve math works.
144
+ MockPriceFeed priceFeed = new MockPriceFeed(1e18, 18);
145
+ vm.prank(multisig());
146
+ jbPrices()
147
+ .addPriceFeedFor(
148
+ 0, uint32(uint160(JBConstants.NATIVE_TOKEN)), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed
149
+ );
150
+
151
+ // Deploy the REVLoans contract.
152
+ LOANS_CONTRACT = new REVLoans({
153
+ controller: jbController(),
154
+ projects: jbProjects(),
155
+ revId: FEE_PROJECT_ID,
156
+ owner: address(this),
157
+ permit2: permit2(),
158
+ trustedForwarder: TRUSTED_FORWARDER
159
+ });
160
+
161
+ // Deploy the REVDeployer with a deterministic salt.
162
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
163
+ jbController(),
164
+ SUCKER_REGISTRY,
165
+ FEE_PROJECT_ID,
166
+ HOOK_DEPLOYER,
167
+ PUBLISHER,
168
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
169
+ address(LOANS_CONTRACT),
170
+ TRUSTED_FORWARDER
171
+ );
172
+
173
+ // Approve the deployer to configure the fee project.
174
+ vm.prank(multisig());
175
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
176
+
177
+ // Deploy the fee project's revnet configuration.
178
+ _deployFeeProject();
179
+
180
+ // Deploy the test revnet that loans will be issued against.
181
+ _deployRevnet();
182
+
183
+ // Give the test users 100 ETH each.
184
+ vm.deal(USER, 100e18);
185
+ vm.deal(USER2, 100e18);
186
+ }
187
+
188
+ // ---------------------------------------------------------------
189
+ // Internal helpers
190
+ // ---------------------------------------------------------------
191
+
192
+ /// @dev Deploys the fee project (project 1) with a single stage.
193
+ function _deployFeeProject() internal {
194
+ // Accept native token through the multi terminal.
195
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
196
+ acc[0] = JBAccountingContext({
197
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
198
+ });
199
+
200
+ // Configure a single terminal.
201
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
202
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
203
+
204
+ // A single stage with auto-issuance for the multisig.
205
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
206
+ JBSplit[] memory splits = new JBSplit[](1);
207
+ splits[0].beneficiary = payable(multisig());
208
+ splits[0].percent = 10_000;
209
+
210
+ // Auto-issue 70k tokens to multisig on this chain.
211
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
212
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
213
+
214
+ // Build the stage configuration.
215
+ stages[0] = REVStageConfig({
216
+ startsAtOrAfter: uint40(block.timestamp),
217
+ autoIssuances: ai,
218
+ splitPercent: 2000,
219
+ splits: splits,
220
+ initialIssuance: uint112(1000e18),
221
+ issuanceCutFrequency: 90 days,
222
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
223
+ cashOutTaxRate: 6000,
224
+ extraMetadata: 0
225
+ });
226
+
227
+ // Build the revnet configuration for the fee project.
228
+ REVConfig memory cfg = REVConfig({
229
+ // forge-lint: disable-next-line(named-struct-fields)
230
+ description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
231
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
232
+ splitOperator: multisig(),
233
+ stageConfigurations: stages
234
+ });
235
+
236
+ // Deploy the fee project revnet.
237
+ vm.prank(multisig());
238
+ REV_DEPLOYER.deployFor({
239
+ revnetId: FEE_PROJECT_ID,
240
+ configuration: cfg,
241
+ terminalConfigurations: tc,
242
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
243
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
244
+ }),
245
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
246
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
247
+ });
248
+ }
249
+
250
+ /// @dev Deploys the test revnet (project 2) with a single stage and 60% cash-out tax.
251
+ function _deployRevnet() internal {
252
+ // Accept native token through the multi terminal.
253
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
254
+ acc[0] = JBAccountingContext({
255
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
256
+ });
257
+
258
+ // Configure a single terminal.
259
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
260
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
261
+
262
+ // A single stage with auto-issuance for the multisig.
263
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
264
+ JBSplit[] memory splits = new JBSplit[](1);
265
+ splits[0].beneficiary = payable(multisig());
266
+ splits[0].percent = 10_000;
267
+
268
+ // Auto-issue 70k tokens to multisig on this chain.
269
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
270
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
271
+
272
+ // Build the stage configuration.
273
+ stages[0] = REVStageConfig({
274
+ startsAtOrAfter: uint40(block.timestamp),
275
+ autoIssuances: ai,
276
+ splitPercent: 2000,
277
+ splits: splits,
278
+ initialIssuance: uint112(1000e18),
279
+ issuanceCutFrequency: 90 days,
280
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
281
+ cashOutTaxRate: 6000,
282
+ extraMetadata: 0
283
+ });
284
+
285
+ // Build the revnet configuration for the test project.
286
+ REVConfig memory cfg = REVConfig({
287
+ // forge-lint: disable-next-line(named-struct-fields)
288
+ description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
289
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
290
+ splitOperator: multisig(),
291
+ stageConfigurations: stages
292
+ });
293
+
294
+ // Deploy the test revnet (revnetId 0 means "create new").
295
+ (REVNET_ID,) = REV_DEPLOYER.deployFor({
296
+ revnetId: 0,
297
+ configuration: cfg,
298
+ terminalConfigurations: tc,
299
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
300
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
301
+ }),
302
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
303
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
304
+ });
305
+ }
306
+
307
+ /// @dev Creates a loan for the given user by paying ETH into the revnet then borrowing.
308
+ /// @param user The address that will own the loan.
309
+ /// @param ethAmount The amount of ETH to pay into the revnet as collateral.
310
+ /// @return loanId The ID of the created loan.
311
+ /// @return tokenCount The number of revnet tokens received from paying.
312
+ function _setupLoan(address user, uint256 ethAmount) internal returns (uint256 loanId, uint256 tokenCount) {
313
+ // Pay ETH into the revnet and receive tokens.
314
+ vm.prank(user);
315
+ tokenCount =
316
+ jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
317
+
318
+ // Compute the borrowable amount from the tokens received.
319
+ uint256 borrowAmount =
320
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
321
+
322
+ // Sanity check: the user should be able to borrow something.
323
+ require(borrowAmount > 0, "Borrow amount should be > 0");
324
+
325
+ // Mock the permissions check so LOANS_CONTRACT can burn the user's tokens.
326
+ mockExpect(
327
+ address(jbPermissions()),
328
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
329
+ abi.encode(true)
330
+ );
331
+
332
+ // Build the loan source pointing at the real multi terminal and native token.
333
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
334
+
335
+ // Borrow with minimum fee percent (25 = 2.5%).
336
+ vm.prank(user);
337
+ (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
338
+ }
339
+
340
+ /// @dev Computes the storage slot for totalLoansBorrowedFor[revnetId].
341
+ /// @param revnetId The revnet ID to compute the mapping slot for.
342
+ /// @return The keccak256 slot for the mapping entry.
343
+ function _totalLoansBorrowedSlot(uint256 revnetId) internal pure returns (bytes32) {
344
+ // Solidity mapping slot: keccak256(abi.encode(key, baseSlot)).
345
+ return keccak256(abi.encode(revnetId, TOTAL_LOANS_BORROWED_FOR_SLOT));
346
+ }
347
+
348
+ // ---------------------------------------------------------------
349
+ // Test 1: borrowFrom overflow guard
350
+ // ---------------------------------------------------------------
351
+
352
+ /// @notice Verifies that borrowFrom reverts with REVLoans_LoanIdOverflow when
353
+ /// the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
354
+ function test_borrowFrom_revertsAtOverflowBoundary() public {
355
+ // Pay ETH into the revnet so the user has tokens for collateral.
356
+ vm.prank(USER);
357
+ uint256 tokens = jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, USER, 0, "", "");
358
+
359
+ // Verify the user received tokens.
360
+ assertGt(tokens, 0, "user should receive tokens from paying");
361
+
362
+ // Compute the borrowable amount from the user's tokens.
363
+ uint256 borrowAmount =
364
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
365
+
366
+ // Sanity check: there should be a borrowable amount.
367
+ assertGt(borrowAmount, 0, "borrowable amount should be > 0");
368
+
369
+ // No permission mock needed: the overflow guard fires before any permission/burn check.
370
+
371
+ // Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
372
+ vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
373
+
374
+ // Confirm the counter is now at the overflow boundary.
375
+ assertEq(
376
+ LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
377
+ _ONE_TRILLION,
378
+ "counter should be at _ONE_TRILLION after vm.store"
379
+ );
380
+
381
+ // Build the loan source pointing at the real terminal and native token.
382
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
383
+
384
+ // Expect the overflow revert.
385
+ vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
386
+
387
+ // Attempt to borrow -- should revert because the counter is at the limit.
388
+ vm.prank(USER);
389
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25);
390
+ }
391
+
392
+ // ---------------------------------------------------------------
393
+ // Test 2: reallocateCollateralFromLoan overflow guard
394
+ // ---------------------------------------------------------------
395
+
396
+ /// @notice Verifies that reallocateCollateralFromLoan reverts with REVLoans_LoanIdOverflow
397
+ /// when the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
398
+ /// @dev A second user injects surplus after the loan is created so that removing a small
399
+ /// amount of collateral still leaves borrowable value >= the original loan amount (otherwise
400
+ /// the ReallocatingMoreCollateralThanBorrowedAmountAllows check fires first).
401
+ function test_reallocateCollateral_revertsAtOverflowBoundary() public {
402
+ // Create a loan with enough collateral that we can split off some for reallocation.
403
+ (uint256 loanId,) = _setupLoan(USER, 10e18);
404
+
405
+ // Verify the loan was created successfully.
406
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
407
+ assertGt(loan.collateral, 0, "loan should have collateral");
408
+ assertGt(loan.amount, 0, "loan should have a borrow amount");
409
+
410
+ // Add surplus to the revnet WITHOUT minting tokens (addToBalanceOf, not pay).
411
+ // This increases the per-token borrowable value so that after removing a small
412
+ // amount of collateral, the borrowable amount still exceeds the original loan
413
+ // amount (avoiding the ReallocatingMoreCollateralThanBorrowedAmountAllows check
414
+ // at line 1181 and reaching the overflow guard at line 1186).
415
+ vm.prank(USER2);
416
+ jbMultiTerminal().addToBalanceOf{value: 50e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 50e18, false, "", "");
417
+
418
+ // No permission mock needed: the overflow guard in _reallocateCollateralFromLoan fires
419
+ // before any permission/burn check is reached.
420
+
421
+ // Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
422
+ vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
423
+
424
+ // Confirm the counter is at the overflow boundary.
425
+ assertEq(
426
+ LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
427
+ _ONE_TRILLION,
428
+ "counter should be at _ONE_TRILLION after vm.store"
429
+ );
430
+
431
+ // Build the loan source matching the existing loan's source.
432
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
433
+
434
+ // Transfer only 1 token of collateral to trigger the reallocation path.
435
+ uint256 collateralToTransfer = 1;
436
+
437
+ // Expect the overflow revert from _reallocateCollateralFromLoan.
438
+ vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
439
+
440
+ // Attempt to reallocate -- should revert because the counter is at the limit.
441
+ vm.prank(USER);
442
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
443
+ loanId,
444
+ collateralToTransfer,
445
+ source,
446
+ 0, // minBorrowAmount for the new loan
447
+ 0, // no additional collateral to add
448
+ payable(USER),
449
+ 25 // prepaidFeePercent (2.5%)
450
+ );
451
+ }
452
+
453
+ // ---------------------------------------------------------------
454
+ // Test 3: repayLoan (partial) overflow guard
455
+ // ---------------------------------------------------------------
456
+
457
+ /// @notice Verifies that a partial repayLoan reverts with REVLoans_LoanIdOverflow
458
+ /// when the totalLoansBorrowedFor counter has reached _ONE_TRILLION.
459
+ /// @dev A partial repayment creates a replacement loan with a new ID, which requires
460
+ /// incrementing the counter. If the counter is at the limit, this must revert.
461
+ function test_partialRepay_revertsAtOverflowBoundary() public {
462
+ // Create a loan for partial repayment testing.
463
+ (uint256 loanId,) = _setupLoan(USER, 5e18);
464
+
465
+ // Verify the loan exists and has a borrow amount.
466
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
467
+ assertGt(loan.amount, 0, "loan should have a borrow amount");
468
+ assertGt(loan.collateral, 0, "loan should have collateral");
469
+
470
+ // Calculate a partial repayment (half the borrow amount, return no collateral).
471
+ uint256 halfAmount = loan.amount / 2;
472
+
473
+ // Sanity check: half amount must be non-zero for a meaningful partial repay.
474
+ assertGt(halfAmount, 0, "half amount should be > 0");
475
+
476
+ // Use vm.store to set totalLoansBorrowedFor[REVNET_ID] to _ONE_TRILLION.
477
+ vm.store(address(LOANS_CONTRACT), _totalLoansBorrowedSlot(REVNET_ID), bytes32(_ONE_TRILLION));
478
+
479
+ // Confirm the counter is at the overflow boundary.
480
+ assertEq(
481
+ LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID),
482
+ _ONE_TRILLION,
483
+ "counter should be at _ONE_TRILLION after vm.store"
484
+ );
485
+
486
+ // Build an empty allowance (no permit2 needed for native token repayment).
487
+ JBSingleAllowance memory allowance;
488
+
489
+ // Expect the overflow revert from the partial-repay branch.
490
+ vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
491
+
492
+ // Attempt a partial repayment -- should revert because creating the replacement loan
493
+ // would exceed the _ONE_TRILLION loan ID namespace.
494
+ vm.prank(USER);
495
+ LOANS_CONTRACT.repayLoan{value: halfAmount}(loanId, halfAmount, 0, payable(USER), allowance);
496
+ }
497
+ }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
3
3
 
4
4
  // forge-lint: disable-next-line(unaliased-plain-import)
5
5
  import "forge-std/Test.sol";
@@ -524,15 +524,11 @@ abstract contract ForkTestBase is TestBaseWorkflow {
524
524
  hooks: IHooks(address(0))
525
525
  });
526
526
 
527
- // Pool is already initialized at 1:1 price by REVDeployer during deployment.
528
- // Just add liquidity and mock the oracle.
529
-
530
- // At 1:1 price, full-range liquidity needs equal amounts of both tokens.
531
- uint256 projectTokenAmount = liquidityTokenAmount;
532
-
533
- // Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
527
+ // Pool is already initialized at fair issuance price by REVDeployer during deployment.
528
+ // At high tick (~69078 for 1000 tokens/ETH), full-range liquidity needs ~32x more project tokens than ETH.
529
+ // Mint 50x project tokens and use a smaller liquidity delta to stay within budget.
534
530
  vm.prank(address(jbController()));
535
- jbTokens().mintFor(address(liqHelper), revnetId, projectTokenAmount);
531
+ jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount * 50);
536
532
  // Fund with ETH for the native currency side.
537
533
  vm.deal(address(liqHelper), liquidityTokenAmount);
538
534
 
@@ -541,11 +537,12 @@ abstract contract ForkTestBase is TestBaseWorkflow {
541
537
  vm.stopPrank();
542
538
 
543
539
  // forge-lint: disable-next-line(unsafe-typecast)
544
- int256 liquidityDelta = int256(liquidityTokenAmount / 2);
540
+ int256 liquidityDelta = int256(liquidityTokenAmount / 50);
545
541
  vm.prank(address(liqHelper));
546
542
  liqHelper.addLiquidity{value: liquidityTokenAmount}(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
547
543
 
548
- _mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
544
+ // Mock geomean oracle at tick 69078 (~1000 tokens/ETH, matching INITIAL_ISSUANCE).
545
+ _mockOracle(liquidityDelta, 69_078, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
549
546
  }
550
547
 
551
548
  /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.26;
2
+ pragma solidity ^0.8.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
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.26;
3
3
 
4
4
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";