@rev-net/core-v6 0.0.12 → 0.0.13

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 (78) hide show
  1. package/AUDIT_INSTRUCTIONS.md +295 -0
  2. package/CHANGE_LOG.md +316 -0
  3. package/README.md +2 -2
  4. package/RISKS.md +180 -35
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +489 -0
  7. package/package.json +9 -9
  8. package/script/Deploy.s.sol +40 -6
  9. package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
  10. package/src/REVDeployer.sol +63 -47
  11. package/src/REVLoans.sol +51 -15
  12. package/src/interfaces/IREVDeployer.sol +0 -1
  13. package/src/structs/REV721TiersHookFlags.sol +1 -0
  14. package/src/structs/REVAutoIssuance.sol +1 -0
  15. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  16. package/src/structs/REVConfig.sol +1 -0
  17. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  18. package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
  19. package/src/structs/REVDescription.sol +1 -0
  20. package/src/structs/REVLoan.sol +1 -0
  21. package/src/structs/REVLoanSource.sol +1 -0
  22. package/src/structs/REVStageConfig.sol +1 -0
  23. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  24. package/test/REV.integrations.t.sol +132 -12
  25. package/test/REVAutoIssuanceFuzz.t.sol +23 -3
  26. package/test/REVDeployerRegressions.t.sol +35 -4
  27. package/test/REVInvincibility.t.sol +58 -8
  28. package/test/REVInvincibilityHandler.sol +29 -0
  29. package/test/REVLifecycle.t.sol +28 -3
  30. package/test/REVLoans.invariants.t.sol +52 -5
  31. package/test/REVLoansAttacks.t.sol +43 -5
  32. package/test/REVLoansFeeRecovery.t.sol +50 -11
  33. package/test/REVLoansFindings.t.sol +27 -3
  34. package/test/REVLoansRegressions.t.sol +25 -3
  35. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  36. package/test/REVLoansSourced.t.sol +56 -7
  37. package/test/REVLoansUnSourced.t.sol +49 -5
  38. package/test/TestBurnHeldTokens.t.sol +32 -5
  39. package/test/TestCEIPattern.t.sol +26 -2
  40. package/test/TestCashOutCallerValidation.t.sol +30 -4
  41. package/test/TestConversionDocumentation.t.sol +26 -5
  42. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  43. package/test/TestCrossSourceReallocation.t.sol +26 -2
  44. package/test/TestERC2771MetaTx.t.sol +557 -0
  45. package/test/TestEmptyBuybackSpecs.t.sol +23 -3
  46. package/test/TestFlashLoanSurplus.t.sol +28 -3
  47. package/test/TestHookArrayOOB.t.sol +24 -4
  48. package/test/TestLiquidationBehavior.t.sol +26 -3
  49. package/test/TestLoanSourceRotation.t.sol +525 -0
  50. package/test/TestLongTailEconomics.t.sol +651 -0
  51. package/test/TestLowFindings.t.sol +65 -2
  52. package/test/TestMixedFixes.t.sol +28 -3
  53. package/test/TestPermit2Signatures.t.sol +657 -0
  54. package/test/TestReallocationSandwich.t.sol +384 -0
  55. package/test/TestRevnetRegressions.t.sol +324 -0
  56. package/test/TestSplitWeightAdjustment.t.sol +24 -2
  57. package/test/TestSplitWeightE2E.t.sol +29 -2
  58. package/test/TestSplitWeightFork.t.sol +46 -7
  59. package/test/TestStageTransitionBorrowable.t.sol +24 -2
  60. package/test/TestSwapTerminalPermission.t.sol +23 -3
  61. package/test/TestUint112Overflow.t.sol +28 -2
  62. package/test/TestZeroRepayment.t.sol +26 -2
  63. package/test/fork/ForkTestBase.sol +46 -3
  64. package/test/fork/TestCashOutFork.t.sol +1 -1
  65. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  66. package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
  67. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  68. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  69. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  70. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  71. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  72. package/test/helpers/REVEmpty721Config.sol +1 -0
  73. package/test/mock/MockBuybackDataHook.sol +1 -0
  74. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
  77. package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
  78. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
@@ -0,0 +1,267 @@
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
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
24
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
25
+ import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
26
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
27
+ import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
28
+ import {REVLoans} from "../../src/REVLoans.sol";
29
+ import {REVLoan} from "../../src/structs/REVLoan.sol";
30
+ import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
31
+ import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
32
+ import {REVDescription} from "../../src/structs/REVDescription.sol";
33
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
34
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
35
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
36
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
37
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
38
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
39
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
40
+ import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
41
+ import {JBPermissionIds} from "@bananapus/permission-ids-v6/src/JBPermissionIds.sol";
42
+ import {JBPermissioned} from "@bananapus/core-v6/src/abstract/JBPermissioned.sol";
43
+
44
+ /// @notice Validates that borrowFrom() reverts with a clear error when the caller hasn't granted BURN_TOKENS
45
+ /// permission to the REVLoans contract.
46
+ /// @dev Without this upfront check, the transaction would revert deep in JBController.burnTokensOf with a
47
+ /// less informative JBPermissioned_Unauthorized error.
48
+ contract TestBurnPermissionRequired is TestBaseWorkflow {
49
+ // forge-lint: disable-next-line(mixed-case-variable)
50
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
51
+
52
+ // forge-lint: disable-next-line(mixed-case-variable)
53
+ REVDeployer REV_DEPLOYER;
54
+ // forge-lint: disable-next-line(mixed-case-variable)
55
+ JB721TiersHook EXAMPLE_HOOK;
56
+ // forge-lint: disable-next-line(mixed-case-variable)
57
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
58
+ // forge-lint: disable-next-line(mixed-case-variable)
59
+ IJB721TiersHookStore HOOK_STORE;
60
+ // forge-lint: disable-next-line(mixed-case-variable)
61
+ IJBAddressRegistry ADDRESS_REGISTRY;
62
+ // forge-lint: disable-next-line(mixed-case-variable)
63
+ REVLoans LOANS_CONTRACT;
64
+ // forge-lint: disable-next-line(mixed-case-variable)
65
+ MockERC20 TOKEN;
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
+ // forge-lint: disable-next-line(mixed-case-variable)
76
+ uint256 REVNET_ID;
77
+
78
+ address user = makeAddr("user");
79
+
80
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
81
+
82
+ function setUp() public override {
83
+ super.setUp();
84
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
85
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
86
+ HOOK_STORE = new JB721TiersHookStore();
87
+ EXAMPLE_HOOK = new JB721TiersHook(
88
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
89
+ );
90
+ ADDRESS_REGISTRY = new JBAddressRegistry();
91
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
92
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
93
+ MOCK_BUYBACK = new MockBuybackDataHook();
94
+ TOKEN = new MockERC20("1/2 ETH", "1/2");
95
+ MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
96
+ vm.prank(multisig());
97
+ jbPrices()
98
+ .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
99
+ LOANS_CONTRACT = new REVLoans({
100
+ controller: jbController(),
101
+ projects: jbProjects(),
102
+ revId: FEE_PROJECT_ID,
103
+ owner: address(this),
104
+ permit2: permit2(),
105
+ trustedForwarder: TRUSTED_FORWARDER
106
+ });
107
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
108
+ jbController(),
109
+ SUCKER_REGISTRY,
110
+ FEE_PROJECT_ID,
111
+ HOOK_DEPLOYER,
112
+ PUBLISHER,
113
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
114
+ address(LOANS_CONTRACT),
115
+ TRUSTED_FORWARDER
116
+ );
117
+ vm.prank(multisig());
118
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
119
+ _deployFeeProject();
120
+ _deployRevnet();
121
+ vm.deal(user, 100e18);
122
+ }
123
+
124
+ function _deployFeeProject() internal {
125
+ JBAccountingContext[] memory acc = new JBAccountingContext[](2);
126
+ acc[0] = JBAccountingContext({
127
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
128
+ });
129
+ acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
130
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
131
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
132
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
133
+ JBSplit[] memory splits = new JBSplit[](1);
134
+ splits[0].beneficiary = payable(multisig());
135
+ splits[0].percent = 10_000;
136
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
137
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
138
+ stages[0] = REVStageConfig({
139
+ startsAtOrAfter: uint40(block.timestamp),
140
+ autoIssuances: ai,
141
+ splitPercent: 2000,
142
+ splits: splits,
143
+ initialIssuance: uint112(1000e18),
144
+ issuanceCutFrequency: 90 days,
145
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
146
+ cashOutTaxRate: 6000,
147
+ extraMetadata: 0
148
+ });
149
+ REVConfig memory cfg = REVConfig({
150
+ // forge-lint: disable-next-line(named-struct-fields)
151
+ description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
152
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
153
+ splitOperator: multisig(),
154
+ stageConfigurations: stages
155
+ });
156
+ vm.prank(multisig());
157
+ REV_DEPLOYER.deployFor({
158
+ revnetId: FEE_PROJECT_ID,
159
+ configuration: cfg,
160
+ terminalConfigurations: tc,
161
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
162
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
163
+ }),
164
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
165
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
166
+ });
167
+ }
168
+
169
+ function _deployRevnet() internal {
170
+ JBAccountingContext[] memory acc = new JBAccountingContext[](2);
171
+ acc[0] = JBAccountingContext({
172
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
173
+ });
174
+ acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
175
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
176
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
177
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
178
+ JBSplit[] memory splits = new JBSplit[](1);
179
+ splits[0].beneficiary = payable(multisig());
180
+ splits[0].percent = 10_000;
181
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
182
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
183
+ stages[0] = REVStageConfig({
184
+ startsAtOrAfter: uint40(block.timestamp),
185
+ autoIssuances: ai,
186
+ splitPercent: 2000,
187
+ splits: splits,
188
+ initialIssuance: uint112(1000e18),
189
+ issuanceCutFrequency: 90 days,
190
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
191
+ cashOutTaxRate: 6000,
192
+ extraMetadata: 0
193
+ });
194
+ REVConfig memory cfg = REVConfig({
195
+ // forge-lint: disable-next-line(named-struct-fields)
196
+ description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
197
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
198
+ splitOperator: multisig(),
199
+ stageConfigurations: stages
200
+ });
201
+ (REVNET_ID,) = REV_DEPLOYER.deployFor({
202
+ revnetId: 0,
203
+ configuration: cfg,
204
+ terminalConfigurations: tc,
205
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
206
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
207
+ }),
208
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
209
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
210
+ });
211
+ }
212
+
213
+ /// @notice borrowFrom should revert when the caller hasn't granted BURN_TOKENS permission.
214
+ /// @dev The controller enforces this with JBPermissioned_Unauthorized when burnTokensOf is called.
215
+ function test_borrowFrom_revertsWithoutBurnPermission() public {
216
+ // Pay into the revnet to get tokens.
217
+ vm.prank(user);
218
+ uint256 tokenCount =
219
+ jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user, 0, "", "");
220
+ require(tokenCount > 0, "Should have received tokens");
221
+
222
+ // Attempt to borrow WITHOUT granting BURN_TOKENS permission → should revert.
223
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
224
+ vm.prank(user);
225
+ vm.expectRevert(
226
+ abi.encodeWithSelector(
227
+ JBPermissioned.JBPermissioned_Unauthorized.selector,
228
+ user, // account (the token holder)
229
+ address(LOANS_CONTRACT), // sender (the contract trying to burn)
230
+ REVNET_ID, // projectId
231
+ JBPermissionIds.BURN_TOKENS // permissionId
232
+ )
233
+ );
234
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
235
+ }
236
+
237
+ /// @notice borrowFrom should succeed when the caller has granted BURN_TOKENS permission.
238
+ function test_borrowFrom_succeedsWithBurnPermission() public {
239
+ // Pay into the revnet to get tokens.
240
+ vm.prank(user);
241
+ uint256 tokenCount =
242
+ jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user, 0, "", "");
243
+ require(tokenCount > 0, "Should have received tokens");
244
+
245
+ // Grant BURN_TOKENS permission to the loans contract via the real permissions system.
246
+ uint8[] memory permissionIds = new uint8[](1);
247
+ permissionIds[0] = JBPermissionIds.BURN_TOKENS;
248
+ vm.prank(user);
249
+ jbPermissions()
250
+ .setPermissionsFor({
251
+ account: user,
252
+ permissionsData: JBPermissionsData({
253
+ operator: address(LOANS_CONTRACT), projectId: uint64(REVNET_ID), permissionIds: permissionIds
254
+ })
255
+ });
256
+
257
+ // Borrow should now succeed.
258
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
259
+ vm.prank(user);
260
+ (uint256 loanId, REVLoan memory loan) =
261
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25);
262
+
263
+ assertTrue(loanId > 0, "Loan ID should be non-zero");
264
+ assertTrue(loan.createdAt > 0, "Loan should be created");
265
+ assertTrue(loan.amount > 0, "Loan amount should be non-zero");
266
+ }
267
+ }
@@ -0,0 +1,228 @@
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
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
24
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
25
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
26
+ import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
27
+ import {REVLoans} from "../../src/REVLoans.sol";
28
+ import {REVLoan} from "../../src/structs/REVLoan.sol";
29
+ import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
30
+ import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
31
+ import {REVDescription} from "../../src/structs/REVDescription.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
+ /// @notice Validates that liquidateExpiredLoansFrom rejects loan number ranges that would overflow into another
42
+ /// revnet's namespace.
43
+ /// @dev _generateLoanId computes: (revnetId * _ONE_TRILLION) + loanNumber. If startingLoanId + count > _ONE_TRILLION,
44
+ /// the generated loanId would collide with a different revnet's loans.
45
+ contract TestCrossRevnetLiquidation is TestBaseWorkflow {
46
+ // forge-lint: disable-next-line(mixed-case-variable)
47
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
48
+
49
+ // forge-lint: disable-next-line(mixed-case-variable)
50
+ REVDeployer REV_DEPLOYER;
51
+ // forge-lint: disable-next-line(mixed-case-variable)
52
+ JB721TiersHook EXAMPLE_HOOK;
53
+ // forge-lint: disable-next-line(mixed-case-variable)
54
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
55
+ // forge-lint: disable-next-line(mixed-case-variable)
56
+ IJB721TiersHookStore HOOK_STORE;
57
+ // forge-lint: disable-next-line(mixed-case-variable)
58
+ IJBAddressRegistry ADDRESS_REGISTRY;
59
+ // forge-lint: disable-next-line(mixed-case-variable)
60
+ REVLoans LOANS_CONTRACT;
61
+ // forge-lint: disable-next-line(mixed-case-variable)
62
+ MockERC20 TOKEN;
63
+ // forge-lint: disable-next-line(mixed-case-variable)
64
+ IJBSuckerRegistry SUCKER_REGISTRY;
65
+ // forge-lint: disable-next-line(mixed-case-variable)
66
+ CTPublisher PUBLISHER;
67
+ // forge-lint: disable-next-line(mixed-case-variable)
68
+ MockBuybackDataHook MOCK_BUYBACK;
69
+
70
+ // forge-lint: disable-next-line(mixed-case-variable)
71
+ uint256 FEE_PROJECT_ID;
72
+ // forge-lint: disable-next-line(mixed-case-variable)
73
+ uint256 REVNET_ID;
74
+
75
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
76
+
77
+ /// @dev _ONE_TRILLION mirrors the private constant in REVLoans.sol.
78
+ uint256 private constant _ONE_TRILLION = 1_000_000_000_000;
79
+
80
+ function setUp() public override {
81
+ super.setUp();
82
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
83
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
84
+ HOOK_STORE = new JB721TiersHookStore();
85
+ EXAMPLE_HOOK = new JB721TiersHook(
86
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
87
+ );
88
+ ADDRESS_REGISTRY = new JBAddressRegistry();
89
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
90
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
91
+ MOCK_BUYBACK = new MockBuybackDataHook();
92
+ TOKEN = new MockERC20("1/2 ETH", "1/2");
93
+ MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
94
+ vm.prank(multisig());
95
+ jbPrices()
96
+ .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
97
+ LOANS_CONTRACT = new REVLoans({
98
+ controller: jbController(),
99
+ projects: jbProjects(),
100
+ revId: FEE_PROJECT_ID,
101
+ owner: address(this),
102
+ permit2: permit2(),
103
+ trustedForwarder: TRUSTED_FORWARDER
104
+ });
105
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
106
+ jbController(),
107
+ SUCKER_REGISTRY,
108
+ FEE_PROJECT_ID,
109
+ HOOK_DEPLOYER,
110
+ PUBLISHER,
111
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
112
+ address(LOANS_CONTRACT),
113
+ TRUSTED_FORWARDER
114
+ );
115
+ vm.prank(multisig());
116
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
117
+ _deployFeeProject();
118
+ _deployRevnet();
119
+ }
120
+
121
+ function _deployFeeProject() internal {
122
+ JBAccountingContext[] memory acc = new JBAccountingContext[](2);
123
+ acc[0] = JBAccountingContext({
124
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
125
+ });
126
+ acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
127
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
128
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
129
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
130
+ JBSplit[] memory splits = new JBSplit[](1);
131
+ splits[0].beneficiary = payable(multisig());
132
+ splits[0].percent = 10_000;
133
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
134
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
135
+ stages[0] = REVStageConfig({
136
+ startsAtOrAfter: uint40(block.timestamp),
137
+ autoIssuances: ai,
138
+ splitPercent: 2000,
139
+ splits: splits,
140
+ initialIssuance: uint112(1000e18),
141
+ issuanceCutFrequency: 90 days,
142
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
143
+ cashOutTaxRate: 6000,
144
+ extraMetadata: 0
145
+ });
146
+ REVConfig memory cfg = REVConfig({
147
+ // forge-lint: disable-next-line(named-struct-fields)
148
+ description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
149
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
150
+ splitOperator: multisig(),
151
+ stageConfigurations: stages
152
+ });
153
+ vm.prank(multisig());
154
+ REV_DEPLOYER.deployFor({
155
+ revnetId: FEE_PROJECT_ID,
156
+ configuration: cfg,
157
+ terminalConfigurations: tc,
158
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
159
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
160
+ }),
161
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
162
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
163
+ });
164
+ }
165
+
166
+ function _deployRevnet() internal {
167
+ JBAccountingContext[] memory acc = new JBAccountingContext[](2);
168
+ acc[0] = JBAccountingContext({
169
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
170
+ });
171
+ acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
172
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
173
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
174
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
175
+ JBSplit[] memory splits = new JBSplit[](1);
176
+ splits[0].beneficiary = payable(multisig());
177
+ splits[0].percent = 10_000;
178
+ REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
179
+ ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
180
+ stages[0] = REVStageConfig({
181
+ startsAtOrAfter: uint40(block.timestamp),
182
+ autoIssuances: ai,
183
+ splitPercent: 2000,
184
+ splits: splits,
185
+ initialIssuance: uint112(1000e18),
186
+ issuanceCutFrequency: 90 days,
187
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
188
+ cashOutTaxRate: 6000,
189
+ extraMetadata: 0
190
+ });
191
+ REVConfig memory cfg = REVConfig({
192
+ // forge-lint: disable-next-line(named-struct-fields)
193
+ description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
194
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
195
+ splitOperator: multisig(),
196
+ stageConfigurations: stages
197
+ });
198
+ (REVNET_ID,) = REV_DEPLOYER.deployFor({
199
+ revnetId: 0,
200
+ configuration: cfg,
201
+ terminalConfigurations: tc,
202
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
203
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
204
+ }),
205
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
206
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
207
+ });
208
+ }
209
+
210
+ /// @notice liquidateExpiredLoansFrom should revert when startingLoanId would overflow into another revnet's
211
+ /// namespace.
212
+ function test_liquidateExpiredLoans_revertsOnCrossRevnetOverflow() public {
213
+ vm.expectRevert(REVLoans.REVLoans_LoanIdOverflow.selector);
214
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, _ONE_TRILLION, 1);
215
+ }
216
+
217
+ /// @notice liquidateExpiredLoansFrom should work for valid ranges within the revnet's namespace.
218
+ function test_liquidateExpiredLoans_normalRangeStillWorks() public {
219
+ // Calling with a valid range on a revnet with no loans should simply do nothing (no revert).
220
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 1, 5);
221
+ }
222
+
223
+ /// @notice The boundary case where startingLoanId + count == _ONE_TRILLION should succeed.
224
+ function test_liquidateExpiredLoans_boundaryExactlyAtLimit() public {
225
+ // Exactly at the boundary (not over) should not revert.
226
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, _ONE_TRILLION - 1, 1);
227
+ }
228
+ }
@@ -1,28 +1,35 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
7
  import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
- import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
8
+ // import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
9
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
10
  import /* {*} from */ "./../../src/REVDeployer.sol";
11
+ // forge-lint: disable-next-line(unaliased-plain-import)
8
12
  import "@croptop/core-v6/src/CTPublisher.sol";
9
13
  import {MockBuybackDataHook} from "./../mock/MockBuybackDataHook.sol";
14
+ // forge-lint: disable-next-line(unaliased-plain-import)
10
15
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
16
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
17
  import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
18
+ // forge-lint: disable-next-line(unaliased-plain-import)
12
19
  import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
20
+ // forge-lint: disable-next-line(unaliased-plain-import)
13
21
  import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
22
+ // forge-lint: disable-next-line(unaliased-plain-import)
14
23
  import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
15
24
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
25
  import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
17
26
  import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
18
27
  import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
19
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
20
28
  import {REVLoans} from "../../src/REVLoans.sol";
21
29
  import {REVLoan} from "../../src/structs/REVLoan.sol";
22
30
  import {REVStageConfig, REVAutoIssuance} from "../../src/structs/REVStageConfig.sol";
23
31
  import {REVLoanSource} from "../../src/structs/REVLoanSource.sol";
24
32
  import {REVDescription} from "../../src/structs/REVDescription.sol";
25
- import {IREVLoans} from "../../src/interfaces/IREVLoans.sol";
26
33
  import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
27
34
  import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
28
35
  import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
@@ -31,30 +38,44 @@ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStor
31
38
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
32
39
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
33
40
  import {REVEmpty721Config} from "../helpers/REVEmpty721Config.sol";
34
- import {REVCroptopAllowedPost} from "../../src/structs/REVCroptopAllowedPost.sol";
35
41
 
36
42
  /// @notice totalLoansBorrowedFor is a cumulative counter, not an active loan count.
37
43
  /// @dev The rename from numberOfLoansFor to totalLoansBorrowedFor clarifies that the counter only increments
38
44
  /// and never decrements. Repaying or liquidating a loan does NOT reduce the counter. This test verifies that
39
45
  /// the counter remains at its high-water mark after loans are fully repaid and after loans are liquidated.
40
46
  contract TestCumulativeLoanCounter is TestBaseWorkflow {
47
+ // forge-lint: disable-next-line(mixed-case-variable)
41
48
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
42
49
 
50
+ // forge-lint: disable-next-line(mixed-case-variable)
43
51
  REVDeployer REV_DEPLOYER;
52
+ // forge-lint: disable-next-line(mixed-case-variable)
44
53
  JB721TiersHook EXAMPLE_HOOK;
54
+ // forge-lint: disable-next-line(mixed-case-variable)
45
55
  IJB721TiersHookDeployer HOOK_DEPLOYER;
56
+ // forge-lint: disable-next-line(mixed-case-variable)
46
57
  IJB721TiersHookStore HOOK_STORE;
58
+ // forge-lint: disable-next-line(mixed-case-variable)
47
59
  IJBAddressRegistry ADDRESS_REGISTRY;
60
+ // forge-lint: disable-next-line(mixed-case-variable)
48
61
  REVLoans LOANS_CONTRACT;
62
+ // forge-lint: disable-next-line(mixed-case-variable)
49
63
  IJBSuckerRegistry SUCKER_REGISTRY;
64
+ // forge-lint: disable-next-line(mixed-case-variable)
50
65
  CTPublisher PUBLISHER;
66
+ // forge-lint: disable-next-line(mixed-case-variable)
51
67
  MockBuybackDataHook MOCK_BUYBACK;
52
68
 
69
+ // forge-lint: disable-next-line(mixed-case-variable)
53
70
  uint256 FEE_PROJECT_ID;
71
+ // forge-lint: disable-next-line(mixed-case-variable)
54
72
  uint256 REVNET_ID;
55
73
 
74
+ // forge-lint: disable-next-line(mixed-case-variable)
56
75
  address USER1 = makeAddr("user1");
76
+ // forge-lint: disable-next-line(mixed-case-variable)
57
77
  address USER2 = makeAddr("user2");
78
+ // forge-lint: disable-next-line(mixed-case-variable)
58
79
  address USER3 = makeAddr("user3");
59
80
 
60
81
  address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
@@ -129,6 +150,7 @@ contract TestCumulativeLoanCounter is TestBaseWorkflow {
129
150
  extraMetadata: 0
130
151
  });
131
152
  REVConfig memory cfg = REVConfig({
153
+ // forge-lint: disable-next-line(named-struct-fields)
132
154
  description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
133
155
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
134
156
  splitOperator: multisig(),
@@ -172,6 +194,7 @@ contract TestCumulativeLoanCounter is TestBaseWorkflow {
172
194
  extraMetadata: 0
173
195
  });
174
196
  REVConfig memory cfg = REVConfig({
197
+ // forge-lint: disable-next-line(named-struct-fields)
175
198
  description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
176
199
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
177
200
  splitOperator: multisig(),