@rev-net/core-v6 0.0.37 → 0.0.40

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 (112) 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 +69 -67
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +26 -22
  11. package/src/REVOwner.sol +147 -29
  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/src/structs/REVAutoIssuance.sol +4 -2
  16. package/src/structs/REVConfig.sol +8 -5
  17. package/src/structs/REVDescription.sol +6 -5
  18. package/src/structs/REVLoan.sol +8 -5
  19. package/src/structs/REVStageConfig.sol +14 -16
  20. package/ADMINISTRATION.md +0 -73
  21. package/ARCHITECTURE.md +0 -116
  22. package/AUDIT_INSTRUCTIONS.md +0 -90
  23. package/RISKS.md +0 -107
  24. package/SKILLS.md +0 -46
  25. package/STYLE_GUIDE.md +0 -610
  26. package/USER_JOURNEYS.md +0 -195
  27. package/foundry.lock +0 -11
  28. package/slither-ci.config.json +0 -10
  29. package/sphinx.lock +0 -507
  30. package/test/REV.integrations.t.sol +0 -573
  31. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  32. package/test/REVDeployerRegressions.t.sol +0 -396
  33. package/test/REVInvincibility.t.sol +0 -1371
  34. package/test/REVInvincibilityHandler.sol +0 -387
  35. package/test/REVLifecycle.t.sol +0 -420
  36. package/test/REVLoans.invariants.t.sol +0 -724
  37. package/test/REVLoansAttacks.t.sol +0 -816
  38. package/test/REVLoansFeeRecovery.t.sol +0 -783
  39. package/test/REVLoansFindings.t.sol +0 -711
  40. package/test/REVLoansRegressions.t.sol +0 -364
  41. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  42. package/test/REVLoansSourced.t.sol +0 -1839
  43. package/test/REVLoansUnSourced.t.sol +0 -409
  44. package/test/TestAuditFixVerification.t.sol +0 -675
  45. package/test/TestBurnHeldTokens.t.sol +0 -394
  46. package/test/TestCEIPattern.t.sol +0 -508
  47. package/test/TestCashOutCallerValidation.t.sol +0 -452
  48. package/test/TestConversionDocumentation.t.sol +0 -365
  49. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  50. package/test/TestCrossSourceReallocation.t.sol +0 -361
  51. package/test/TestERC2771MetaTx.t.sol +0 -585
  52. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  53. package/test/TestFlashLoanSurplus.t.sol +0 -365
  54. package/test/TestHiddenTokens.t.sol +0 -474
  55. package/test/TestHookArrayOOB.t.sol +0 -278
  56. package/test/TestLiquidationBehavior.t.sol +0 -398
  57. package/test/TestLoanSourceRotation.t.sol +0 -553
  58. package/test/TestLoansCashOutDelay.t.sol +0 -493
  59. package/test/TestLongTailEconomics.t.sol +0 -677
  60. package/test/TestLowFindings.t.sol +0 -677
  61. package/test/TestMixedFixes.t.sol +0 -593
  62. package/test/TestPermit2Signatures.t.sol +0 -683
  63. package/test/TestReallocationSandwich.t.sol +0 -412
  64. package/test/TestRevnetRegressions.t.sol +0 -350
  65. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  66. package/test/TestSplitWeightE2E.t.sol +0 -605
  67. package/test/TestSplitWeightFork.t.sol +0 -855
  68. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  69. package/test/TestSwapTerminalPermission.t.sol +0 -262
  70. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  71. package/test/TestUint112Overflow.t.sol +0 -311
  72. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  73. package/test/TestZeroRepayment.t.sol +0 -354
  74. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  75. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  76. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  77. package/test/audit/NemesisVerification.t.sol +0 -97
  78. package/test/audit/OperatorDelegation.t.sol +0 -356
  79. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  80. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  81. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  82. package/test/audit/ReallocatePermission.t.sol +0 -363
  83. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  84. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  85. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  86. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  87. package/test/fork/ForkTestBase.sol +0 -727
  88. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  89. package/test/fork/TestCashOutFork.t.sol +0 -253
  90. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  91. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  92. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  93. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  94. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  95. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  96. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  97. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  98. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  99. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  100. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  101. package/test/helpers/MaliciousContracts.sol +0 -247
  102. package/test/helpers/REVEmpty721Config.sol +0 -45
  103. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  104. package/test/mock/MockBuybackDataHook.sol +0 -112
  105. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  106. package/test/mock/MockSuckerRegistry.sol +0 -17
  107. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  108. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  109. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  110. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  111. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  112. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,711 +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
- // import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
9
- // forge-lint: disable-next-line(unaliased-plain-import)
10
- import /* {*} from */ "./../src/REVDeployer.sol";
11
- // forge-lint: disable-next-line(unaliased-plain-import)
12
- import "@croptop/core-v6/src/CTPublisher.sol";
13
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
14
-
15
- // forge-lint: disable-next-line(unaliased-plain-import)
16
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
17
- // forge-lint: disable-next-line(unaliased-plain-import)
18
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
19
- // forge-lint: disable-next-line(unaliased-plain-import)
20
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
21
- // forge-lint: disable-next-line(unaliased-plain-import)
22
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
23
- // forge-lint: disable-next-line(unaliased-plain-import)
24
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
25
-
26
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
27
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
28
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
29
- import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
30
- import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
31
- import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
32
- import {REVLoans} from "../src/REVLoans.sol";
33
- import {REVLoan} from "../src/structs/REVLoan.sol";
34
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
35
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
36
- import {REVDescription} from "../src/structs/REVDescription.sol";
37
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
38
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
39
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
40
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
41
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
42
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
43
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
44
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
45
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
46
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
47
- import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
48
- import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
49
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
50
- import {REVOwner} from "../src/REVOwner.sol";
51
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
52
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
53
-
54
- /// @notice A fake terminal that returns garbage accounting contexts.
55
- /// Used to test unvalidated loan source terminal rejection.
56
- contract GarbageTerminal is ERC165, IJBPayoutTerminal {
57
- function useAllowanceOf(
58
- uint256,
59
- address,
60
- uint256,
61
- uint256,
62
- uint256,
63
- address payable,
64
- address payable,
65
- string calldata
66
- )
67
- external
68
- pure
69
- override
70
- returns (uint256)
71
- {
72
- return 0;
73
- }
74
-
75
- function accountingContextForTokenOf(uint256, address) external pure override returns (JBAccountingContext memory) {
76
- // Return garbage values to demonstrate the danger.
77
- return JBAccountingContext({token: address(0xdead), decimals: 42, currency: 999_999});
78
- }
79
-
80
- function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {
81
- JBAccountingContext[] memory contexts = new JBAccountingContext[](1);
82
- contexts[0] = JBAccountingContext({token: address(0xdead), decimals: 42, currency: 999_999});
83
- return contexts;
84
- }
85
-
86
- function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
87
-
88
- function addToBalanceOf(
89
- uint256,
90
- address,
91
- uint256,
92
- bool,
93
- string calldata,
94
- bytes calldata
95
- )
96
- external
97
- payable
98
- override
99
- {}
100
-
101
- function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
102
- return 0;
103
- }
104
-
105
- function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
106
- return 0;
107
- }
108
-
109
- function pay(
110
- uint256,
111
- address,
112
- uint256,
113
- address,
114
- uint256,
115
- string calldata,
116
- bytes calldata
117
- )
118
- external
119
- payable
120
- override
121
- returns (uint256)
122
- {
123
- return 0;
124
- }
125
-
126
- function sendPayoutsOf(uint256, address, uint256, uint256, uint256) external pure override returns (uint256) {
127
- return 0;
128
- }
129
-
130
- function previewPayFor(
131
- uint256,
132
- address,
133
- uint256,
134
- address,
135
- bytes calldata
136
- )
137
- external
138
- pure
139
- override
140
- returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
141
- {
142
- JBRuleset memory ruleset;
143
- return (ruleset, 0, 0, new JBPayHookSpecification[](0));
144
- }
145
-
146
- function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
147
- return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
148
- || super.supportsInterface(interfaceId);
149
- }
150
-
151
- receive() external payable {}
152
- }
153
-
154
- /// @notice Regression tests for loan findings.
155
- /// Unvalidated loan source terminal
156
- /// RepayLoan event emits zeroed values
157
- /// Auto-issuance timing guard bypass (false positive)
158
- /// repayLoan revert on excess collateral (false positive)
159
- contract REVLoansFindings is TestBaseWorkflow {
160
- // forge-lint: disable-next-line(mixed-case-variable)
161
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
162
- // forge-lint: disable-next-line(mixed-case-variable)
163
- bytes32 ERC20_SALT = "REV_TOKEN";
164
-
165
- // forge-lint: disable-next-line(mixed-case-variable)
166
- REVDeployer REV_DEPLOYER;
167
- // forge-lint: disable-next-line(mixed-case-variable)
168
- REVOwner REV_OWNER;
169
- // forge-lint: disable-next-line(mixed-case-variable)
170
- JB721TiersHook EXAMPLE_HOOK;
171
- // forge-lint: disable-next-line(mixed-case-variable)
172
- IJB721TiersHookDeployer HOOK_DEPLOYER;
173
- // forge-lint: disable-next-line(mixed-case-variable)
174
- IJB721TiersHookStore HOOK_STORE;
175
- // forge-lint: disable-next-line(mixed-case-variable)
176
- IJBAddressRegistry ADDRESS_REGISTRY;
177
- // forge-lint: disable-next-line(mixed-case-variable)
178
- IREVLoans LOANS_CONTRACT;
179
- // forge-lint: disable-next-line(mixed-case-variable)
180
- IJBSuckerRegistry SUCKER_REGISTRY;
181
- // forge-lint: disable-next-line(mixed-case-variable)
182
- CTPublisher PUBLISHER;
183
- // forge-lint: disable-next-line(mixed-case-variable)
184
- MockBuybackDataHook MOCK_BUYBACK;
185
-
186
- // forge-lint: disable-next-line(mixed-case-variable)
187
- uint256 FEE_PROJECT_ID;
188
- // forge-lint: disable-next-line(mixed-case-variable)
189
- uint256 REVNET_ID;
190
-
191
- // forge-lint: disable-next-line(mixed-case-variable)
192
- address USER = makeAddr("user");
193
-
194
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
195
-
196
- function setUp() public override {
197
- super.setUp();
198
-
199
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
200
-
201
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
202
- HOOK_STORE = new JB721TiersHookStore();
203
- EXAMPLE_HOOK = new JB721TiersHook(
204
- jbDirectory(),
205
- jbPermissions(),
206
- jbPrices(),
207
- jbRulesets(),
208
- HOOK_STORE,
209
- jbSplits(),
210
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
211
- multisig()
212
- );
213
- ADDRESS_REGISTRY = new JBAddressRegistry();
214
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
215
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
216
- MOCK_BUYBACK = new MockBuybackDataHook();
217
-
218
- LOANS_CONTRACT = new REVLoans({
219
- controller: jbController(),
220
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
221
- revId: FEE_PROJECT_ID,
222
- owner: address(this),
223
- permit2: permit2(),
224
- trustedForwarder: TRUSTED_FORWARDER
225
- });
226
-
227
- REV_OWNER = new REVOwner(
228
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
229
- jbDirectory(),
230
- FEE_PROJECT_ID,
231
- SUCKER_REGISTRY,
232
- address(LOANS_CONTRACT),
233
- address(0)
234
- );
235
-
236
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
237
- jbController(),
238
- SUCKER_REGISTRY,
239
- FEE_PROJECT_ID,
240
- HOOK_DEPLOYER,
241
- PUBLISHER,
242
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
243
- address(LOANS_CONTRACT),
244
- TRUSTED_FORWARDER,
245
- address(REV_OWNER)
246
- );
247
-
248
- REV_OWNER.setDeployer(REV_DEPLOYER);
249
-
250
- vm.prank(multisig());
251
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
252
-
253
- // Deploy the fee revnet (project ID 1).
254
- _deployFeeRevnet();
255
-
256
- // Deploy a second revnet to borrow from.
257
- _deployBorrowableRevnet();
258
-
259
- // Give user ETH.
260
- vm.deal(USER, 100e18);
261
- }
262
-
263
- function _deployFeeRevnet() internal {
264
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
265
- accountingContextsToAccept[0] = JBAccountingContext({
266
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
267
- });
268
-
269
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
270
- terminalConfigurations[0] =
271
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
272
-
273
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
274
- JBSplit[] memory splits = new JBSplit[](1);
275
- splits[0].beneficiary = payable(multisig());
276
- splits[0].percent = 10_000;
277
-
278
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
279
- issuanceConfs[0] =
280
- REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
281
-
282
- stageConfigurations[0] = REVStageConfig({
283
- startsAtOrAfter: uint40(block.timestamp),
284
- autoIssuances: issuanceConfs,
285
- splitPercent: 2000,
286
- splits: splits,
287
- initialIssuance: uint112(1000e18),
288
- issuanceCutFrequency: 90 days,
289
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
290
- cashOutTaxRate: 6000,
291
- extraMetadata: 0
292
- });
293
-
294
- REVConfig memory revnetConfiguration = REVConfig({
295
- // forge-lint: disable-next-line(named-struct-fields)
296
- description: REVDescription("Revnet", "$REV", "ipfs://test", ERC20_SALT),
297
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
298
- splitOperator: multisig(),
299
- stageConfigurations: stageConfigurations
300
- });
301
-
302
- vm.prank(multisig());
303
- REV_DEPLOYER.deployFor({
304
- revnetId: FEE_PROJECT_ID,
305
- configuration: revnetConfiguration,
306
- terminalConfigurations: terminalConfigurations,
307
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
308
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
309
- }),
310
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
311
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
312
- });
313
- }
314
-
315
- function _deployBorrowableRevnet() internal {
316
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
317
- accountingContextsToAccept[0] = JBAccountingContext({
318
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
319
- });
320
-
321
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
322
- terminalConfigurations[0] =
323
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
324
-
325
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
326
- JBSplit[] memory splits = new JBSplit[](1);
327
- splits[0].beneficiary = payable(multisig());
328
- splits[0].percent = 10_000;
329
-
330
- stageConfigurations[0] = REVStageConfig({
331
- startsAtOrAfter: uint40(block.timestamp),
332
- autoIssuances: new REVAutoIssuance[](0),
333
- splitPercent: 0,
334
- splits: splits,
335
- initialIssuance: uint112(1000e18),
336
- issuanceCutFrequency: 0,
337
- issuanceCutPercent: 0,
338
- cashOutTaxRate: 5000,
339
- extraMetadata: 0
340
- });
341
-
342
- REVConfig memory revnetConfiguration = REVConfig({
343
- // forge-lint: disable-next-line(named-struct-fields)
344
- description: REVDescription("Borrowable", "BRW", "ipfs://brw", "BRW_TOKEN"),
345
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
346
- splitOperator: multisig(),
347
- stageConfigurations: stageConfigurations
348
- });
349
-
350
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
351
- revnetId: 0,
352
- configuration: revnetConfiguration,
353
- terminalConfigurations: terminalConfigurations,
354
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
355
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("BRW"))
356
- }),
357
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
358
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
359
- });
360
- }
361
-
362
- /// @notice Helper: pay into the revnet and get tokens.
363
- function _payAndGetTokens(uint256 amount) internal returns (uint256 tokens) {
364
- vm.prank(USER);
365
- tokens = jbMultiTerminal().pay{value: amount}(REVNET_ID, JBConstants.NATIVE_TOKEN, amount, USER, 0, "", "");
366
- }
367
-
368
- /// @notice Helper: mock permission for LOANS_CONTRACT to burn user tokens.
369
- function _mockBurnPermission() internal {
370
- mockExpect(
371
- address(jbPermissions()),
372
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)),
373
- abi.encode(true)
374
- );
375
- }
376
-
377
- /// @notice Helper: borrow against tokens.
378
- function _borrow(uint256 tokens) internal returns (uint256 loanId, REVLoan memory loan, uint256 loanable) {
379
- loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
380
-
381
- _mockBurnPermission();
382
-
383
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
384
-
385
- vm.prank(USER);
386
- (loanId, loan) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, loanable, tokens, payable(USER), 25, USER);
387
- }
388
-
389
- //*********************************************************************//
390
- // --------- Unvalidated Loan Source Terminal ------------------- //
391
- //*********************************************************************//
392
-
393
- /// @notice borrowFrom rejects a fake terminal not registered in the directory.
394
- function test_borrowFromRejectsUnregisteredTerminal() public {
395
- // Step 1: User pays into the revnet to get tokens.
396
- uint256 tokens = _payAndGetTokens(1e18);
397
- assertGt(tokens, 0, "user should receive tokens");
398
-
399
- // Step 2: Create a fake terminal that returns garbage accounting contexts.
400
- GarbageTerminal fakeTerminal = new GarbageTerminal();
401
-
402
- // Step 3: Verify the fake terminal is NOT in the directory.
403
- assertFalse(
404
- jbDirectory().isTerminalOf(REVNET_ID, IJBTerminal(address(fakeTerminal))),
405
- "fake terminal should NOT be registered"
406
- );
407
-
408
- // Step 4: Attempt to borrow using the fake terminal.
409
- uint256 loanable =
410
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
411
- assertGt(loanable, 0, "should have borrowable amount");
412
-
413
- // NOTE: Do NOT mock burn permission here. The call should revert
414
- // at the terminal validation check before it ever reaches the burn step.
415
-
416
- REVLoanSource memory fakeSource =
417
- REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: IJBPayoutTerminal(address(fakeTerminal))});
418
-
419
- // Step 5: Expect revert with the new REVLoans_InvalidTerminal error.
420
- vm.expectRevert(
421
- abi.encodeWithSelector(REVLoans.REVLoans_InvalidTerminal.selector, address(fakeTerminal), REVNET_ID)
422
- );
423
-
424
- vm.prank(USER);
425
- LOANS_CONTRACT.borrowFrom(REVNET_ID, fakeSource, loanable, tokens, payable(USER), 25, USER);
426
- }
427
-
428
- //*********************************************************************//
429
- // ------- RepayLoan Event Emits Zeroed Values ----------------- //
430
- //*********************************************************************//
431
-
432
- /// @notice RepayLoan event emits non-zero loan amount and collateral
433
- /// when fully repaying a loan.
434
- function test_repayLoanEventEmitsNonZeroValues() public {
435
- // Step 1: Pay in and borrow.
436
- uint256 tokens = _payAndGetTokens(1e18);
437
- (uint256 loanId, REVLoan memory loan,) = _borrow(tokens);
438
-
439
- assertGt(loan.amount, 0, "loan amount should be non-zero");
440
- assertGt(loan.collateral, 0, "loan collateral should be non-zero");
441
-
442
- // Step 2: Calculate the repay amount (loan amount + source fee).
443
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
444
- uint256 totalRepay = loan.amount + sourceFee;
445
-
446
- // Step 3: Give user enough ETH for repayment.
447
- vm.deal(USER, totalRepay);
448
-
449
- // Step 4: Expect the RepayLoan event with non-zero loan values.
450
- // The `loan` field (4th param) should contain the original pre-repay loan data.
451
- // We check that the event is emitted by looking for the indexed fields.
452
- vm.expectEmit(true, true, true, false);
453
- emit IREVLoans.RepayLoan({
454
- loanId: loanId,
455
- revnetId: REVNET_ID,
456
- paidOffLoanId: loanId,
457
- // These fields are the ones we care about -- they should be non-zero.
458
- loan: loan,
459
- paidOffLoan: loan, // placeholder, we only check the `loan` field
460
- repayBorrowAmount: totalRepay,
461
- sourceFeeAmount: sourceFee,
462
- collateralCountToReturn: loan.collateral,
463
- beneficiary: payable(USER),
464
- caller: USER
465
- });
466
-
467
- // Step 5: Fully repay the loan (return all collateral).
468
- JBSingleAllowance memory allowance;
469
- vm.prank(USER);
470
- LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, loan.collateral, payable(USER), allowance);
471
- }
472
-
473
- /// @notice Secondary check: verify the original loan data in the emitted event
474
- /// has the expected non-zero amount and collateral by recording logs.
475
- function test_repayLoanEventLoanFieldIsNonZero() public {
476
- // Step 1: Pay in and borrow.
477
- uint256 tokens = _payAndGetTokens(1e18);
478
- (uint256 loanId, REVLoan memory loan,) = _borrow(tokens);
479
-
480
- uint256 originalAmount = loan.amount;
481
- uint256 originalCollateral = loan.collateral;
482
-
483
- // Step 2: Calculate repay amount.
484
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
485
- uint256 totalRepay = loan.amount + sourceFee;
486
- vm.deal(USER, totalRepay);
487
-
488
- // Step 3: Record logs to inspect the event data.
489
- vm.recordLogs();
490
-
491
- JBSingleAllowance memory allowance;
492
- vm.prank(USER);
493
- LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, loan.collateral, payable(USER), allowance);
494
-
495
- // Step 4: Find the RepayLoan event and decode the loan struct.
496
- Vm.Log[] memory entries = vm.getRecordedLogs();
497
- bytes32 repayLoanSig = keccak256(
498
- "RepayLoan(uint256,uint256,uint256,(uint112,uint112,uint48,uint16,uint32,(address,address)),(uint112,uint112,uint48,uint16,uint32,(address,address)),uint256,uint256,uint256,address,address)"
499
- );
500
-
501
- bool foundEvent = false;
502
- for (uint256 i = 0; i < entries.length; i++) {
503
- if (entries[i].topics[0] == repayLoanSig) {
504
- foundEvent = true;
505
- // Decode the non-indexed data.
506
- // The data contains: loan, paidOffLoan, repayBorrowAmount, sourceFeeAmount,
507
- // collateralCountToReturn, beneficiary, caller
508
- (REVLoan memory emittedLoan,,,,,,) =
509
- abi.decode(entries[i].data, (REVLoan, REVLoan, uint256, uint256, uint256, address, address));
510
-
511
- // The emitted loan should have the ORIGINAL non-zero values.
512
- assertEq(emittedLoan.amount, originalAmount, "emitted loan.amount should match original");
513
- assertEq(emittedLoan.collateral, originalCollateral, "emitted loan.collateral should match original");
514
- assertGt(emittedLoan.amount, 0, "emitted loan.amount must be non-zero");
515
- assertGt(emittedLoan.collateral, 0, "emitted loan.collateral must be non-zero");
516
- break;
517
- }
518
- }
519
-
520
- assertTrue(foundEvent, "RepayLoan event should have been emitted");
521
- }
522
-
523
- //*********************************************************************//
524
- // --- Auto-Issuance Timing Guard Works Correctly ------------- //
525
- //*********************************************************************//
526
-
527
- /// @notice Proves that block.timestamp + i matches actual ruleset IDs,
528
- /// and the timing guard in autoIssueFor correctly prevents premature issuance.
529
- function test_autoIssueTimingGuardWorksCorrectly() public {
530
- // Step 1: Deploy a revnet with 2 stages where stage 2 starts far in the future
531
- // and has auto-issuance.
532
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](1);
533
- accountingContextsToAccept[0] = JBAccountingContext({
534
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
535
- });
536
-
537
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
538
- terminalConfigurations[0] =
539
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
540
-
541
- REVStageConfig[] memory stages = new REVStageConfig[](2);
542
- JBSplit[] memory splits = new JBSplit[](1);
543
- splits[0].beneficiary = payable(multisig());
544
- splits[0].percent = 10_000;
545
-
546
- // Stage 1: starts now, no auto-issuance.
547
- stages[0] = REVStageConfig({
548
- startsAtOrAfter: uint40(block.timestamp),
549
- autoIssuances: new REVAutoIssuance[](0),
550
- splitPercent: 2000,
551
- splits: splits,
552
- initialIssuance: uint112(1000e18),
553
- issuanceCutFrequency: 0,
554
- issuanceCutPercent: 0,
555
- cashOutTaxRate: 5000,
556
- extraMetadata: 0
557
- });
558
-
559
- // Stage 2: starts 365 days in the future, HAS auto-issuance.
560
- REVAutoIssuance[] memory stage2AutoIssuances = new REVAutoIssuance[](1);
561
- stage2AutoIssuances[0] =
562
- REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(50_000e18), beneficiary: multisig()});
563
-
564
- stages[1] = REVStageConfig({
565
- startsAtOrAfter: uint40(block.timestamp + 365 days),
566
- autoIssuances: stage2AutoIssuances,
567
- splitPercent: 1000,
568
- splits: splits,
569
- initialIssuance: uint112(500e18),
570
- issuanceCutFrequency: 0,
571
- issuanceCutPercent: 0,
572
- cashOutTaxRate: 3000,
573
- extraMetadata: 0
574
- });
575
-
576
- REVConfig memory config = REVConfig({
577
- // forge-lint: disable-next-line(named-struct-fields)
578
- description: REVDescription("FP1Test", "FP1", "ipfs://fp1", "FP1_TOKEN"),
579
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
580
- splitOperator: multisig(),
581
- stageConfigurations: stages
582
- });
583
-
584
- // Record the deploy timestamp -- this is used for stage ID calculation.
585
- uint256 deployTimestamp = block.timestamp;
586
-
587
- (uint256 fp1RevnetId,) = REV_DEPLOYER.deployFor({
588
- revnetId: 0,
589
- configuration: config,
590
- terminalConfigurations: terminalConfigurations,
591
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
592
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FP1")
593
- }),
594
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
595
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
596
- });
597
-
598
- // Step 2: Verify the second ruleset ID matches deployTimestamp + 1.
599
- // JBRulesets assigns IDs as: block.timestamp, block.timestamp+1, etc. when queued in one tx.
600
- uint256 stage2RulesetId = deployTimestamp + 1;
601
-
602
- (JBRuleset memory ruleset,) = jbController().getRulesetOf({projectId: fp1RevnetId, rulesetId: stage2RulesetId});
603
-
604
- // The ruleset should exist and have the correct startsAtOrAfter.
605
- assertGt(ruleset.id, 0, "stage 2 ruleset should exist");
606
- assertEq(ruleset.start, deployTimestamp + 365 days, "stage 2 should start 365 days from deploy");
607
-
608
- // Step 3: Verify amountToAutoIssue was stored with the correct stage ID.
609
- uint256 storedAutoIssue = REV_DEPLOYER.amountToAutoIssue(fp1RevnetId, stage2RulesetId, multisig());
610
- assertEq(storedAutoIssue, 50_000e18, "auto-issuance amount should be stored at deployTimestamp + 1");
611
-
612
- // Step 4: Call autoIssueFor now -- it should revert because stage 2 hasn't started yet.
613
- vm.expectRevert(abi.encodeWithSelector(REVDeployer.REVDeployer_StageNotStarted.selector, stage2RulesetId));
614
- REV_DEPLOYER.autoIssueFor(fp1RevnetId, stage2RulesetId, multisig());
615
-
616
- // Step 5: Warp to after stage 2 starts and verify auto-issuance works.
617
- vm.warp(deployTimestamp + 365 days + 1);
618
-
619
- REV_DEPLOYER.autoIssueFor(fp1RevnetId, stage2RulesetId, multisig());
620
-
621
- // Verify the tokens were minted.
622
- uint256 balance = jbController().TOKENS().totalBalanceOf({holder: multisig(), projectId: fp1RevnetId});
623
- assertGe(balance, 50_000e18, "multisig should have received the auto-issued tokens");
624
- }
625
-
626
- //*********************************************************************//
627
- // --- repayLoan Correctly Reverts On Excess Collateral ------- //
628
- //*********************************************************************//
629
-
630
- /// @notice When collateral value exceeds the loan amount (e.g. from price appreciation
631
- /// or surplus growth), partial repayment correctly reverts because the remaining
632
- /// collateral supports more than the loan amount. reallocateCollateralFromLoan
633
- /// is the correct alternative.
634
- function test_repayLoanCorrectlyRevertsOnExcessCollateral() public {
635
- // Step 1: User pays in and borrows.
636
- uint256 tokens = _payAndGetTokens(1e18);
637
- (uint256 loanId, REVLoan memory loan,) = _borrow(tokens);
638
-
639
- uint256 loanAmount = loan.amount;
640
- uint256 loanCollateral = loan.collateral;
641
-
642
- assertGt(loanAmount, 0, "loan should have non-zero amount");
643
- assertGt(loanCollateral, 0, "loan should have non-zero collateral");
644
-
645
- // Step 2: Simulate surplus growth by adding balance directly (no token minting).
646
- // Using addToBalanceOf increases surplus without increasing supply, so each token
647
- // is now backed by more surplus and the collateral value exceeds the loan amount.
648
- {
649
- address whale = makeAddr("whale");
650
- vm.deal(whale, 50e18);
651
- vm.prank(whale);
652
- jbMultiTerminal().addToBalanceOf{value: 50e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 50e18, false, "", "");
653
- }
654
-
655
- // Step 3: Verify the collateral value has increased.
656
- {
657
- uint256 newBorrowable = LOANS_CONTRACT.borrowableAmountFrom(
658
- REVNET_ID, loanCollateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
659
- );
660
- assertGt(
661
- newBorrowable, loanAmount, "collateral value should exceed original loan amount after surplus growth"
662
- );
663
- }
664
-
665
- // Step 4: Try to repay returning only SOME collateral such that remaining collateral
666
- // supports more than the loan amount. This should revert with
667
- // REVLoans_NewBorrowAmountGreaterThanLoanAmount.
668
- {
669
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loanAmount);
670
- uint256 totalRepay = loanAmount + sourceFee;
671
- vm.deal(USER, totalRepay);
672
-
673
- JBSingleAllowance memory allowance;
674
-
675
- vm.prank(USER);
676
- vm.expectRevert(); // REVLoans_NewBorrowAmountGreaterThanLoanAmount
677
- LOANS_CONTRACT.repayLoan{value: totalRepay}(loanId, totalRepay, 1, payable(USER), allowance);
678
- }
679
-
680
- // Step 5: Show that reallocateCollateralFromLoan is the correct alternative.
681
- // The user can reallocate excess collateral to a new loan instead.
682
- _mockBurnPermission();
683
-
684
- uint256 collateralToTransfer = loanCollateral / 10;
685
-
686
- uint256 minBorrow = LOANS_CONTRACT.borrowableAmountFrom(
687
- REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
688
- );
689
-
690
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
691
-
692
- vm.prank(USER);
693
- (,, REVLoan memory reallocatedLoan, REVLoan memory newLoan) = LOANS_CONTRACT.reallocateCollateralFromLoan({
694
- loanId: loanId,
695
- collateralCountToTransfer: collateralToTransfer,
696
- source: source,
697
- minBorrowAmount: minBorrow,
698
- collateralCountToAdd: 0,
699
- beneficiary: payable(USER),
700
- prepaidFeePercent: 25
701
- });
702
-
703
- // Verify the reallocation succeeded.
704
- assertEq(
705
- reallocatedLoan.collateral,
706
- loanCollateral - collateralToTransfer,
707
- "reallocated loan should have reduced collateral"
708
- );
709
- assertGt(newLoan.collateral, 0, "new loan should have collateral from transfer");
710
- }
711
- }