@rev-net/core-v6 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +17 -10
  11. package/src/REVOwner.sol +121 -14
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -107
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -365
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  71. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  72. package/test/audit/NemesisVerification.t.sol +0 -97
  73. package/test/audit/OperatorDelegation.t.sol +0 -356
  74. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  75. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  76. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  77. package/test/audit/ReallocatePermission.t.sol +0 -363
  78. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  79. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  80. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  81. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  82. package/test/fork/ForkTestBase.sol +0 -727
  83. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  84. package/test/fork/TestCashOutFork.t.sol +0 -253
  85. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  86. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  87. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  88. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  89. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  90. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  91. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  92. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  93. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  94. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  95. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  96. package/test/helpers/MaliciousContracts.sol +0 -247
  97. package/test/helpers/REVEmpty721Config.sol +0 -45
  98. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  99. package/test/mock/MockBuybackDataHook.sol +0 -112
  100. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  101. package/test/mock/MockSuckerRegistry.sol +0 -17
  102. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  103. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  104. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  105. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  106. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  107. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,677 +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
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
26
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
- import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
28
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
29
- import {REVLoans} from "../src/REVLoans.sol";
30
- import {REVLoan} from "../src/structs/REVLoan.sol";
31
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
32
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
33
- import {REVDescription} from "../src/structs/REVDescription.sol";
34
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
35
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
36
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
37
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
38
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
39
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
40
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
41
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
42
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
43
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
44
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
45
- import {REVOwner} from "../src/REVOwner.sol";
46
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
47
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
48
-
49
- struct FeeProjectConfig {
50
- REVConfig configuration;
51
- JBTerminalConfig[] terminalConfigurations;
52
- REVSuckerDeploymentConfig suckerDeploymentConfiguration;
53
- }
54
-
55
- contract TestLowFindings is TestBaseWorkflow {
56
- // forge-lint: disable-next-line(mixed-case-variable)
57
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
58
- // forge-lint: disable-next-line(mixed-case-variable)
59
- bytes32 ERC20_SALT = "REV_TOKEN";
60
-
61
- // forge-lint: disable-next-line(mixed-case-variable)
62
- REVDeployer REV_DEPLOYER;
63
- // forge-lint: disable-next-line(mixed-case-variable)
64
- REVOwner REV_OWNER;
65
- // forge-lint: disable-next-line(mixed-case-variable)
66
- JB721TiersHook EXAMPLE_HOOK;
67
- // forge-lint: disable-next-line(mixed-case-variable)
68
- IJB721TiersHookDeployer HOOK_DEPLOYER;
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
- IJB721TiersHookStore HOOK_STORE;
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- IJBAddressRegistry ADDRESS_REGISTRY;
73
- // forge-lint: disable-next-line(mixed-case-variable)
74
- IREVLoans LOANS_CONTRACT;
75
- // forge-lint: disable-next-line(mixed-case-variable)
76
- MockERC20 TOKEN;
77
- // forge-lint: disable-next-line(mixed-case-variable)
78
- IJBSuckerRegistry SUCKER_REGISTRY;
79
- // forge-lint: disable-next-line(mixed-case-variable)
80
- CTPublisher PUBLISHER;
81
- // forge-lint: disable-next-line(mixed-case-variable)
82
- MockBuybackDataHook MOCK_BUYBACK;
83
-
84
- // forge-lint: disable-next-line(mixed-case-variable)
85
- uint256 FEE_PROJECT_ID;
86
- // forge-lint: disable-next-line(mixed-case-variable)
87
- uint256 REVNET_ID;
88
-
89
- // forge-lint: disable-next-line(mixed-case-variable)
90
- address USER = makeAddr("user");
91
-
92
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
93
-
94
- function getFeeProjectConfig() internal view returns (FeeProjectConfig memory) {
95
- uint8 decimals = 18;
96
- uint256 decimalMultiplier = 10 ** decimals;
97
-
98
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
99
- accountingContextsToAccept[0] = JBAccountingContext({
100
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
101
- });
102
- accountingContextsToAccept[1] =
103
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
104
-
105
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
106
- terminalConfigurations[0] =
107
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
108
-
109
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
110
- JBSplit[] memory splits = new JBSplit[](1);
111
- splits[0].beneficiary = payable(multisig());
112
- splits[0].percent = 10_000;
113
-
114
- stageConfigurations[0] = REVStageConfig({
115
- startsAtOrAfter: uint40(block.timestamp),
116
- autoIssuances: new REVAutoIssuance[](0),
117
- splitPercent: 2000,
118
- splits: splits,
119
- // forge-lint: disable-next-line(unsafe-typecast)
120
- initialIssuance: uint112(1000 * decimalMultiplier),
121
- issuanceCutFrequency: 90 days,
122
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
123
- cashOutTaxRate: 6000,
124
- extraMetadata: 0
125
- });
126
-
127
- REVConfig memory revnetConfiguration = REVConfig({
128
- // forge-lint: disable-next-line(named-struct-fields)
129
- description: REVDescription("Revnet", "$REV", "ipfs://test", ERC20_SALT),
130
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
131
- splitOperator: multisig(),
132
- stageConfigurations: stageConfigurations
133
- });
134
-
135
- return FeeProjectConfig({
136
- configuration: revnetConfiguration,
137
- terminalConfigurations: terminalConfigurations,
138
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
139
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
140
- })
141
- });
142
- }
143
-
144
- /// @notice Deploy a revnet with two stages for testing stage transitions.
145
- /// Stage 0: cashOutTaxRate=2000 (20%), starts now
146
- /// Stage 1: cashOutTaxRate=6000 (60%), starts after 30 days
147
- function _deployTwoStageRevnet() internal returns (uint256 revnetId) {
148
- uint8 decimals = 18;
149
- uint256 decimalMultiplier = 10 ** decimals;
150
-
151
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
152
- accountingContextsToAccept[0] = JBAccountingContext({
153
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
154
- });
155
- accountingContextsToAccept[1] =
156
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
157
-
158
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
159
- terminalConfigurations[0] =
160
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
161
-
162
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
163
- JBSplit[] memory splits = new JBSplit[](1);
164
- splits[0].beneficiary = payable(multisig());
165
- splits[0].percent = 10_000;
166
-
167
- // Stage 0: low tax rate (20%).
168
- stageConfigurations[0] = REVStageConfig({
169
- startsAtOrAfter: uint40(block.timestamp),
170
- autoIssuances: new REVAutoIssuance[](0),
171
- splitPercent: 2000,
172
- splits: splits,
173
- // forge-lint: disable-next-line(unsafe-typecast)
174
- initialIssuance: uint112(1000 * decimalMultiplier),
175
- issuanceCutFrequency: 0,
176
- issuanceCutPercent: 0,
177
- cashOutTaxRate: 2000, // 20%
178
- extraMetadata: 0
179
- });
180
-
181
- // Stage 1: high tax rate (60%), starts after 30 days.
182
- stageConfigurations[1] = REVStageConfig({
183
- startsAtOrAfter: uint40(block.timestamp + 30 days),
184
- autoIssuances: new REVAutoIssuance[](0),
185
- splitPercent: 2000,
186
- splits: splits,
187
- initialIssuance: 0, // inherit
188
- issuanceCutFrequency: 0,
189
- issuanceCutPercent: 0,
190
- cashOutTaxRate: 6000, // 60%
191
- extraMetadata: 0
192
- });
193
-
194
- REVLoanSource[] memory loanSources = new REVLoanSource[](1);
195
- loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
196
-
197
- REVConfig memory revnetConfiguration = REVConfig({
198
- // forge-lint: disable-next-line(named-struct-fields)
199
- description: REVDescription("TwoStage", "$TWO", "ipfs://test", "TWO_TOKEN"),
200
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
201
- splitOperator: multisig(),
202
- stageConfigurations: stageConfigurations
203
- });
204
-
205
- (revnetId,) = REV_DEPLOYER.deployFor({
206
- revnetId: 0,
207
- configuration: revnetConfiguration,
208
- terminalConfigurations: terminalConfigurations,
209
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
210
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("TWO"))
211
- }),
212
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
213
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
214
- });
215
- }
216
-
217
- /// @notice Deploy a single-stage revnet with loans enabled.
218
- function _deploySingleStageRevnet() internal returns (uint256 revnetId) {
219
- uint8 decimals = 18;
220
- uint256 decimalMultiplier = 10 ** decimals;
221
-
222
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
223
- accountingContextsToAccept[0] = JBAccountingContext({
224
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
225
- });
226
- accountingContextsToAccept[1] =
227
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
228
-
229
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
230
- terminalConfigurations[0] =
231
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
232
-
233
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
234
- JBSplit[] memory splits = new JBSplit[](1);
235
- splits[0].beneficiary = payable(multisig());
236
- splits[0].percent = 10_000;
237
-
238
- stageConfigurations[0] = REVStageConfig({
239
- startsAtOrAfter: uint40(block.timestamp),
240
- autoIssuances: new REVAutoIssuance[](0),
241
- splitPercent: 2000,
242
- splits: splits,
243
- // forge-lint: disable-next-line(unsafe-typecast)
244
- initialIssuance: uint112(1000 * decimalMultiplier),
245
- issuanceCutFrequency: 0,
246
- issuanceCutPercent: 0,
247
- cashOutTaxRate: 0, // 0% tax for simplicity
248
- extraMetadata: 0
249
- });
250
-
251
- REVLoanSource[] memory loanSources = new REVLoanSource[](1);
252
- loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
253
-
254
- REVConfig memory revnetConfiguration = REVConfig({
255
- // forge-lint: disable-next-line(named-struct-fields)
256
- description: REVDescription("Single", "$SGL", "ipfs://test", "SGL_TOKEN"),
257
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
258
- splitOperator: multisig(),
259
- stageConfigurations: stageConfigurations
260
- });
261
-
262
- (revnetId,) = REV_DEPLOYER.deployFor({
263
- revnetId: 0,
264
- configuration: revnetConfiguration,
265
- terminalConfigurations: terminalConfigurations,
266
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
267
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("SGL"))
268
- }),
269
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
270
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
271
- });
272
- }
273
-
274
- function setUp() public override {
275
- super.setUp();
276
-
277
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
278
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
279
- HOOK_STORE = new JB721TiersHookStore();
280
- EXAMPLE_HOOK = new JB721TiersHook(
281
- jbDirectory(),
282
- jbPermissions(),
283
- jbPrices(),
284
- jbRulesets(),
285
- HOOK_STORE,
286
- jbSplits(),
287
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
288
- multisig()
289
- );
290
- ADDRESS_REGISTRY = new JBAddressRegistry();
291
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
292
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
293
- MOCK_BUYBACK = new MockBuybackDataHook();
294
- TOKEN = new MockERC20("1/2 ETH", "1/2");
295
-
296
- MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
297
- vm.prank(multisig());
298
- jbPrices()
299
- .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
300
-
301
- LOANS_CONTRACT = new REVLoans({
302
- controller: jbController(),
303
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
304
- revId: FEE_PROJECT_ID,
305
- owner: address(this),
306
- permit2: permit2(),
307
- trustedForwarder: TRUSTED_FORWARDER
308
- });
309
-
310
- REV_OWNER = new REVOwner(
311
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
312
- jbDirectory(),
313
- FEE_PROJECT_ID,
314
- SUCKER_REGISTRY,
315
- address(LOANS_CONTRACT),
316
- address(0)
317
- );
318
-
319
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
320
- jbController(),
321
- SUCKER_REGISTRY,
322
- FEE_PROJECT_ID,
323
- HOOK_DEPLOYER,
324
- PUBLISHER,
325
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
326
- address(LOANS_CONTRACT),
327
- TRUSTED_FORWARDER,
328
- address(REV_OWNER)
329
- );
330
-
331
- REV_OWNER.setDeployer(REV_DEPLOYER);
332
-
333
- // Deploy fee project.
334
- vm.prank(multisig());
335
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
336
-
337
- FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
338
- vm.prank(multisig());
339
- REV_DEPLOYER.deployFor({
340
- revnetId: FEE_PROJECT_ID,
341
- configuration: feeProjectConfig.configuration,
342
- terminalConfigurations: feeProjectConfig.terminalConfigurations,
343
- suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
344
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
345
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
346
- });
347
-
348
- vm.deal(USER, 1000 ether);
349
- }
350
-
351
- /// @notice Stage transition reduces borrowable amount due to higher cashOutTaxRate.
352
- function test_stageTransition_reducesLoanHealth() public {
353
- uint256 revnetId = _deployTwoStageRevnet();
354
-
355
- // Pay ETH into revnet to create surplus and get tokens.
356
- vm.prank(USER);
357
- uint256 tokens =
358
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
359
- assertGt(tokens, 0, "Should have received tokens");
360
-
361
- // Check borrowable amount in stage 0 (20% tax).
362
- uint256 borrowableStage0 =
363
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
364
- assertGt(borrowableStage0, 0, "Should have borrowable amount in stage 0");
365
-
366
- // Warp to stage 1 (60% tax).
367
- vm.warp(block.timestamp + 30 days + 1);
368
-
369
- // Check borrowable amount in stage 1 — should be lower due to higher tax.
370
- uint256 borrowableStage1 =
371
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
372
-
373
- assertLt(borrowableStage1, borrowableStage0, "Borrowable amount should decrease when cashOutTaxRate increases");
374
- }
375
-
376
- /// @notice After full repayment, loan data is deleted (storage cleared).
377
- function test_loanDataDeletedAfterRepay() public {
378
- uint256 revnetId = _deploySingleStageRevnet();
379
-
380
- // Pay ETH into revnet to get tokens.
381
- vm.prank(USER);
382
- uint256 tokens =
383
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
384
-
385
- uint256 loanable =
386
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
387
- // Skip if nothing borrowable.
388
- vm.assume(loanable > 0);
389
-
390
- // Mock permission for BURN (permission ID 10).
391
- mockExpect(
392
- address(jbPermissions()),
393
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
394
- abi.encode(true)
395
- );
396
-
397
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
398
-
399
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
400
-
401
- vm.prank(USER);
402
- (uint256 loanId,) =
403
- LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid, USER);
404
-
405
- REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
406
- assertGt(loanBefore.amount, 0, "Loan should have an amount");
407
- assertGt(loanBefore.collateral, 0, "Loan should have collateral");
408
-
409
- // Fully repay the loan — return all collateral.
410
- JBSingleAllowance memory allowance;
411
-
412
- vm.prank(USER);
413
- LOANS_CONTRACT.repayLoan{value: loanBefore.amount * 2}(
414
- loanId, loanBefore.amount * 2, loanBefore.collateral, payable(USER), allowance
415
- );
416
-
417
- // After repayment, loan storage should be cleared.
418
- REVLoan memory loanAfter = LOANS_CONTRACT.loanOf(loanId);
419
- assertEq(loanAfter.amount, 0, "Loan amount should be 0 after repay");
420
- assertEq(loanAfter.collateral, 0, "Loan collateral should be 0 after repay");
421
- assertEq(loanAfter.createdAt, 0, "Loan createdAt should be 0 after repay");
422
- }
423
-
424
- /// @notice After liquidation, loan data is deleted (storage cleared).
425
- function test_loanDataDeletedAfterLiquidation() public {
426
- uint256 revnetId = _deploySingleStageRevnet();
427
-
428
- // Pay ETH into revnet to get tokens.
429
- vm.prank(USER);
430
- uint256 tokens =
431
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
432
-
433
- uint256 loanable =
434
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
435
- // Skip if nothing borrowable.
436
- vm.assume(loanable > 0);
437
-
438
- // Mock permission for BURN (permission ID 10).
439
- mockExpect(
440
- address(jbPermissions()),
441
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
442
- abi.encode(true)
443
- );
444
-
445
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
446
-
447
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
448
-
449
- vm.prank(USER);
450
- (uint256 loanId,) =
451
- LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid, USER);
452
-
453
- REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
454
- assertGt(loanBefore.amount, 0, "Loan should exist before liquidation");
455
-
456
- // Warp past LOAN_LIQUIDATION_DURATION (3650 days).
457
- vm.warp(block.timestamp + 3650 days + 1);
458
-
459
- // Get the loan number from the ID (loanId = revnetId * 1_000_000_000_000 + loanNumber).
460
- // For the first loan, loanNumber is 1.
461
- LOANS_CONTRACT.liquidateExpiredLoansFrom(revnetId, 1, 1);
462
-
463
- // After liquidation, loan storage should be cleared.
464
- REVLoan memory loanAfter = LOANS_CONTRACT.loanOf(loanId);
465
- assertEq(loanAfter.amount, 0, "Loan amount should be 0 after liquidation");
466
- assertEq(loanAfter.collateral, 0, "Loan collateral should be 0 after liquidation");
467
- assertEq(loanAfter.createdAt, 0, "Loan createdAt should be 0 after liquidation");
468
- }
469
-
470
- /// @notice Partial repay (return some but not all collateral) clears old loan storage
471
- /// and creates a replacement loan. Exercises the `else` branch in `_repayLoan`.
472
- function test_partialRepay_clearsOldLoanStorage() public {
473
- uint256 revnetId = _deploySingleStageRevnet();
474
-
475
- // Pay ETH into revnet to get tokens.
476
- vm.prank(USER);
477
- uint256 tokens =
478
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
479
-
480
- uint256 loanable =
481
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
482
- vm.assume(loanable > 0);
483
-
484
- // Mock permission for BURN (permission ID 10).
485
- mockExpect(
486
- address(jbPermissions()),
487
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
488
- abi.encode(true)
489
- );
490
-
491
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
492
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
493
-
494
- vm.prank(USER);
495
- (uint256 loanId,) =
496
- LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid, USER);
497
-
498
- REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
499
- assertGt(loanBefore.collateral, 1, "Need >1 collateral for partial return");
500
-
501
- // Partial repay: return HALF the collateral (triggers else branch in _repayLoan).
502
- uint256 halfCollateral = loanBefore.collateral / 2;
503
- JBSingleAllowance memory allowance;
504
-
505
- // Send loan.amount as maxRepay — more than enough for partial repay.
506
- vm.prank(USER);
507
- (uint256 newLoanId, REVLoan memory newLoan) = LOANS_CONTRACT.repayLoan{value: loanBefore.amount}(
508
- loanId, loanBefore.amount, halfCollateral, payable(USER), allowance
509
- );
510
-
511
- // Old loan storage should be cleared (the delete we're testing).
512
- REVLoan memory oldLoan = LOANS_CONTRACT.loanOf(loanId);
513
- assertEq(oldLoan.amount, 0, "Old loan amount should be 0 after partial repay");
514
- assertEq(oldLoan.collateral, 0, "Old loan collateral should be 0 after partial repay");
515
- assertEq(oldLoan.createdAt, 0, "Old loan createdAt should be 0 after partial repay");
516
-
517
- // New replacement loan should exist with remaining values.
518
- assertGt(newLoan.amount, 0, "New loan should have amount");
519
- assertGt(newLoan.collateral, 0, "New loan should have collateral");
520
- assertLt(newLoan.amount, loanBefore.amount, "New loan amount should be less than original");
521
-
522
- // Verify via storage read too (not just return value).
523
- REVLoan memory newLoanFromStorage = LOANS_CONTRACT.loanOf(newLoanId);
524
- assertEq(newLoanFromStorage.amount, newLoan.amount, "Storage should match return value");
525
- }
526
-
527
- /// @notice Repaying with excess ETH correctly refunds the difference.
528
- /// This tests the sourceToken caching fix — before the fix, `loan.source.token` was read
529
- /// after `_repayLoan` deleted the storage, yielding `address(0)` and reverting.
530
- function test_repayLoan_refundsExcessWithCorrectToken() public {
531
- uint256 revnetId = _deploySingleStageRevnet();
532
-
533
- // Pay ETH into revnet to get tokens.
534
- vm.prank(USER);
535
- uint256 tokens =
536
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
537
-
538
- uint256 loanable =
539
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
540
- vm.assume(loanable > 0);
541
-
542
- // Mock permission for BURN (permission ID 10).
543
- mockExpect(
544
- address(jbPermissions()),
545
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
546
- abi.encode(true)
547
- );
548
-
549
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
550
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
551
-
552
- vm.prank(USER);
553
- (uint256 loanId,) =
554
- LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid, USER);
555
-
556
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
557
-
558
- // Partial repay returning half collateral. Send 2x the loan amount — guaranteed excess.
559
- uint256 halfCollateral = loan.collateral / 2;
560
- uint256 excessivePayment = loan.amount * 2;
561
-
562
- uint256 balBefore = USER.balance;
563
- JBSingleAllowance memory allowance;
564
-
565
- vm.prank(USER);
566
- LOANS_CONTRACT.repayLoan{value: excessivePayment}(
567
- loanId, excessivePayment, halfCollateral, payable(USER), allowance
568
- );
569
-
570
- uint256 balAfter = USER.balance;
571
-
572
- // User sent `excessivePayment` but should get excess back.
573
- // Net cost = repayBorrowAmount (includes fee). Must be less than loan.amount.
574
- uint256 netCost = balBefore - balAfter;
575
- assertLt(netCost, excessivePayment, "User should have been refunded excess ETH");
576
- assertGt(netCost, 0, "User should have paid something");
577
- }
578
-
579
- /// @notice Reallocation clears old loan storage after creating replacement.
580
- /// Exercises the `delete _loanOf[loanId]` in `_reallocateCollateralFromLoan`.
581
- function test_reallocateCollateral_clearsOldLoanStorage() public {
582
- uint256 revnetId = _deploySingleStageRevnet();
583
-
584
- // Pay ETH into revnet to get tokens.
585
- vm.prank(USER);
586
- uint256 tokens =
587
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
588
-
589
- uint256 loanable =
590
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
591
- vm.assume(loanable > 0);
592
-
593
- // Mock permission for BURN (permission ID 10).
594
- mockExpect(
595
- address(jbPermissions()),
596
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
597
- abi.encode(true)
598
- );
599
-
600
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
601
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
602
-
603
- // Borrow the full max against all tokens.
604
- vm.prank(USER);
605
- (uint256 loanId,) =
606
- LOANS_CONTRACT.borrowFrom(revnetId, source, loanable, tokens, payable(USER), minPrepaid, USER);
607
-
608
- REVLoan memory loanBefore = LOANS_CONTRACT.loanOf(loanId);
609
- assertGt(loanBefore.collateral, 0, "Loan should have collateral");
610
-
611
- // Increase surplus WITHOUT minting new tokens — makes existing collateral worth more.
612
- // This allows reallocating collateral since borrowable(reduced collateral) > loan.amount.
613
- // forge-lint: disable-next-line(mixed-case-variable)
614
- address DONOR = makeAddr("donor");
615
- vm.deal(DONOR, 100 ether);
616
- vm.prank(DONOR);
617
- jbMultiTerminal().addToBalanceOf{value: 50 ether}(revnetId, JBConstants.NATIVE_TOKEN, 50 ether, false, "", "");
618
-
619
- // Transfer a small amount of collateral (10%) to a new loan.
620
- uint256 collateralToTransfer = loanBefore.collateral / 10;
621
- assertGt(collateralToTransfer, 0, "Must transfer some collateral");
622
-
623
- vm.prank(USER);
624
- (uint256 reallocatedLoanId, uint256 newLoanId, REVLoan memory reallocatedLoan,) = LOANS_CONTRACT.reallocateCollateralFromLoan(
625
- loanId, collateralToTransfer, source, 0, 0, payable(USER), minPrepaid
626
- );
627
-
628
- // Old loan storage should be cleared.
629
- REVLoan memory oldLoan = LOANS_CONTRACT.loanOf(loanId);
630
- assertEq(oldLoan.amount, 0, "Old loan amount should be 0 after reallocation");
631
- assertEq(oldLoan.collateral, 0, "Old loan collateral should be 0 after reallocation");
632
- assertEq(oldLoan.createdAt, 0, "Old loan createdAt should be 0 after reallocation");
633
-
634
- // Reallocated loan should have reduced collateral but same amount.
635
- assertEq(reallocatedLoan.amount, loanBefore.amount, "Reallocated loan should keep original amount");
636
- assertLt(reallocatedLoan.collateral, loanBefore.collateral, "Reallocated loan should have less collateral");
637
-
638
- // New loan should exist.
639
- assertTrue(newLoanId != loanId, "New loan should have different ID");
640
- assertTrue(newLoanId != reallocatedLoanId, "New loan should differ from reallocated loan");
641
- }
642
-
643
- /// @notice Borrowing with a collateral count so small that the bonding curve rounds the borrow amount to zero
644
- /// should revert with `REVLoans_ZeroBorrowAmount`.
645
- function test_borrowFrom_revertsOnZeroBorrowAmount() public {
646
- uint256 revnetId = _deploySingleStageRevnet();
647
-
648
- // Pay ETH into revnet to create surplus and get tokens.
649
- vm.prank(USER);
650
- uint256 tokens =
651
- jbMultiTerminal().pay{value: 10 ether}(revnetId, JBConstants.NATIVE_TOKEN, 10 ether, USER, 0, "", "");
652
- assertGt(tokens, 0, "Should have received tokens");
653
-
654
- // Confirm that 1 wei of collateral produces a zero borrowable amount.
655
- // With surplus ~10e18 and totalSupply ~10_000e18, mulDiv(10e18, 1, 10_000e18) rounds to 0.
656
- uint256 borrowable =
657
- LOANS_CONTRACT.borrowableAmountFrom(revnetId, 1, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
658
- assertEq(borrowable, 0, "Borrowable amount for 1 wei of collateral should be 0");
659
-
660
- // Mock the BURN permission (permission ID 11) for the loans contract.
661
- // Use vm.mockCall only (not mockExpect which also adds vm.expectCall) because
662
- // borrowFrom reverts with REVLoans_ZeroBorrowAmount before the permission check is reached.
663
- vm.mockCall(
664
- address(jbPermissions()),
665
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetId, 11, true, true)),
666
- abi.encode(true)
667
- );
668
-
669
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
670
- uint256 minPrepaid = LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT();
671
-
672
- // Attempt to borrow with 1 wei of collateral -- bonding curve returns 0, should revert.
673
- vm.prank(USER);
674
- vm.expectRevert(REVLoans.REVLoans_ZeroBorrowAmount.selector);
675
- LOANS_CONTRACT.borrowFrom(revnetId, source, 0, 1, payable(USER), minPrepaid, USER);
676
- }
677
- }