@rev-net/core-v6 0.0.36 → 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 (101) 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 +134 -90
  11. package/src/REVOwner.sol +124 -17
  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 -97
  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 -368
  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/CodexCrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/CodexPhantomSurplusTerminal.t.sol +0 -367
  71. package/test/audit/CodexREVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -142
  72. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  73. package/test/audit/NemesisOperatorDelegation.t.sol +0 -356
  74. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  75. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  76. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  77. package/test/fork/ForkTestBase.sol +0 -727
  78. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  79. package/test/fork/TestCashOutFork.t.sol +0 -253
  80. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  81. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  82. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  83. package/test/fork/TestLoanERC20Fork.t.sol +0 -465
  84. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  85. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  86. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  87. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  88. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  89. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  90. package/test/helpers/MaliciousContracts.sol +0 -247
  91. package/test/helpers/REVEmpty721Config.sol +0 -45
  92. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  93. package/test/mock/MockBuybackDataHook.sol +0 -112
  94. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  95. package/test/mock/MockSuckerRegistry.sol +0 -17
  96. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  97. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  98. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  99. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  100. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  101. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,816 +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 {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
29
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
30
- import {REVLoans} from "../src/REVLoans.sol";
31
- import {REVLoan} from "../src/structs/REVLoan.sol";
32
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
33
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
34
- import {REVDescription} from "../src/structs/REVDescription.sol";
35
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
36
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
37
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
38
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
39
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
40
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
41
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
42
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
43
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
44
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
45
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
46
- import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.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 {REVOwner} from "../src/REVOwner.sol";
50
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
51
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
52
-
53
- /// @notice A malicious terminal that re-enters REVLoans during fee payment in _adjust().
54
- /// @dev Reentrancy during pay() callback in _adjust.
55
- contract ReentrantTerminal is ERC165, IJBPayoutTerminal {
56
- IREVLoans public loans;
57
- uint256 public revnetId;
58
- bool public shouldReenter;
59
- bool public reentered;
60
-
61
- // Parameters for re-entrant borrowFrom call
62
- uint256 public reenterCollateral;
63
- REVLoanSource public reenterSource;
64
-
65
- function setReentrancy(
66
- IREVLoans _loans,
67
- uint256 _revnetId,
68
- uint256 _collateral,
69
- REVLoanSource memory _source
70
- )
71
- external
72
- {
73
- loans = _loans;
74
- revnetId = _revnetId;
75
- reenterCollateral = _collateral;
76
- reenterSource = _source;
77
- shouldReenter = true;
78
- }
79
-
80
- function pay(
81
- uint256,
82
- address,
83
- uint256,
84
- address,
85
- uint256,
86
- string calldata,
87
- bytes calldata
88
- )
89
- external
90
- payable
91
- override
92
- returns (uint256)
93
- {
94
- // On fee payment during _adjust, try to re-enter borrowFrom
95
- if (shouldReenter && !reentered) {
96
- reentered = true;
97
- // Attempt reentrancy: borrow again during fee payment
98
- try loans.borrowFrom(
99
- revnetId,
100
- reenterSource,
101
- 0, // minBorrowAmount
102
- reenterCollateral,
103
- payable(address(this)),
104
- 25, // MIN_PREPAID_FEE_PERCENT
105
- address(this)
106
- ) {}
107
- catch {
108
- // Expected to revert if reentrancy guard exists
109
- }
110
- }
111
- return 0;
112
- }
113
-
114
- function accountingContextForTokenOf(uint256, address) external pure override returns (JBAccountingContext memory) {
115
- return JBAccountingContext({
116
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
117
- });
118
- }
119
-
120
- function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {
121
- return new JBAccountingContext[](0);
122
- }
123
-
124
- function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
125
-
126
- function addToBalanceOf(
127
- uint256,
128
- address,
129
- uint256,
130
- bool,
131
- string calldata,
132
- bytes calldata
133
- )
134
- external
135
- payable
136
- override
137
- {}
138
-
139
- function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
140
- return 0;
141
- }
142
-
143
- function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
144
- return 0;
145
- }
146
-
147
- function sendPayoutsOf(uint256, address, uint256, uint256, uint256) external pure override returns (uint256) {
148
- return 0;
149
- }
150
-
151
- function useAllowanceOf(
152
- uint256,
153
- address,
154
- uint256,
155
- uint256,
156
- uint256,
157
- address payable,
158
- address payable,
159
- string calldata
160
- )
161
- external
162
- pure
163
- override
164
- returns (uint256)
165
- {
166
- return 0;
167
- }
168
-
169
- function previewPayFor(
170
- uint256,
171
- address,
172
- uint256,
173
- address,
174
- bytes calldata
175
- )
176
- external
177
- pure
178
- override
179
- returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
180
- {
181
- JBRuleset memory ruleset;
182
- return (ruleset, 0, 0, new JBPayHookSpecification[](0));
183
- }
184
-
185
- function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
186
- return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
187
- || super.supportsInterface(interfaceId);
188
- }
189
-
190
- receive() external payable {}
191
- }
192
-
193
- struct AttackProjectConfig {
194
- REVConfig configuration;
195
- JBTerminalConfig[] terminalConfigurations;
196
- REVSuckerDeploymentConfig suckerDeploymentConfiguration;
197
- }
198
-
199
- /// @title REVLoansAttacks
200
- /// @notice Attack tests for REVLoans covering uint112 truncation, reentrancy,
201
- /// collateral race conditions, liquidation edge cases, and fuzz testing.
202
- contract REVLoansAttacks is TestBaseWorkflow {
203
- // forge-lint: disable-next-line(mixed-case-variable)
204
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
205
- // forge-lint: disable-next-line(mixed-case-variable)
206
- bytes32 ERC20_SALT = "REV_TOKEN";
207
-
208
- // forge-lint: disable-next-line(mixed-case-variable)
209
- REVDeployer REV_DEPLOYER;
210
- // forge-lint: disable-next-line(mixed-case-variable)
211
- REVOwner REV_OWNER;
212
- // forge-lint: disable-next-line(mixed-case-variable)
213
- JB721TiersHook EXAMPLE_HOOK;
214
- // forge-lint: disable-next-line(mixed-case-variable)
215
- IJB721TiersHookDeployer HOOK_DEPLOYER;
216
- // forge-lint: disable-next-line(mixed-case-variable)
217
- IJB721TiersHookStore HOOK_STORE;
218
- // forge-lint: disable-next-line(mixed-case-variable)
219
- IJBAddressRegistry ADDRESS_REGISTRY;
220
- // forge-lint: disable-next-line(mixed-case-variable)
221
- IREVLoans LOANS_CONTRACT;
222
- // forge-lint: disable-next-line(mixed-case-variable)
223
- MockERC20 TOKEN;
224
- // forge-lint: disable-next-line(mixed-case-variable)
225
- IJBSuckerRegistry SUCKER_REGISTRY;
226
- // forge-lint: disable-next-line(mixed-case-variable)
227
- CTPublisher PUBLISHER;
228
- // forge-lint: disable-next-line(mixed-case-variable)
229
- MockBuybackDataHook MOCK_BUYBACK;
230
-
231
- // forge-lint: disable-next-line(mixed-case-variable)
232
- uint256 FEE_PROJECT_ID;
233
- // forge-lint: disable-next-line(mixed-case-variable)
234
- uint256 REVNET_ID;
235
-
236
- // forge-lint: disable-next-line(mixed-case-variable)
237
- address USER = makeAddr("user");
238
- // forge-lint: disable-next-line(mixed-case-variable)
239
- address ATTACKER = makeAddr("attacker");
240
-
241
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
242
-
243
- function _getFeeProjectConfig() internal view returns (AttackProjectConfig memory) {
244
- string memory name = "Revnet";
245
- string memory symbol = "$REV";
246
- string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx";
247
- uint8 decimals = 18;
248
- uint256 decimalMultiplier = 10 ** decimals;
249
-
250
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
251
- accountingContextsToAccept[0] = JBAccountingContext({
252
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
253
- });
254
- accountingContextsToAccept[1] =
255
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
256
-
257
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
258
- terminalConfigurations[0] =
259
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
260
-
261
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
262
- JBSplit[] memory splits = new JBSplit[](1);
263
- splits[0].beneficiary = payable(multisig());
264
- splits[0].percent = 10_000;
265
-
266
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
267
- issuanceConfs[0] = REVAutoIssuance({
268
- // forge-lint: disable-next-line(unsafe-typecast)
269
- chainId: uint32(block.chainid),
270
- // forge-lint: disable-next-line(unsafe-typecast)
271
- count: uint104(70_000 * decimalMultiplier),
272
- beneficiary: multisig()
273
- });
274
-
275
- stageConfigurations[0] = REVStageConfig({
276
- startsAtOrAfter: uint40(block.timestamp),
277
- autoIssuances: issuanceConfs,
278
- splitPercent: 2000,
279
- splits: splits,
280
- // forge-lint: disable-next-line(unsafe-typecast)
281
- initialIssuance: uint112(1000 * decimalMultiplier),
282
- issuanceCutFrequency: 90 days,
283
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
284
- cashOutTaxRate: 6000,
285
- extraMetadata: 0
286
- });
287
-
288
- REVConfig memory revnetConfiguration = REVConfig({
289
- // forge-lint: disable-next-line(named-struct-fields)
290
- description: REVDescription(name, symbol, projectUri, ERC20_SALT),
291
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
292
- splitOperator: multisig(),
293
- stageConfigurations: stageConfigurations
294
- });
295
-
296
- return AttackProjectConfig({
297
- configuration: revnetConfiguration,
298
- terminalConfigurations: terminalConfigurations,
299
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
300
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
301
- })
302
- });
303
- }
304
-
305
- function _getRevnetConfig() internal view returns (AttackProjectConfig memory) {
306
- string memory name = "NANA";
307
- string memory symbol = "$NANA";
308
- string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx";
309
- uint8 decimals = 18;
310
- uint256 decimalMultiplier = 10 ** decimals;
311
-
312
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
313
- accountingContextsToAccept[0] = JBAccountingContext({
314
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
315
- });
316
- accountingContextsToAccept[1] =
317
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
318
-
319
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
320
- terminalConfigurations[0] =
321
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
322
-
323
- JBSplit[] memory splits = new JBSplit[](1);
324
- splits[0].beneficiary = payable(multisig());
325
- splits[0].percent = 10_000;
326
-
327
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
328
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
329
- issuanceConfs[0] = REVAutoIssuance({
330
- // forge-lint: disable-next-line(unsafe-typecast)
331
- chainId: uint32(block.chainid),
332
- // forge-lint: disable-next-line(unsafe-typecast)
333
- count: uint104(70_000 * decimalMultiplier),
334
- beneficiary: multisig()
335
- });
336
-
337
- stageConfigurations[0] = REVStageConfig({
338
- startsAtOrAfter: uint40(block.timestamp),
339
- autoIssuances: issuanceConfs,
340
- splitPercent: 2000,
341
- splits: splits,
342
- // forge-lint: disable-next-line(unsafe-typecast)
343
- initialIssuance: uint112(1000 * decimalMultiplier),
344
- issuanceCutFrequency: 90 days,
345
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
346
- cashOutTaxRate: 6000,
347
- extraMetadata: 0
348
- });
349
-
350
- REVConfig memory revnetConfiguration = REVConfig({
351
- // forge-lint: disable-next-line(named-struct-fields)
352
- description: REVDescription(name, symbol, projectUri, "NANA_TOKEN"),
353
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
354
- splitOperator: multisig(),
355
- stageConfigurations: stageConfigurations
356
- });
357
-
358
- return AttackProjectConfig({
359
- configuration: revnetConfiguration,
360
- terminalConfigurations: terminalConfigurations,
361
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
362
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
363
- })
364
- });
365
- }
366
-
367
- function setUp() public override {
368
- super.setUp();
369
-
370
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
371
-
372
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
373
- HOOK_STORE = new JB721TiersHookStore();
374
- EXAMPLE_HOOK = new JB721TiersHook(
375
- jbDirectory(),
376
- jbPermissions(),
377
- jbPrices(),
378
- jbRulesets(),
379
- HOOK_STORE,
380
- jbSplits(),
381
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
382
- multisig()
383
- );
384
- ADDRESS_REGISTRY = new JBAddressRegistry();
385
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
386
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
387
- MOCK_BUYBACK = new MockBuybackDataHook();
388
- TOKEN = new MockERC20("1/2 ETH", "1/2");
389
-
390
- MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
391
- vm.prank(multisig());
392
- jbPrices()
393
- .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
394
-
395
- LOANS_CONTRACT = new REVLoans({
396
- controller: jbController(),
397
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
398
- revId: FEE_PROJECT_ID,
399
- owner: address(this),
400
- permit2: permit2(),
401
- trustedForwarder: TRUSTED_FORWARDER
402
- });
403
-
404
- REV_OWNER = new REVOwner(
405
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
406
- jbDirectory(),
407
- FEE_PROJECT_ID,
408
- SUCKER_REGISTRY,
409
- address(LOANS_CONTRACT),
410
- address(0)
411
- );
412
-
413
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
414
- jbController(),
415
- SUCKER_REGISTRY,
416
- FEE_PROJECT_ID,
417
- HOOK_DEPLOYER,
418
- PUBLISHER,
419
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
420
- address(LOANS_CONTRACT),
421
- TRUSTED_FORWARDER,
422
- address(REV_OWNER)
423
- );
424
-
425
- REV_OWNER.setDeployer(REV_DEPLOYER);
426
-
427
- // Deploy fee project
428
- vm.prank(address(multisig()));
429
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
430
-
431
- AttackProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
432
- vm.prank(address(multisig()));
433
- REV_DEPLOYER.deployFor({
434
- revnetId: FEE_PROJECT_ID,
435
- configuration: feeProjectConfig.configuration,
436
- terminalConfigurations: feeProjectConfig.terminalConfigurations,
437
- suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
438
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
439
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
440
- });
441
-
442
- // Deploy second revnet with loans enabled
443
- AttackProjectConfig memory revnetConfig = _getRevnetConfig();
444
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
445
- revnetId: 0,
446
- configuration: revnetConfig.configuration,
447
- terminalConfigurations: revnetConfig.terminalConfigurations,
448
- suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration,
449
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
450
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
451
- });
452
-
453
- vm.deal(USER, 1000e18);
454
- vm.deal(ATTACKER, 1000e18);
455
- }
456
-
457
- // =========================================================================
458
- // Helper: create a loan and return the loanId and token count
459
- // =========================================================================
460
- function _setupLoan(
461
- address user,
462
- uint256 ethAmount,
463
- uint256 prepaidFee
464
- )
465
- internal
466
- returns (uint256 loanId, uint256 tokenCount, uint256 borrowAmount)
467
- {
468
- // Pay into revnet to get tokens
469
- vm.prank(user);
470
- tokenCount =
471
- jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
472
-
473
- // Check borrowable amount
474
- borrowAmount =
475
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
476
-
477
- if (borrowAmount == 0) return (0, tokenCount, 0);
478
-
479
- // Mock permission for loans contract to burn tokens
480
- mockExpect(
481
- address(jbPermissions()),
482
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
483
- abi.encode(true)
484
- );
485
-
486
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
487
-
488
- vm.prank(user);
489
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
490
- }
491
-
492
- // =========================================================================
493
- // uint112 truncation — loan amount silently wraps
494
- // =========================================================================
495
- /// @notice Verify that borrowing an amount > uint112.max is properly handled.
496
- /// @dev The _adjust function casts newBorrowAmount to uint112 without overflow checks.
497
- /// If borrowAmount exceeds uint112.max, it silently truncates. This test verifies the behavior.
498
- function test_uint112Truncation_loanAmountSilentlyTruncates() public {
499
- // uint112.max = 5192296858534827628530496329220095
500
- // We need a revnet with enough surplus that collateral yields a borrowAmount > uint112.max.
501
- // In practice, this requires enormous token supplies. We test the boundary:
502
- // pay a very large amount to build up surplus, then borrow against it.
503
-
504
- uint256 hugeAmount = 100e18;
505
- vm.prank(USER);
506
- uint256 tokens =
507
- jbMultiTerminal().pay{value: hugeAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, hugeAmount, USER, 0, "", "");
508
-
509
- // Check borrowable amount
510
- uint256 borrowable =
511
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
512
-
513
- // The borrowable amount with 18 decimals and reasonable surplus should be < uint112.max.
514
- // Verify it does not overflow for normal amounts.
515
- assertLt(borrowable, type(uint112).max, "Borrowable amount should be within uint112 range for normal amounts");
516
-
517
- // Now verify that the uint112 cast would truncate if somehow a larger value were used.
518
- // We can directly verify the truncation behavior:
519
- uint256 overflowValue = uint256(type(uint112).max) + 1;
520
- // forge-lint: disable-next-line(unsafe-typecast)
521
- uint112 truncated = uint112(overflowValue);
522
- assertEq(truncated, 0, "uint112 truncation of max+1 should wrap to 0");
523
-
524
- // And for a value just slightly above max:
525
- uint256 slightlyOver = uint256(type(uint112).max) + 1000;
526
- // forge-lint: disable-next-line(unsafe-typecast)
527
- uint112 truncated2 = uint112(slightlyOver);
528
- assertEq(truncated2, 999, "uint112 truncation should wrap around");
529
- }
530
-
531
- // =========================================================================
532
- // collateral > uint112.max wraps
533
- // =========================================================================
534
- /// @notice Verify that collateral > uint112.max would be truncated in the loan struct.
535
- /// @dev loan.collateral = uint112(newCollateralCount) truncates silently.
536
- function test_uint112Truncation_collateralTruncates() public {
537
- // Verify the truncation math
538
- uint256 maxCollateral = type(uint112).max;
539
- uint256 overflowCollateral = maxCollateral + 1;
540
-
541
- // Direct cast would truncate
542
- // forge-lint: disable-next-line(unsafe-typecast)
543
- uint112 truncated = uint112(overflowCollateral);
544
- assertEq(truncated, 0, "Collateral overflow should truncate to 0");
545
-
546
- // In practice, the user needs to have > uint112.max tokens.
547
- // With 18 decimal tokens, uint112.max ≈ 5.19e15 tokens (5.19 quadrillion).
548
- // This is extremely unlikely but the code should still protect against it.
549
- // Verify that paying a reasonable amount stays within bounds:
550
- uint256 payAmount = 50e18;
551
- vm.prank(USER);
552
- uint256 tokens =
553
- jbMultiTerminal().pay{value: payAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, USER, 0, "", "");
554
-
555
- // Token count with 18 decimals should be well within uint112 range
556
- assertLt(tokens, type(uint112).max, "Normal token count should not overflow uint112");
557
- }
558
-
559
- // =========================================================================
560
- // reentrancy — _adjust calls terminal.pay() which could re-enter
561
- // =========================================================================
562
- /// @notice Verify that reentrancy during _adjust's fee payment is handled.
563
- /// @dev The _adjust function calls loan.source.terminal.pay() to pay fees.
564
- /// A malicious terminal could use this callback to re-enter borrowFrom().
565
- /// Since Solidity 0.8.23 doesn't have native reentrancy guards on REVLoans,
566
- /// the state (loan.amount, loan.collateral) is written AFTER the external call.
567
- function test_reentrancy_adjustPayReenter() public {
568
- // This test demonstrates the reentrancy window:
569
- // 1. borrowFrom → _adjust → terminal.pay() (external call at line 910)
570
- // 2. During terminal.pay(), state updates at lines 922-923 haven't happened yet
571
- // 3. The malicious terminal tries to call borrowFrom again
572
-
573
- // First, create a legitimate loan to ensure the system works
574
- uint256 payAmount = 10e18;
575
- vm.prank(USER);
576
- uint256 tokens =
577
- jbMultiTerminal().pay{value: payAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, USER, 0, "", "");
578
-
579
- uint256 borrowable =
580
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
581
- assertTrue(borrowable > 0, "Should have borrowable amount");
582
-
583
- // The reentrancy vulnerability exists because _adjust calls terminal.pay()
584
- // at line 910 BEFORE writing loan.amount and loan.collateral at lines 922-923.
585
- // A malicious terminal receiving the fee payment could call borrowFrom() again
586
- // before the first loan's state is finalized.
587
-
588
- // Verify the ordering: external call at line 910, state write at lines 922-923
589
- // This is a checks-effects-interactions violation.
590
- // The loan amount and collateral are read from storage during _borrowAmountFrom,
591
- // so a re-entrant call would see stale values.
592
- assertTrue(true, "reentrancy window confirmed between terminal.pay() and state writes");
593
- }
594
-
595
- // =========================================================================
596
- // re-enter repayLoan during fee payment
597
- // =========================================================================
598
- /// @notice Verify that reentering repayLoan during _adjust's fee payment is handled.
599
- /// @dev Malicious terminal calls repayLoan() during fee payment.
600
- function test_reentrancy_adjustRepayReenter() public {
601
- // Similar to above, but the re-entrant call targets repayLoan instead of borrowFrom.
602
- // The concern is that during _adjust → terminal.pay(), a call to repayLoan
603
- // could modify loan state before the original _adjust completes.
604
-
605
- // Setup: create a loan first
606
- uint256 payAmount = 10e18;
607
- (uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, payAmount, 25);
608
- vm.assume(borrowAmount > 0);
609
-
610
- // The loan exists. The reentrancy risk during repayLoan:
611
- // repayLoan → _repayLoan → _adjust → terminal.pay() [external call]
612
- // → re-enter repayLoan on same loanId
613
- // → but the original _burn(loanId) at line 1013 happens BEFORE _adjust
614
- // → so the re-entrant call would fail on _ownerOf check
615
- // This means repayLoan has partial protection via the burn-then-adjust pattern.
616
-
617
- // Verify the loan exists
618
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
619
- assertTrue(loan.amount > 0, "Loan should exist");
620
- assertTrue(loan.collateral > 0, "Loan should have collateral");
621
- }
622
-
623
- // =========================================================================
624
- // Collateral race: burn tokens then another user cashes out at elevated rate
625
- // =========================================================================
626
- /// @notice Between collateral burn and useAllowance, another user cashes out at elevated per-token surplus.
627
- /// @dev When tokens are burned as collateral (reducing supply), the per-token surplus
628
- /// increases for remaining holders before the loan funds are disbursed.
629
- function test_collateralRace_burnThenAllowancePull() public {
630
- // User A and User B both pay into the revnet
631
- address userA = makeAddr("userA");
632
- address userB = makeAddr("userB");
633
- vm.deal(userA, 100e18);
634
- vm.deal(userB, 100e18);
635
-
636
- // Both users pay 10 ETH
637
- vm.prank(userA);
638
- uint256 tokensA =
639
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, userA, 0, "", "");
640
-
641
- vm.prank(userB);
642
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, userB, 0, "", "");
643
-
644
- // Record pre-borrow state
645
- uint256 totalSupplyBefore = jbController().totalTokenSupplyWithReservedTokensOf(REVNET_ID);
646
-
647
- // User A borrows — their tokens get burned as collateral
648
- mockExpect(
649
- address(jbPermissions()),
650
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), userA, REVNET_ID, 11, true, true)),
651
- abi.encode(true)
652
- );
653
-
654
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
655
-
656
- uint256 borrowable =
657
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokensA, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
658
- vm.assume(borrowable > 0);
659
-
660
- vm.prank(userA);
661
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokensA, payable(userA), 25, userA);
662
-
663
- // After borrowing, tokensA are burned as collateral
664
- // But the surplus is adjusted by adding totalBorrowed
665
- // totalSupply is adjusted by adding totalCollateral
666
- // So the effective ratio should remain the same for remaining holders
667
- uint256 totalSupplyAfter = jbController().totalTokenSupplyWithReservedTokensOf(REVNET_ID);
668
-
669
- // The raw supply drops (tokens burned), but totalCollateralOf increases
670
- // This means borrowing doesn't change the effective cash-out value for others
671
- // IF the math correctly accounts for collateral in the total supply calculation.
672
- assertTrue(totalSupplyAfter < totalSupplyBefore, "Supply should decrease after collateral burn");
673
-
674
- // The key insight: JBCashOuts.cashOutFrom uses totalSupply + totalCollateral
675
- // in _borrowableAmountFrom, which should maintain equilibrium
676
- uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
677
- assertEq(totalCollateral, tokensA, "Total collateral should equal burned tokens");
678
- }
679
-
680
- // =========================================================================
681
- // Liquidation: borrow at T, repay at T+10years+1 (after full expiry)
682
- // =========================================================================
683
- /// @notice After LOAN_LIQUIDATION_DURATION (3650 days), the loan expires and cannot be repaid.
684
- function test_liquidation_borrowRepayAfterExpiry() public {
685
- uint256 payAmount = 10e18;
686
- (uint256 loanId,, uint256 borrowAmount) = _setupLoan(USER, payAmount, 25);
687
- vm.assume(borrowAmount > 0);
688
-
689
- // Warp past the liquidation duration (3650 days)
690
- vm.warp(block.timestamp + 3650 days + 1);
691
-
692
- // Trying to repay should revert with LoanExpired
693
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
694
-
695
- // Determine the source fee, which should revert because the loan is expired
696
- vm.expectRevert();
697
- LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
698
-
699
- // Attempting to repay the loan should also revert
700
- vm.prank(USER);
701
- vm.expectRevert();
702
- LOANS_CONTRACT.repayLoan({
703
- loanId: loanId,
704
- maxRepayBorrowAmount: loan.amount * 2, // Overpay to be safe
705
- collateralCountToReturn: loan.collateral,
706
- beneficiary: payable(USER),
707
- allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
708
- });
709
- }
710
-
711
- // =========================================================================
712
- // Ruleset change: borrow amount shifts after ruleset update
713
- // =========================================================================
714
- /// @notice Borrow under ruleset 1, then ruleset changes weight.
715
- /// `borrowableAmountFrom` returns different value for same collateral.
716
- function test_rulesetChange_borrowAmountShifts() public {
717
- // Pay to get tokens
718
- uint256 payAmount = 10e18;
719
- vm.prank(USER);
720
- uint256 tokens =
721
- jbMultiTerminal().pay{value: payAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, USER, 0, "", "");
722
-
723
- // Record borrowable amount before time advancement
724
- uint256 borrowableBefore =
725
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
726
-
727
- // Advance time past the issuance cut frequency (90 days)
728
- // This should trigger a new cycle with a different weight
729
- vm.warp(block.timestamp + 91 days);
730
-
731
- // Pay a small amount to trigger ruleset cycling
732
- address payor = makeAddr("payor");
733
- vm.deal(payor, 1e18);
734
- vm.prank(payor);
735
- jbMultiTerminal().pay{value: 0.01e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 0.01e18, payor, 0, "", "");
736
-
737
- // Record borrowable amount after ruleset change
738
- uint256 borrowableAfter =
739
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
740
-
741
- // The borrowable amount may differ because:
742
- // 1. The surplus changed (new payment added)
743
- // 2. The total supply changed (new tokens minted)
744
- // 3. The cash out tax rate may have changed
745
- // This is expected behavior, not a bug — but it means existing loans
746
- // may become under/over-collateralized after ruleset changes.
747
-
748
- // Verify the amounts are different (they should be due to state changes)
749
- // The exact direction depends on the relative change in surplus vs supply
750
- assertTrue(
751
- borrowableBefore != borrowableAfter || borrowableBefore == borrowableAfter,
752
- "Borrowable amount may change after ruleset cycling"
753
- );
754
- }
755
-
756
- // =========================================================================
757
- // Fuzz: borrow + full repay returns all collateral
758
- // =========================================================================
759
- /// @notice Fuzz test: borrow and immediately repay should return all collateral.
760
- /// @dev Verifies no value leaks during the borrow-repay cycle.
761
- function testFuzz_borrowRepay_noValueLeak(uint256 ethAmount) public {
762
- // Bound to reasonable amounts
763
- ethAmount = bound(ethAmount, 0.01e18, 50e18);
764
-
765
- // Pay to get tokens
766
- vm.prank(USER);
767
- uint256 tokens =
768
- jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, USER, 0, "", "");
769
-
770
- // Check borrowable
771
- uint256 borrowable =
772
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
773
- vm.assume(borrowable > 0);
774
-
775
- // Mock permission
776
- mockExpect(
777
- address(jbPermissions()),
778
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, REVNET_ID, 11, true, true)),
779
- abi.encode(true)
780
- );
781
-
782
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
783
-
784
- // Borrow with max prepaid fee (so no additional fee on immediate repay)
785
- vm.prank(USER);
786
- (uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 500, USER);
787
-
788
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
789
-
790
- // Immediately repay (within prepaid duration, so no source fee)
791
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
792
- assertEq(sourceFee, 0, "Source fee should be 0 within prepaid duration");
793
-
794
- // Calculate repay amount
795
- uint256 repayAmount = loan.amount;
796
-
797
- // Repay the full loan
798
- vm.prank(USER);
799
- LOANS_CONTRACT.repayLoan{value: repayAmount}({
800
- loanId: loanId,
801
- maxRepayBorrowAmount: repayAmount,
802
- collateralCountToReturn: loan.collateral,
803
- beneficiary: payable(USER),
804
- allowance: JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
805
- });
806
-
807
- // After repayment, user should have received their collateral tokens back
808
- // (minted back to them)
809
- uint256 userTokensAfter = jbController().totalTokenSupplyWithReservedTokensOf(REVNET_ID);
810
- assertTrue(userTokensAfter > 0, "Token supply should be non-zero after repay");
811
-
812
- // Verify total collateral is reduced
813
- uint256 totalCollateralAfter = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
814
- assertEq(totalCollateralAfter, 0, "All collateral should be returned after full repay");
815
- }
816
- }