@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,553 +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
- // forge-lint: disable-next-line(unaliased-plain-import)
9
- import /* {*} from */ "./../src/REVDeployer.sol";
10
- // forge-lint: disable-next-line(unaliased-plain-import)
11
- import "@croptop/core-v6/src/CTPublisher.sol";
12
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
- // forge-lint: disable-next-line(unaliased-plain-import)
14
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
15
- // forge-lint: disable-next-line(unaliased-plain-import)
16
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
17
- // forge-lint: disable-next-line(unaliased-plain-import)
18
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
19
- // forge-lint: disable-next-line(unaliased-plain-import)
20
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
21
- // forge-lint: disable-next-line(unaliased-plain-import)
22
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
23
-
24
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
25
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
26
- import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
27
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
28
- import {REVLoans} from "../src/REVLoans.sol";
29
- import {REVLoan} from "../src/structs/REVLoan.sol";
30
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
31
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
32
- import {REVDescription} from "../src/structs/REVDescription.sol";
33
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
34
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
35
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
36
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
37
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
38
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
39
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
40
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
41
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
42
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
43
- import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
44
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
45
- import {REVOwner} from "../src/REVOwner.sol";
46
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
47
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
48
-
49
- /// @notice Tests for loan source rotation: verify behavior when loans are taken from different sources (tokens)
50
- /// and that existing loans remain valid and repayable after new sources are introduced.
51
- contract TestLoanSourceRotation is TestBaseWorkflow {
52
- // forge-lint: disable-next-line(mixed-case-variable)
53
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
54
-
55
- // forge-lint: disable-next-line(mixed-case-variable)
56
- REVDeployer REV_DEPLOYER;
57
- // forge-lint: disable-next-line(mixed-case-variable)
58
- REVOwner REV_OWNER;
59
- // forge-lint: disable-next-line(mixed-case-variable)
60
- JB721TiersHook EXAMPLE_HOOK;
61
- // forge-lint: disable-next-line(mixed-case-variable)
62
- IJB721TiersHookDeployer HOOK_DEPLOYER;
63
- // forge-lint: disable-next-line(mixed-case-variable)
64
- IJB721TiersHookStore HOOK_STORE;
65
- // forge-lint: disable-next-line(mixed-case-variable)
66
- IJBAddressRegistry ADDRESS_REGISTRY;
67
- // forge-lint: disable-next-line(mixed-case-variable)
68
- IREVLoans LOANS_CONTRACT;
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
- MockERC20 TOKEN;
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- IJBSuckerRegistry SUCKER_REGISTRY;
73
- // forge-lint: disable-next-line(mixed-case-variable)
74
- CTPublisher PUBLISHER;
75
- // forge-lint: disable-next-line(mixed-case-variable)
76
- MockBuybackDataHook MOCK_BUYBACK;
77
-
78
- // forge-lint: disable-next-line(mixed-case-variable)
79
- uint256 FEE_PROJECT_ID;
80
- // forge-lint: disable-next-line(mixed-case-variable)
81
- uint256 REVNET_ID;
82
-
83
- // forge-lint: disable-next-line(mixed-case-variable)
84
- address USER = makeAddr("user");
85
-
86
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
87
-
88
- function setUp() public override {
89
- super.setUp();
90
-
91
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
92
-
93
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
94
- HOOK_STORE = new JB721TiersHookStore();
95
- EXAMPLE_HOOK = new JB721TiersHook(
96
- jbDirectory(),
97
- jbPermissions(),
98
- jbPrices(),
99
- jbRulesets(),
100
- HOOK_STORE,
101
- jbSplits(),
102
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
103
- multisig()
104
- );
105
- ADDRESS_REGISTRY = new JBAddressRegistry();
106
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
107
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
108
- MOCK_BUYBACK = new MockBuybackDataHook();
109
-
110
- // Deploy a 6-decimal ERC-20 token.
111
- TOKEN = new MockERC20("Stable Token", "STABLE");
112
-
113
- // Price feed: TOKEN -> ETH. 1 TOKEN (6 dec) = 0.0005 ETH.
114
- MockPriceFeed priceFeed = new MockPriceFeed(5e14, 18);
115
- vm.prank(multisig());
116
- jbPrices()
117
- .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
118
-
119
- LOANS_CONTRACT = new REVLoans({
120
- controller: jbController(),
121
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
122
- revId: FEE_PROJECT_ID,
123
- owner: address(this),
124
- permit2: permit2(),
125
- trustedForwarder: TRUSTED_FORWARDER
126
- });
127
-
128
- REV_OWNER = new REVOwner(
129
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
130
- jbDirectory(),
131
- FEE_PROJECT_ID,
132
- SUCKER_REGISTRY,
133
- address(LOANS_CONTRACT),
134
- address(0)
135
- );
136
-
137
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
138
- jbController(),
139
- SUCKER_REGISTRY,
140
- FEE_PROJECT_ID,
141
- HOOK_DEPLOYER,
142
- PUBLISHER,
143
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
144
- address(LOANS_CONTRACT),
145
- TRUSTED_FORWARDER,
146
- address(REV_OWNER)
147
- );
148
-
149
- REV_OWNER.setDeployer(REV_DEPLOYER);
150
-
151
- vm.prank(multisig());
152
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
153
-
154
- _deployFeeProject();
155
- _deployRevnet();
156
-
157
- vm.deal(USER, 1000e18);
158
- }
159
-
160
- function _deployFeeProject() internal {
161
- JBAccountingContext[] memory acc = new JBAccountingContext[](2);
162
- acc[0] = JBAccountingContext({
163
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
164
- });
165
- acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
166
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
167
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
168
-
169
- JBSplit[] memory splits = new JBSplit[](1);
170
- splits[0].beneficiary = payable(multisig());
171
- splits[0].percent = 10_000;
172
-
173
- REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
174
- ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
175
-
176
- REVStageConfig[] memory stages = new REVStageConfig[](1);
177
- stages[0] = REVStageConfig({
178
- startsAtOrAfter: uint40(block.timestamp),
179
- autoIssuances: ai,
180
- splitPercent: 2000,
181
- splits: splits,
182
- initialIssuance: uint112(1000e18),
183
- issuanceCutFrequency: 90 days,
184
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
185
- cashOutTaxRate: 6000,
186
- extraMetadata: 0
187
- });
188
-
189
- REVConfig memory cfg = REVConfig({
190
- // forge-lint: disable-next-line(named-struct-fields)
191
- description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
192
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
193
- splitOperator: multisig(),
194
- stageConfigurations: stages
195
- });
196
-
197
- vm.prank(multisig());
198
- REV_DEPLOYER.deployFor({
199
- revnetId: FEE_PROJECT_ID,
200
- configuration: cfg,
201
- terminalConfigurations: tc,
202
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
203
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
204
- }),
205
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
206
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
207
- });
208
- }
209
-
210
- function _deployRevnet() internal {
211
- JBAccountingContext[] memory acc = new JBAccountingContext[](2);
212
- acc[0] = JBAccountingContext({
213
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
214
- });
215
- acc[1] = JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
216
- JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
217
- tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
218
-
219
- JBSplit[] memory splits = new JBSplit[](1);
220
- splits[0].beneficiary = payable(multisig());
221
- splits[0].percent = 10_000;
222
-
223
- REVAutoIssuance[] memory ai = new REVAutoIssuance[](1);
224
- ai[0] = REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(70_000e18), beneficiary: multisig()});
225
-
226
- REVStageConfig[] memory stages = new REVStageConfig[](1);
227
- stages[0] = REVStageConfig({
228
- startsAtOrAfter: uint40(block.timestamp),
229
- autoIssuances: ai,
230
- splitPercent: 2000,
231
- splits: splits,
232
- initialIssuance: uint112(1000e18),
233
- issuanceCutFrequency: 90 days,
234
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
235
- cashOutTaxRate: 6000,
236
- extraMetadata: 0
237
- });
238
-
239
- REVConfig memory cfg = REVConfig({
240
- // forge-lint: disable-next-line(named-struct-fields)
241
- description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
242
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
243
- splitOperator: multisig(),
244
- stageConfigurations: stages
245
- });
246
-
247
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
248
- revnetId: 0,
249
- configuration: cfg,
250
- terminalConfigurations: tc,
251
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
252
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
253
- }),
254
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
255
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
256
- });
257
- }
258
-
259
- /// @notice Helper: take a loan from a given source (ETH or TOKEN).
260
- function _borrowWithSource(
261
- address user,
262
- uint256 ethAmount,
263
- REVLoanSource memory source
264
- )
265
- internal
266
- returns (uint256 loanId, uint256 tokenCount, uint256 borrowAmount)
267
- {
268
- // Pay with ETH to get revnet tokens.
269
- vm.prank(user);
270
- tokenCount =
271
- jbMultiTerminal().pay{value: ethAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, ethAmount, user, 0, "", "");
272
-
273
- // Check borrowable amount for the given source.
274
- borrowAmount = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(source.token)));
275
- if (borrowAmount == 0) return (0, tokenCount, 0);
276
-
277
- // Mock permission for burn.
278
- mockExpect(
279
- address(jbPermissions()),
280
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
281
- abi.encode(true)
282
- );
283
-
284
- vm.prank(user);
285
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), 25, user);
286
- }
287
-
288
- //*********************************************************************//
289
- // --- Loan Source Rotation Tests ------------------------------------ //
290
- //*********************************************************************//
291
-
292
- /// @notice First loan uses ETH source. A second loan from a different user uses TOKEN source.
293
- /// Both should coexist and the loan sources array should reflect both.
294
- function test_loanFromMultipleSources() public {
295
- address user2 = makeAddr("user2");
296
- vm.deal(user2, 100e18);
297
-
298
- // Loan 1: borrow from ETH source.
299
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
300
- (uint256 loanId1,,) = _borrowWithSource(USER, 10e18, ethSource);
301
- require(loanId1 != 0, "ETH loan setup failed");
302
-
303
- // Verify ETH is now a loan source.
304
- assertTrue(
305
- LOANS_CONTRACT.isLoanSourceOf(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
306
- "ETH should be registered as a loan source"
307
- );
308
-
309
- // Loan 2: borrow from TOKEN source (need to fund the terminal with TOKEN first).
310
- // Use addToBalanceOf to properly register TOKEN in the revnet's accounting.
311
- uint256 tokenFunding = 1_000_000e6;
312
- TOKEN.mint(address(this), tokenFunding);
313
- TOKEN.approve(address(jbMultiTerminal()), tokenFunding);
314
- jbMultiTerminal().addToBalanceOf(REVNET_ID, address(TOKEN), tokenFunding, false, "", "");
315
-
316
- TOKEN.mint(user2, 100_000e6);
317
-
318
- // User2 pays ETH to get revnet tokens.
319
- vm.prank(user2);
320
- uint256 user2Tokens =
321
- jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, user2, 0, "", "");
322
-
323
- REVLoanSource memory tokenSource = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
324
- uint256 tokenBorrowable =
325
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, user2Tokens, 6, uint32(uint160(address(TOKEN))));
326
-
327
- if (tokenBorrowable > 0) {
328
- mockExpect(
329
- address(jbPermissions()),
330
- abi.encodeCall(
331
- IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user2, REVNET_ID, 11, true, true)
332
- ),
333
- abi.encode(true)
334
- );
335
-
336
- vm.prank(user2);
337
- (uint256 loanId2,) =
338
- LOANS_CONTRACT.borrowFrom(REVNET_ID, tokenSource, 0, user2Tokens, payable(user2), 25, user2);
339
- assertGt(loanId2, 0, "TOKEN loan should be created");
340
-
341
- // Both sources should now be registered.
342
- assertTrue(
343
- LOANS_CONTRACT.isLoanSourceOf(REVNET_ID, jbMultiTerminal(), address(TOKEN)),
344
- "TOKEN should be registered as a loan source"
345
- );
346
-
347
- // Loan sources array should have both entries.
348
- REVLoanSource[] memory sources = LOANS_CONTRACT.loanSourcesOf(REVNET_ID);
349
- assertGe(sources.length, 2, "should have at least 2 loan sources");
350
- }
351
- }
352
-
353
- /// @notice After taking a loan from ETH source, repay it fully. The source remains registered
354
- /// (sources array only grows, never shrinks).
355
- function test_repayEthLoan_sourcePersistsAfterRepay() public {
356
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
357
-
358
- (uint256 loanId,,) = _borrowWithSource(USER, 10e18, ethSource);
359
- require(loanId != 0, "Loan setup failed");
360
-
361
- // Verify loan is active.
362
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
363
- assertGt(loan.amount, 0, "loan should have a positive amount");
364
- assertGt(loan.collateral, 0, "loan should have collateral");
365
-
366
- // Record the total collateral before repay.
367
- uint256 collateralBefore = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
368
-
369
- // Calculate source fee for the full repay.
370
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
371
- uint256 repayTotal = loan.amount + sourceFee;
372
-
373
- // Repay the full loan.
374
- vm.deal(USER, repayTotal + 1e18); // Ensure enough ETH for repay.
375
- vm.prank(USER);
376
- LOANS_CONTRACT.repayLoan{value: repayTotal}(
377
- loanId,
378
- repayTotal,
379
- loan.collateral, // Return all collateral.
380
- payable(USER),
381
- JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
382
- );
383
-
384
- // Source should still be registered (sources array only grows).
385
- assertTrue(
386
- LOANS_CONTRACT.isLoanSourceOf(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN),
387
- "ETH source should persist after loan repay"
388
- );
389
-
390
- // Collateral should be decreased.
391
- uint256 collateralAfter = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
392
- assertLt(collateralAfter, collateralBefore, "total collateral should decrease after full repay");
393
- }
394
-
395
- /// @notice Take two sequential loans from different sources. Verify the second loan does not affect
396
- /// the first loan's terms (collateral, amount, source).
397
- function test_secondSourceDoesNotAffectFirstLoan() public {
398
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
399
-
400
- // First loan with ETH source.
401
- (uint256 loanId1,,) = _borrowWithSource(USER, 10e18, ethSource);
402
- require(loanId1 != 0, "First loan setup failed");
403
-
404
- // Capture first loan details.
405
- REVLoan memory loan1Before = LOANS_CONTRACT.loanOf(loanId1);
406
- uint256 totalBorrowedFromEthBefore =
407
- LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
408
-
409
- // Second loan: also from ETH source but by a different user.
410
- address user2 = makeAddr("user2");
411
- vm.deal(user2, 100e18);
412
-
413
- REVLoanSource memory ethSource2 = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
414
- vm.prank(user2);
415
- uint256 user2Tokens =
416
- jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user2, 0, "", "");
417
- uint256 user2Borrowable =
418
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, user2Tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
419
-
420
- if (user2Borrowable > 0) {
421
- mockExpect(
422
- address(jbPermissions()),
423
- abi.encodeCall(
424
- IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user2, REVNET_ID, 11, true, true)
425
- ),
426
- abi.encode(true)
427
- );
428
-
429
- vm.prank(user2);
430
- (uint256 loanId2,) =
431
- LOANS_CONTRACT.borrowFrom(REVNET_ID, ethSource2, 0, user2Tokens, payable(user2), 25, user2);
432
- assertGt(loanId2, 0, "second loan should be created");
433
-
434
- // First loan should be unaffected.
435
- REVLoan memory loan1After = LOANS_CONTRACT.loanOf(loanId1);
436
- assertEq(loan1After.amount, loan1Before.amount, "first loan amount should be unchanged");
437
- assertEq(loan1After.collateral, loan1Before.collateral, "first loan collateral should be unchanged");
438
- assertEq(loan1After.source.token, loan1Before.source.token, "first loan source token should be unchanged");
439
- assertEq(
440
- address(loan1After.source.terminal),
441
- address(loan1Before.source.terminal),
442
- "first loan source terminal should be unchanged"
443
- );
444
-
445
- // Total borrowed from ETH source should have increased.
446
- uint256 totalBorrowedFromEthAfter =
447
- LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
448
- assertGt(
449
- totalBorrowedFromEthAfter,
450
- totalBorrowedFromEthBefore,
451
- "total borrowed from ETH should increase with new loan"
452
- );
453
- }
454
- }
455
-
456
- /// @notice The fee calculation should be consistent regardless of which source is used.
457
- /// Both ETH and TOKEN sources should use the same prepaid fee percent logic.
458
- function test_feeCalculationConsistency_acrossSources() public {
459
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
460
-
461
- // Take ETH loan.
462
- (uint256 ethLoanId,,) = _borrowWithSource(USER, 10e18, ethSource);
463
- require(ethLoanId != 0, "ETH loan failed");
464
-
465
- REVLoan memory ethLoan = LOANS_CONTRACT.loanOf(ethLoanId);
466
-
467
- // Verify the prepaid fee percent is what we set (25 = 2.5%, the minimum).
468
- assertEq(ethLoan.prepaidFeePercent, 25, "ETH loan should have 2.5% prepaid fee");
469
-
470
- // Verify the prepaid duration is consistent with the fee percent.
471
- // prepaidDuration = prepaidFeePercent * LOAN_LIQUIDATION_DURATION / MAX_PREPAID_FEE_PERCENT.
472
- uint256 expectedDuration =
473
- (25 * LOANS_CONTRACT.LOAN_LIQUIDATION_DURATION()) / LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT();
474
- assertEq(ethLoan.prepaidDuration, expectedDuration, "prepaid duration should match formula");
475
-
476
- // Verify source fee amount is nonzero for a nonzero loan.
477
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(ethLoan, ethLoan.amount);
478
- // Within the prepaid window, the source fee should be zero (already prepaid).
479
- // After the prepaid window, fees accumulate linearly.
480
- assertEq(sourceFee, 0, "source fee should be 0 within prepaid window");
481
-
482
- // Warp well past the prepaid duration (halfway through the remaining loan term).
483
- // With prepaid=25/500, prepaid covers ~182.5 days. Warp an additional 5 years past that
484
- // so the fee calculation is significant enough to not round to zero.
485
- vm.warp(block.timestamp + ethLoan.prepaidDuration + 365 days * 5);
486
-
487
- // Now the source fee should be > 0 because we are well past the prepaid window.
488
- uint256 sourceFeeAfter = LOANS_CONTRACT.determineSourceFeeAmount(ethLoan, ethLoan.amount);
489
- assertGt(sourceFeeAfter, 0, "source fee should be nonzero well after prepaid window expires");
490
- }
491
-
492
- /// @notice Verify that totalBorrowedFrom is tracked per-source and does not bleed across sources.
493
- function test_totalBorrowedFrom_isolatedPerSource() public {
494
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
495
-
496
- // Take ETH loan.
497
- (uint256 loanId,,) = _borrowWithSource(USER, 10e18, ethSource);
498
- require(loanId != 0, "Loan failed");
499
-
500
- // Check totalBorrowedFrom for ETH source.
501
- uint256 totalBorrowedEth =
502
- LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), JBConstants.NATIVE_TOKEN);
503
- assertGt(totalBorrowedEth, 0, "should have nonzero total borrowed from ETH");
504
-
505
- // TOKEN source should have zero total borrowed.
506
- uint256 totalBorrowedToken = LOANS_CONTRACT.totalBorrowedFrom(REVNET_ID, jbMultiTerminal(), address(TOKEN));
507
- assertEq(totalBorrowedToken, 0, "TOKEN source should have zero total borrowed");
508
- }
509
-
510
- /// @notice Verify that taking a loan, then time passing, then taking another loan from the same source
511
- /// correctly increments the loan counter.
512
- function test_loanCounterIncrements_acrossTimePeriods() public {
513
- REVLoanSource memory ethSource = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
514
-
515
- uint256 countBefore = LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID);
516
-
517
- // First loan.
518
- (uint256 loanId1,,) = _borrowWithSource(USER, 5e18, ethSource);
519
- require(loanId1 != 0, "First loan failed");
520
-
521
- uint256 countAfterFirst = LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID);
522
- assertEq(countAfterFirst, countBefore + 1, "counter should increment by 1");
523
-
524
- // Warp 30 days.
525
- vm.warp(block.timestamp + 30 days);
526
-
527
- // Second loan.
528
- address user2 = makeAddr("user2_counter");
529
- vm.deal(user2, 100e18);
530
-
531
- vm.prank(user2);
532
- uint256 tokens = jbMultiTerminal().pay{value: 5e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 5e18, user2, 0, "", "");
533
- uint256 borrowable =
534
- LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
535
-
536
- if (borrowable > 0) {
537
- mockExpect(
538
- address(jbPermissions()),
539
- abi.encodeCall(
540
- IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user2, REVNET_ID, 11, true, true)
541
- ),
542
- abi.encode(true)
543
- );
544
-
545
- vm.prank(user2);
546
- (uint256 loanId2,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, ethSource, 0, tokens, payable(user2), 25, user2);
547
- assertGt(loanId2, 0, "second loan should succeed");
548
-
549
- uint256 countAfterSecond = LOANS_CONTRACT.totalLoansBorrowedFor(REVNET_ID);
550
- assertEq(countAfterSecond, countBefore + 2, "counter should increment by 2 total");
551
- }
552
- }
553
- }