@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,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
- }