@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,783 +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 {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
28
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
29
- import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
30
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
31
- import {REVLoans} from "../src/REVLoans.sol";
32
- import {REVLoan} from "../src/structs/REVLoan.sol";
33
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
34
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
35
- import {REVDescription} from "../src/structs/REVDescription.sol";
36
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
37
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
38
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
39
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
40
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
41
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
42
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
43
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
44
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
45
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.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 {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 terminal mock that always reverts on pay(), used to simulate fee payment failure.
55
- contract RevertingFeeTerminal is ERC165, IJBPayoutTerminal {
56
- function pay(
57
- uint256,
58
- address,
59
- uint256,
60
- address,
61
- uint256,
62
- string calldata,
63
- bytes calldata
64
- )
65
- external
66
- payable
67
- override
68
- returns (uint256)
69
- {
70
- revert("Fee payment failed");
71
- }
72
-
73
- function accountingContextForTokenOf(uint256, address) external pure override returns (JBAccountingContext memory) {
74
- return JBAccountingContext({
75
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
76
- });
77
- }
78
-
79
- function accountingContextsOf(uint256) external pure override returns (JBAccountingContext[] memory) {
80
- return new JBAccountingContext[](0);
81
- }
82
-
83
- function addAccountingContextsFor(uint256, JBAccountingContext[] calldata) external override {}
84
- function addToBalanceOf(
85
- uint256,
86
- address,
87
- uint256,
88
- bool,
89
- string calldata,
90
- bytes calldata
91
- )
92
- external
93
- payable
94
- override
95
- {}
96
-
97
- function currentSurplusOf(uint256, address[] calldata, uint256, uint256) external pure override returns (uint256) {
98
- return 0;
99
- }
100
-
101
- function migrateBalanceOf(uint256, address, IJBTerminal) external pure override returns (uint256) {
102
- return 0;
103
- }
104
-
105
- function sendPayoutsOf(uint256, address, uint256, uint256, uint256) external pure override returns (uint256) {
106
- return 0;
107
- }
108
-
109
- function useAllowanceOf(
110
- uint256,
111
- address,
112
- uint256,
113
- uint256,
114
- uint256,
115
- address payable,
116
- address payable,
117
- string calldata
118
- )
119
- external
120
- pure
121
- override
122
- returns (uint256)
123
- {
124
- return 0;
125
- }
126
-
127
- function previewPayFor(
128
- uint256,
129
- address,
130
- uint256,
131
- address,
132
- bytes calldata
133
- )
134
- external
135
- pure
136
- override
137
- returns (JBRuleset memory, uint256, uint256, JBPayHookSpecification[] memory)
138
- {
139
- JBRuleset memory ruleset;
140
- return (ruleset, 0, 0, new JBPayHookSpecification[](0));
141
- }
142
-
143
- function supportsInterface(bytes4 interfaceId) public view override(ERC165, IERC165) returns (bool) {
144
- return interfaceId == type(IJBTerminal).interfaceId || interfaceId == type(IJBPayoutTerminal).interfaceId
145
- || super.supportsInterface(interfaceId);
146
- }
147
-
148
- receive() external payable {}
149
- }
150
-
151
- struct FeeRecoveryProjectConfig {
152
- REVConfig configuration;
153
- JBTerminalConfig[] terminalConfigurations;
154
- REVSuckerDeploymentConfig suckerDeploymentConfiguration;
155
- }
156
-
157
- /// @title REVLoansFeeRecovery
158
- /// @notice Tests for the fee payment error recovery in REVLoans._addTo().
159
- /// @dev When feeTerminal.pay() reverts, the borrower should receive the fee amount back
160
- /// instead of losing it. For ERC-20 tokens, the dangling allowance must also be cleaned up.
161
- contract REVLoansFeeRecovery is TestBaseWorkflow {
162
- // forge-lint: disable-next-line(mixed-case-variable)
163
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
164
- // forge-lint: disable-next-line(mixed-case-variable)
165
- bytes32 ERC20_SALT = "REV_TOKEN";
166
-
167
- // forge-lint: disable-next-line(mixed-case-variable)
168
- REVDeployer REV_DEPLOYER;
169
- // forge-lint: disable-next-line(mixed-case-variable)
170
- REVOwner REV_OWNER;
171
- // forge-lint: disable-next-line(mixed-case-variable)
172
- JB721TiersHook EXAMPLE_HOOK;
173
- // forge-lint: disable-next-line(mixed-case-variable)
174
- IJB721TiersHookDeployer HOOK_DEPLOYER;
175
- // forge-lint: disable-next-line(mixed-case-variable)
176
- IJB721TiersHookStore HOOK_STORE;
177
- // forge-lint: disable-next-line(mixed-case-variable)
178
- IJBAddressRegistry ADDRESS_REGISTRY;
179
- // forge-lint: disable-next-line(mixed-case-variable)
180
- IREVLoans LOANS_CONTRACT;
181
- // forge-lint: disable-next-line(mixed-case-variable)
182
- MockERC20 TOKEN;
183
- // forge-lint: disable-next-line(mixed-case-variable)
184
- IJBSuckerRegistry SUCKER_REGISTRY;
185
- // forge-lint: disable-next-line(mixed-case-variable)
186
- CTPublisher PUBLISHER;
187
- // forge-lint: disable-next-line(mixed-case-variable)
188
- MockBuybackDataHook MOCK_BUYBACK;
189
- // forge-lint: disable-next-line(mixed-case-variable)
190
- RevertingFeeTerminal REVERTING_TERMINAL;
191
-
192
- // forge-lint: disable-next-line(mixed-case-variable)
193
- uint256 FEE_PROJECT_ID;
194
- // forge-lint: disable-next-line(mixed-case-variable)
195
- uint256 REVNET_ID;
196
-
197
- // forge-lint: disable-next-line(mixed-case-variable)
198
- address USER = makeAddr("user");
199
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
200
-
201
- function _getFeeProjectConfig() internal view returns (FeeRecoveryProjectConfig memory) {
202
- uint8 decimals = 18;
203
- uint256 decimalMultiplier = 10 ** decimals;
204
-
205
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
206
- accountingContextsToAccept[0] = JBAccountingContext({
207
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
208
- });
209
- accountingContextsToAccept[1] =
210
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
211
-
212
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
213
- terminalConfigurations[0] =
214
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
215
-
216
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
217
- JBSplit[] memory splits = new JBSplit[](1);
218
- splits[0].beneficiary = payable(multisig());
219
- splits[0].percent = 10_000;
220
-
221
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
222
- issuanceConfs[0] = REVAutoIssuance({
223
- // forge-lint: disable-next-line(unsafe-typecast)
224
- chainId: uint32(block.chainid),
225
- // forge-lint: disable-next-line(unsafe-typecast)
226
- count: uint104(70_000 * decimalMultiplier),
227
- beneficiary: multisig()
228
- });
229
-
230
- stageConfigurations[0] = REVStageConfig({
231
- startsAtOrAfter: uint40(block.timestamp),
232
- autoIssuances: issuanceConfs,
233
- splitPercent: 2000,
234
- splits: splits,
235
- // forge-lint: disable-next-line(unsafe-typecast)
236
- initialIssuance: uint112(1000 * decimalMultiplier),
237
- issuanceCutFrequency: 90 days,
238
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
239
- cashOutTaxRate: 6000,
240
- extraMetadata: 0
241
- });
242
-
243
- REVConfig memory revnetConfiguration = REVConfig({
244
- description: REVDescription({
245
- name: "Revnet",
246
- ticker: "$REV",
247
- uri: "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx",
248
- salt: ERC20_SALT
249
- }),
250
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
251
- splitOperator: multisig(),
252
- stageConfigurations: stageConfigurations
253
- });
254
-
255
- return FeeRecoveryProjectConfig({
256
- configuration: revnetConfiguration,
257
- terminalConfigurations: terminalConfigurations,
258
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
259
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
260
- })
261
- });
262
- }
263
-
264
- function _getRevnetConfig() internal view returns (FeeRecoveryProjectConfig memory) {
265
- uint8 decimals = 18;
266
- uint256 decimalMultiplier = 10 ** decimals;
267
-
268
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
269
- accountingContextsToAccept[0] = JBAccountingContext({
270
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
271
- });
272
- accountingContextsToAccept[1] =
273
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
274
-
275
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
276
- terminalConfigurations[0] =
277
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
278
-
279
- JBSplit[] memory splits = new JBSplit[](1);
280
- splits[0].beneficiary = payable(multisig());
281
- splits[0].percent = 10_000;
282
-
283
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
284
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
285
- issuanceConfs[0] = REVAutoIssuance({
286
- // forge-lint: disable-next-line(unsafe-typecast)
287
- chainId: uint32(block.chainid),
288
- // forge-lint: disable-next-line(unsafe-typecast)
289
- count: uint104(70_000 * decimalMultiplier),
290
- beneficiary: multisig()
291
- });
292
-
293
- stageConfigurations[0] = REVStageConfig({
294
- startsAtOrAfter: uint40(block.timestamp),
295
- autoIssuances: issuanceConfs,
296
- splitPercent: 2000,
297
- splits: splits,
298
- // forge-lint: disable-next-line(unsafe-typecast)
299
- initialIssuance: uint112(1000 * decimalMultiplier),
300
- issuanceCutFrequency: 90 days,
301
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
302
- cashOutTaxRate: 6000,
303
- extraMetadata: 0
304
- });
305
-
306
- REVLoanSource[] memory _loanSources = new REVLoanSource[](2);
307
- _loanSources[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
308
- _loanSources[1] = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
309
-
310
- REVConfig memory revnetConfiguration = REVConfig({
311
- description: REVDescription({
312
- name: "NANA",
313
- ticker: "$NANA",
314
- uri: "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx",
315
- salt: "NANA_TOKEN"
316
- }),
317
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
318
- splitOperator: multisig(),
319
- stageConfigurations: stageConfigurations
320
- });
321
-
322
- return FeeRecoveryProjectConfig({
323
- configuration: revnetConfiguration,
324
- terminalConfigurations: terminalConfigurations,
325
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
326
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
327
- })
328
- });
329
- }
330
-
331
- function setUp() public override {
332
- super.setUp();
333
-
334
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
335
-
336
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
337
- HOOK_STORE = new JB721TiersHookStore();
338
- EXAMPLE_HOOK = new JB721TiersHook(
339
- jbDirectory(),
340
- jbPermissions(),
341
- jbPrices(),
342
- jbRulesets(),
343
- HOOK_STORE,
344
- jbSplits(),
345
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
346
- multisig()
347
- );
348
- ADDRESS_REGISTRY = new JBAddressRegistry();
349
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
350
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
351
- MOCK_BUYBACK = new MockBuybackDataHook();
352
- TOKEN = new MockERC20("1/2 ETH", "1/2");
353
- REVERTING_TERMINAL = new RevertingFeeTerminal();
354
-
355
- MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
356
- vm.prank(multisig());
357
- jbPrices()
358
- .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
359
-
360
- LOANS_CONTRACT = new REVLoans({
361
- controller: jbController(),
362
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
363
- revId: FEE_PROJECT_ID,
364
- owner: address(this),
365
- permit2: permit2(),
366
- trustedForwarder: TRUSTED_FORWARDER
367
- });
368
-
369
- REV_OWNER = new REVOwner(
370
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
371
- jbDirectory(),
372
- FEE_PROJECT_ID,
373
- SUCKER_REGISTRY,
374
- address(LOANS_CONTRACT),
375
- address(0)
376
- );
377
-
378
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
379
- jbController(),
380
- SUCKER_REGISTRY,
381
- FEE_PROJECT_ID,
382
- HOOK_DEPLOYER,
383
- PUBLISHER,
384
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
385
- address(LOANS_CONTRACT),
386
- TRUSTED_FORWARDER,
387
- address(REV_OWNER)
388
- );
389
-
390
- REV_OWNER.setDeployer(REV_DEPLOYER);
391
-
392
- // Deploy fee project.
393
- vm.prank(multisig());
394
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
395
-
396
- FeeRecoveryProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
397
- vm.prank(multisig());
398
- REV_DEPLOYER.deployFor({
399
- revnetId: FEE_PROJECT_ID,
400
- configuration: feeProjectConfig.configuration,
401
- terminalConfigurations: feeProjectConfig.terminalConfigurations,
402
- suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
403
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
404
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
405
- });
406
-
407
- // Deploy revnet with loans enabled.
408
- FeeRecoveryProjectConfig memory revnetConfig = _getRevnetConfig();
409
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
410
- revnetId: 0,
411
- configuration: revnetConfig.configuration,
412
- terminalConfigurations: revnetConfig.terminalConfigurations,
413
- suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration,
414
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
415
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
416
- });
417
-
418
- vm.deal(USER, 1000e18);
419
- }
420
-
421
- // =========================================================================
422
- // Helpers
423
- // =========================================================================
424
-
425
- /// @notice Mock loan permissions for a user.
426
- function _mockLoanPermission(address user) internal {
427
- mockExpect(
428
- address(jbPermissions()),
429
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
430
- abi.encode(true)
431
- );
432
- }
433
-
434
- /// @notice Make the directory return the reverting terminal as the fee terminal for the REV project.
435
- function _mockRevertingFeeTerminal(address token) internal {
436
- vm.mockCall(
437
- address(jbDirectory()),
438
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, FEE_PROJECT_ID, token),
439
- abi.encode(address(REVERTING_TERMINAL))
440
- );
441
- }
442
-
443
- /// @notice Borrow against native ETH and return the borrower's balance change.
444
- function _borrowNative(
445
- address user,
446
- uint256 ethAmount,
447
- uint256 prepaidFee
448
- )
449
- internal
450
- returns (uint256 loanId, uint256 borrowerBalanceBefore, uint256 borrowerBalanceAfter)
451
- {
452
- // Pay into revnet to get tokens.
453
- vm.prank(user);
454
- uint256 tokenCount =
455
- jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
456
-
457
- _mockLoanPermission(user);
458
-
459
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
460
-
461
- borrowerBalanceBefore = user.balance;
462
-
463
- vm.prank(user);
464
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
465
-
466
- borrowerBalanceAfter = user.balance;
467
- }
468
-
469
- // =========================================================================
470
- // Test: Normal fee payment succeeds (regression — confirm existing behavior)
471
- // =========================================================================
472
-
473
- /// @notice When the fee terminal is healthy, the REV fee is deducted from the borrower's payout.
474
- function test_feePaymentSuccess_nativeToken() public {
475
- (, uint256 balanceBefore, uint256 balanceAfter) = _borrowNative(USER, 10e18, 25);
476
-
477
- uint256 received = balanceAfter - balanceBefore;
478
-
479
- // The borrower should have received something (net of both source fee + REV fee).
480
- assertGt(received, 0, "Borrower should receive ETH");
481
-
482
- // No ETH should be stuck in the loans contract.
483
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract");
484
- }
485
-
486
- // =========================================================================
487
- // Test: Fee terminal reverts with native ETH — borrower gets fee back
488
- // =========================================================================
489
-
490
- /// @notice When feeTerminal.pay() reverts, the borrower receives the REV fee amount back.
491
- function test_feePaymentFailure_nativeToken_borrowerGetsMoreETH() public {
492
- // Pay into revnet first so both borrow attempts start from identical state.
493
- vm.prank(USER);
494
- uint256 tokenCount =
495
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
496
-
497
- _mockLoanPermission(USER);
498
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
499
-
500
- // Snapshot state before borrow.
501
- uint256 snap = vm.snapshotState();
502
-
503
- // Normal borrow.
504
- uint256 balBefore = USER.balance;
505
- vm.prank(USER);
506
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
507
- uint256 normalReceived = USER.balance - balBefore;
508
-
509
- // Revert to snapshot — identical state.
510
- vm.revertToState(snap);
511
-
512
- // Mock the fee terminal to revert.
513
- _mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
514
- _mockLoanPermission(USER);
515
-
516
- balBefore = USER.balance;
517
- vm.prank(USER);
518
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
519
- uint256 failReceived = USER.balance - balBefore;
520
-
521
- // The borrower with a failed fee terminal should receive MORE than the normal borrower,
522
- // because the REV fee (1% of borrow amount) is returned to them.
523
- assertGt(failReceived, normalReceived, "Failed-fee borrower should receive more ETH than normal borrower");
524
-
525
- // No ETH should be stuck in the loans contract.
526
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract after fee failure");
527
- }
528
-
529
- // =========================================================================
530
- // Test: Fee terminal reverts with native ETH — amount difference matches REV fee
531
- // =========================================================================
532
-
533
- /// @notice The extra ETH the borrower receives when the fee terminal reverts matches
534
- /// the expected REV fee amount (1% of borrow amount).
535
- function test_feePaymentFailure_nativeToken_exactFeeRecovery() public {
536
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
537
-
538
- // Pay into revnet.
539
- vm.prank(USER);
540
- uint256 tokens =
541
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, USER, 0, "", "");
542
-
543
- _mockLoanPermission(USER);
544
-
545
- // Snapshot.
546
- uint256 snap = vm.snapshotState();
547
-
548
- // Normal borrow.
549
- uint256 balBefore = USER.balance;
550
- vm.prank(USER);
551
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25, USER);
552
- uint256 normalReceived = USER.balance - balBefore;
553
-
554
- // Get the actual borrow amount from the loan to compute expected REV fee.
555
- // Loan ID = revnetId * 1e12 + loanNumber (first loan = 1).
556
- REVLoan memory loan = LOANS_CONTRACT.loanOf(REVNET_ID * 1_000_000_000_000 + 1);
557
- uint256 expectedRevFee =
558
- JBFees.feeAmountFrom({amountBeforeFee: loan.amount, feePercent: LOANS_CONTRACT.REV_PREPAID_FEE_PERCENT()});
559
-
560
- // Revert to snapshot.
561
- vm.revertToState(snap);
562
-
563
- // Mock fee terminal to revert.
564
- _mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
565
- _mockLoanPermission(USER);
566
-
567
- balBefore = USER.balance;
568
- vm.prank(USER);
569
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25, USER);
570
- uint256 failReceived = USER.balance - balBefore;
571
-
572
- // The difference should be the REV fee amount.
573
- uint256 difference = failReceived - normalReceived;
574
- assertEq(difference, expectedRevFee, "Difference should equal the REV fee amount");
575
-
576
- // Verify no funds stuck.
577
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck");
578
- }
579
-
580
- // =========================================================================
581
- // Test: Fee terminal reverts with ERC-20 — allowance is cleaned up
582
- // =========================================================================
583
-
584
- /// @notice When feeTerminal.pay() reverts for an ERC-20 loan, the dangling allowance
585
- /// to the fee terminal is removed via safeDecreaseAllowance.
586
- function test_feePaymentFailure_erc20_allowanceCleaned() public {
587
- // Mock the fee terminal to revert for the TOKEN.
588
- _mockRevertingFeeTerminal(address(TOKEN));
589
-
590
- // Fund user with ERC-20 tokens.
591
- uint256 payAmount = 1_000_000; // 6 decimals
592
- deal(address(TOKEN), USER, payAmount);
593
-
594
- // Pay into revnet with ERC-20.
595
- vm.startPrank(USER);
596
- TOKEN.approve(address(jbMultiTerminal()), payAmount);
597
- uint256 tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
598
- vm.stopPrank();
599
-
600
- _mockLoanPermission(USER);
601
- REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
602
-
603
- // Check allowance to reverting terminal BEFORE borrow.
604
- uint256 allowanceBefore = TOKEN.allowance(address(LOANS_CONTRACT), address(REVERTING_TERMINAL));
605
- assertEq(allowanceBefore, 0, "No pre-existing allowance");
606
-
607
- vm.prank(USER);
608
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(USER), 25, USER);
609
-
610
- // After the borrow, the allowance to the reverting terminal should still be 0
611
- // (the catch block decreased it).
612
- uint256 allowanceAfter = TOKEN.allowance(address(LOANS_CONTRACT), address(REVERTING_TERMINAL));
613
- assertEq(allowanceAfter, 0, "Allowance should be cleaned up after fee failure");
614
-
615
- // No tokens stuck in the loans contract.
616
- assertEq(TOKEN.balanceOf(address(LOANS_CONTRACT)), 0, "No ERC-20 stuck in loans contract");
617
- }
618
-
619
- // =========================================================================
620
- // Test: Fee terminal reverts with ERC-20 — borrower gets fee back
621
- // =========================================================================
622
-
623
- /// @notice When feeTerminal.pay() reverts for an ERC-20 loan, the borrower receives
624
- /// the fee amount that would have gone to the REV project.
625
- function test_feePaymentFailure_erc20_borrowerGetsMoreTokens() public {
626
- uint256 payAmount = 1_000_000; // 6 decimals
627
- REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
628
-
629
- // Pay into revnet with ERC-20.
630
- deal(address(TOKEN), USER, payAmount);
631
- vm.startPrank(USER);
632
- TOKEN.approve(address(jbMultiTerminal()), payAmount);
633
- uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payAmount, USER, 0, "", "");
634
- vm.stopPrank();
635
-
636
- _mockLoanPermission(USER);
637
-
638
- // Snapshot.
639
- uint256 snap = vm.snapshotState();
640
-
641
- // Normal borrow.
642
- uint256 tokenBalBefore = TOKEN.balanceOf(USER);
643
- vm.prank(USER);
644
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25, USER);
645
- uint256 normalReceived = TOKEN.balanceOf(USER) - tokenBalBefore;
646
-
647
- // Revert to snapshot.
648
- vm.revertToState(snap);
649
-
650
- // Mock fee terminal to revert.
651
- _mockRevertingFeeTerminal(address(TOKEN));
652
- _mockLoanPermission(USER);
653
-
654
- tokenBalBefore = TOKEN.balanceOf(USER);
655
- vm.prank(USER);
656
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(USER), 25, USER);
657
- uint256 failReceived = TOKEN.balanceOf(USER) - tokenBalBefore;
658
-
659
- // Failed-fee borrower should receive more tokens.
660
- assertGt(failReceived, normalReceived, "Failed-fee ERC-20 borrower should receive more tokens");
661
- }
662
-
663
- // =========================================================================
664
- // Test: No fee terminal (address(0)) — revFeeAmount is zero, no try/catch
665
- // =========================================================================
666
-
667
- /// @notice When no fee terminal exists for the token, revFeeAmount is 0 and no fee is attempted.
668
- function test_noFeeTerminal_borrowStillWorks() public {
669
- // Mock the directory to return address(0) for the fee terminal.
670
- vm.mockCall(
671
- address(jbDirectory()),
672
- abi.encodeWithSelector(IJBDirectory.primaryTerminalOf.selector, FEE_PROJECT_ID, JBConstants.NATIVE_TOKEN),
673
- abi.encode(address(0))
674
- );
675
-
676
- // Borrow should still work — no fee is taken.
677
- (, uint256 balanceBefore, uint256 balanceAfter) = _borrowNative(USER, 10e18, 25);
678
- uint256 received = balanceAfter - balanceBefore;
679
- assertGt(received, 0, "Borrower should receive ETH even without fee terminal");
680
-
681
- // No ETH stuck.
682
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck");
683
- }
684
-
685
- // =========================================================================
686
- // Test: Multiple borrows with fee failure — no cumulative stuck funds
687
- // =========================================================================
688
-
689
- /// @notice After multiple borrows where the fee terminal reverts, no funds accumulate
690
- /// in the loans contract.
691
- function test_feePaymentFailure_multipleBorrows_noStuckFunds() public {
692
- _mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
693
-
694
- for (uint256 i; i < 3; i++) {
695
- address borrower = makeAddr(string(abi.encodePacked("borrower", i)));
696
- vm.deal(borrower, 100e18);
697
-
698
- vm.prank(borrower);
699
- uint256 tokens =
700
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, borrower, 0, "", "");
701
-
702
- mockExpect(
703
- address(jbPermissions()),
704
- abi.encodeCall(
705
- IJBPermissions.hasPermission, (address(LOANS_CONTRACT), borrower, REVNET_ID, 11, true, true)
706
- ),
707
- abi.encode(true)
708
- );
709
-
710
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
711
-
712
- vm.prank(borrower);
713
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(borrower), 25, borrower);
714
- }
715
-
716
- // After 3 borrows with fee failures, no ETH should be stuck.
717
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck after multiple fee-failed borrows");
718
- }
719
-
720
- // =========================================================================
721
- // Fuzz: Fee recovery always returns correct amount to borrower
722
- // =========================================================================
723
-
724
- /// @notice Fuzz test: regardless of the borrow amount, when the fee terminal reverts,
725
- /// the borrower always receives the full netAmountPaidOut minus only the source fee.
726
- function test_fuzz_feeRecovery_nativeToken(uint256 payAmount) public {
727
- // Bound to reasonable range. Need enough to get a nonzero borrow.
728
- payAmount = bound(payAmount, 1e16, 100e18);
729
-
730
- _mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
731
-
732
- address borrower = makeAddr("fuzzBorrower");
733
- vm.deal(borrower, payAmount + 1e18);
734
-
735
- vm.prank(borrower);
736
- uint256 tokens = jbMultiTerminal().pay{value: payAmount}(
737
- REVNET_ID, JBConstants.NATIVE_TOKEN, payAmount, borrower, 0, "", ""
738
- );
739
-
740
- uint256 borrowable =
741
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
742
-
743
- // Skip if not enough surplus to borrow.
744
- if (borrowable == 0) return;
745
-
746
- _mockLoanPermission(borrower);
747
- REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
748
-
749
- uint256 balanceBefore = borrower.balance;
750
- vm.prank(borrower);
751
- LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokens, payable(borrower), 25, borrower);
752
- uint256 received = borrower.balance - balanceBefore;
753
-
754
- // The borrower should always receive something.
755
- assertGt(received, 0, "Borrower should receive ETH in fuzz");
756
-
757
- // No funds stuck.
758
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in fuzz");
759
- }
760
-
761
- // =========================================================================
762
- // Test: Fee recovery on native token — ETH returned from failed call
763
- // =========================================================================
764
-
765
- /// @notice Verifies that when a native-token fee terminal call reverts, the ETH sent
766
- /// with the call is returned to REVLoans and forwarded to the borrower.
767
- /// The reverting terminal should NOT hold any ETH.
768
- function test_feePaymentFailure_nativeToken_revertingTerminalHoldsNoETH() public {
769
- _mockRevertingFeeTerminal(JBConstants.NATIVE_TOKEN);
770
-
771
- uint256 revertingTerminalBalanceBefore = address(REVERTING_TERMINAL).balance;
772
-
773
- _borrowNative(USER, 10e18, 25);
774
-
775
- // The reverting terminal should not have received any ETH.
776
- assertEq(
777
- address(REVERTING_TERMINAL).balance, revertingTerminalBalanceBefore, "Reverting terminal should hold no ETH"
778
- );
779
-
780
- // No ETH stuck in loans contract.
781
- assertEq(address(LOANS_CONTRACT).balance, 0, "No ETH stuck in loans contract");
782
- }
783
- }