@rev-net/core-v6 0.0.1

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 (92) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/REVNET_SECURITY_CHECKLIST.md +164 -0
  4. package/SECURITY.md +68 -0
  5. package/SKILLS.md +166 -0
  6. package/deployments/revnet-core-v5/arbitrum/REVDeployer.json +2821 -0
  7. package/deployments/revnet-core-v5/arbitrum/REVLoans.json +2260 -0
  8. package/deployments/revnet-core-v5/arbitrum_sepolia/REVDeployer.json +2821 -0
  9. package/deployments/revnet-core-v5/arbitrum_sepolia/REVLoans.json +2260 -0
  10. package/deployments/revnet-core-v5/base/REVDeployer.json +2825 -0
  11. package/deployments/revnet-core-v5/base/REVLoans.json +2264 -0
  12. package/deployments/revnet-core-v5/base_sepolia/REVDeployer.json +2825 -0
  13. package/deployments/revnet-core-v5/base_sepolia/REVLoans.json +2264 -0
  14. package/deployments/revnet-core-v5/ethereum/REVDeployer.json +2825 -0
  15. package/deployments/revnet-core-v5/ethereum/REVLoans.json +2264 -0
  16. package/deployments/revnet-core-v5/optimism/REVDeployer.json +2821 -0
  17. package/deployments/revnet-core-v5/optimism/REVLoans.json +2260 -0
  18. package/deployments/revnet-core-v5/optimism_sepolia/REVDeployer.json +2825 -0
  19. package/deployments/revnet-core-v5/optimism_sepolia/REVLoans.json +2264 -0
  20. package/deployments/revnet-core-v5/sepolia/REVDeployer.json +2825 -0
  21. package/deployments/revnet-core-v5/sepolia/REVLoans.json +2264 -0
  22. package/docs/book.css +13 -0
  23. package/docs/book.toml +13 -0
  24. package/docs/solidity.min.js +74 -0
  25. package/docs/src/README.md +88 -0
  26. package/docs/src/SUMMARY.md +20 -0
  27. package/docs/src/src/README.md +7 -0
  28. package/docs/src/src/REVDeployer.sol/contract.REVDeployer.md +968 -0
  29. package/docs/src/src/REVLoans.sol/contract.REVLoans.md +1047 -0
  30. package/docs/src/src/interfaces/IREVDeployer.sol/interface.IREVDeployer.md +243 -0
  31. package/docs/src/src/interfaces/IREVLoans.sol/interface.IREVLoans.md +296 -0
  32. package/docs/src/src/interfaces/README.md +5 -0
  33. package/docs/src/src/structs/README.md +14 -0
  34. package/docs/src/src/structs/REVAutoIssuance.sol/struct.REVAutoIssuance.md +19 -0
  35. package/docs/src/src/structs/REVBuybackHookConfig.sol/struct.REVBuybackHookConfig.md +19 -0
  36. package/docs/src/src/structs/REVBuybackPoolConfig.sol/struct.REVBuybackPoolConfig.md +21 -0
  37. package/docs/src/src/structs/REVConfig.sol/struct.REVConfig.md +35 -0
  38. package/docs/src/src/structs/REVCroptopAllowedPost.sol/struct.REVCroptopAllowedPost.md +28 -0
  39. package/docs/src/src/structs/REVDeploy721TiersHookConfig.sol/struct.REVDeploy721TiersHookConfig.md +34 -0
  40. package/docs/src/src/structs/REVDescription.sol/struct.REVDescription.md +23 -0
  41. package/docs/src/src/structs/REVLoan.sol/struct.REVLoan.md +28 -0
  42. package/docs/src/src/structs/REVLoanSource.sol/struct.REVLoanSource.md +16 -0
  43. package/docs/src/src/structs/REVStageConfig.sol/struct.REVStageConfig.md +44 -0
  44. package/docs/src/src/structs/REVSuckerDeploymentConfig.sol/struct.REVSuckerDeploymentConfig.md +16 -0
  45. package/foundry.lock +11 -0
  46. package/foundry.toml +23 -0
  47. package/package.json +31 -0
  48. package/remappings.txt +1 -0
  49. package/script/Deploy.s.sol +350 -0
  50. package/script/helpers/RevnetCoreDeploymentLib.sol +72 -0
  51. package/slither-ci.config.json +10 -0
  52. package/sphinx.lock +507 -0
  53. package/src/REVDeployer.sol +1257 -0
  54. package/src/REVLoans.sol +1333 -0
  55. package/src/interfaces/IREVDeployer.sol +198 -0
  56. package/src/interfaces/IREVLoans.sol +241 -0
  57. package/src/structs/REVAutoIssuance.sol +11 -0
  58. package/src/structs/REVConfig.sol +17 -0
  59. package/src/structs/REVCroptopAllowedPost.sol +20 -0
  60. package/src/structs/REVDeploy721TiersHookConfig.sol +25 -0
  61. package/src/structs/REVDescription.sol +14 -0
  62. package/src/structs/REVLoan.sol +19 -0
  63. package/src/structs/REVLoanSource.sol +11 -0
  64. package/src/structs/REVStageConfig.sol +34 -0
  65. package/src/structs/REVSuckerDeploymentConfig.sol +11 -0
  66. package/test/REV.integrations.t.sol +420 -0
  67. package/test/REVAutoIssuanceFuzz.t.sol +276 -0
  68. package/test/REVDeployerAuditRegressions.t.sol +328 -0
  69. package/test/REVInvincibility.t.sol +1275 -0
  70. package/test/REVInvincibilityHandler.sol +357 -0
  71. package/test/REVLifecycle.t.sol +364 -0
  72. package/test/REVLoans.invariants.t.sol +642 -0
  73. package/test/REVLoansAttacks.t.sol +739 -0
  74. package/test/REVLoansAuditRegressions.t.sol +314 -0
  75. package/test/REVLoansFeeRecovery.t.sol +704 -0
  76. package/test/REVLoansSourced.t.sol +1732 -0
  77. package/test/REVLoansUnSourced.t.sol +331 -0
  78. package/test/TestPR09_ConversionDocumentation.t.sol +304 -0
  79. package/test/TestPR10_LiquidationBehavior.t.sol +340 -0
  80. package/test/TestPR11_LowFindings.t.sol +571 -0
  81. package/test/TestPR12_FlashLoanSurplus.t.sol +305 -0
  82. package/test/TestPR13_CrossSourceReallocation.t.sol +302 -0
  83. package/test/TestPR15_CashOutCallerValidation.t.sol +320 -0
  84. package/test/TestPR16_ZeroRepayment.t.sol +297 -0
  85. package/test/TestPR21_Uint112Overflow.t.sol +251 -0
  86. package/test/TestPR22_HookArrayOOB.t.sol +221 -0
  87. package/test/TestPR26_BurnHeldTokens.t.sol +331 -0
  88. package/test/TestPR27_CEIPattern.t.sol +448 -0
  89. package/test/TestPR29_SwapTerminalPermission.t.sol +206 -0
  90. package/test/TestPR32_MixedFixes.t.sol +529 -0
  91. package/test/helpers/MaliciousContracts.sol +233 -0
  92. package/test/mock/MockBuybackDataHook.sol +61 -0
@@ -0,0 +1,1732 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.23;
3
+
4
+ import "forge-std/Test.sol";
5
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+ import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
7
+ import /* {*} from */ "./../src/REVDeployer.sol";
8
+ import "@croptop/core-v6/src/CTPublisher.sol";
9
+ import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
10
+
11
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
12
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
13
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
14
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
15
+ import "@bananapus/swap-terminal-v6/script/helpers/SwapTerminalDeploymentLib.sol";
16
+
17
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
18
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
19
+ import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
20
+ import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
21
+ import {REVLoans} from "../src/REVLoans.sol";
22
+ import {REVLoan} from "../src/structs/REVLoan.sol";
23
+ import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
24
+ import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
25
+ import {REVDescription} from "../src/structs/REVDescription.sol";
26
+ import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
27
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
28
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
29
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
30
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
31
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
32
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
33
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
34
+
35
+ struct FeeProjectConfig {
36
+ REVConfig configuration;
37
+ JBTerminalConfig[] terminalConfigurations;
38
+ REVSuckerDeploymentConfig suckerDeploymentConfiguration;
39
+ }
40
+
41
+ contract REVLoansSourcedTests is TestBaseWorkflow, JBTest {
42
+ /// @notice the salts that are used to deploy the contracts.
43
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
44
+ bytes32 ERC20_SALT = "REV_TOKEN";
45
+
46
+ REVDeployer REV_DEPLOYER;
47
+ JB721TiersHook EXAMPLE_HOOK;
48
+
49
+ /// @notice Deploys tiered ERC-721 hooks for revnets.
50
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
51
+ IJB721TiersHookStore HOOK_STORE;
52
+ IJBAddressRegistry ADDRESS_REGISTRY;
53
+
54
+ IREVLoans LOANS_CONTRACT;
55
+
56
+ MockERC20 TOKEN;
57
+
58
+ /// @notice Deploys and tracks suckers for revnets.
59
+ IJBSuckerRegistry SUCKER_REGISTRY;
60
+
61
+ CTPublisher PUBLISHER;
62
+ MockBuybackDataHook MOCK_BUYBACK;
63
+
64
+ uint256 FEE_PROJECT_ID;
65
+ uint256 REVNET_ID;
66
+
67
+ address USER = makeAddr("user");
68
+
69
+ /// @notice The address that is allowed to forward calls.
70
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
71
+
72
+ function getFeeProjectConfig() internal view returns (FeeProjectConfig memory) {
73
+ // Define constants
74
+ string memory name = "Revnet";
75
+ string memory symbol = "$REV";
76
+ string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx";
77
+ uint8 decimals = 18;
78
+ uint256 decimalMultiplier = 10 ** decimals;
79
+
80
+ // The tokens that the project accepts and stores.
81
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
82
+
83
+ // Accept the chain's native currency through the multi terminal.
84
+ accountingContextsToAccept[0] = JBAccountingContext({
85
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
86
+ });
87
+
88
+ // For the tests we need to allow these payments, otherwise other revnets can't pay a fee.
89
+ // IRL, this would be handled by a swap terminal.
90
+ accountingContextsToAccept[1] =
91
+ JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
92
+
93
+ // The terminals that the project will accept funds through.
94
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
95
+ terminalConfigurations[0] =
96
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
97
+
98
+ // The project's revnet stage configurations.
99
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](3);
100
+
101
+ JBSplit[] memory splits = new JBSplit[](1);
102
+ splits[0].beneficiary = payable(multisig());
103
+ splits[0].percent = 10_000;
104
+
105
+ {
106
+ REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
107
+ issuanceConfs[0] = REVAutoIssuance({
108
+ chainId: uint32(block.chainid), count: uint104(70_000 * decimalMultiplier), beneficiary: multisig()
109
+ });
110
+
111
+ stageConfigurations[0] = REVStageConfig({
112
+ startsAtOrAfter: uint40(block.timestamp),
113
+ autoIssuances: issuanceConfs,
114
+ splitPercent: 2000, // 20%
115
+ splits: splits,
116
+ initialIssuance: uint112(1000 * decimalMultiplier),
117
+ issuanceCutFrequency: 90 days,
118
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
119
+ cashOutTaxRate: 6000, // 0.6
120
+ extraMetadata: 0
121
+ });
122
+ }
123
+
124
+ stageConfigurations[1] = REVStageConfig({
125
+ startsAtOrAfter: uint40(stageConfigurations[0].startsAtOrAfter + 720 days),
126
+ autoIssuances: new REVAutoIssuance[](0),
127
+ splitPercent: 2000, // 20%
128
+ initialIssuance: 0, // inherit from previous cycle.
129
+ splits: splits,
130
+ issuanceCutFrequency: 180 days,
131
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
132
+ cashOutTaxRate: 1000, // 0.1
133
+ extraMetadata: 0
134
+ });
135
+
136
+ stageConfigurations[2] = REVStageConfig({
137
+ startsAtOrAfter: uint40(stageConfigurations[1].startsAtOrAfter + (20 * 365 days)),
138
+ autoIssuances: new REVAutoIssuance[](0),
139
+ splitPercent: 0,
140
+ initialIssuance: 1,
141
+ splits: splits,
142
+ issuanceCutFrequency: 0,
143
+ issuanceCutPercent: 0,
144
+ cashOutTaxRate: 6000, // 0.6
145
+ extraMetadata: 0
146
+ });
147
+
148
+ // The project's revnet configuration
149
+ REVConfig memory revnetConfiguration = REVConfig({
150
+ description: REVDescription(name, symbol, projectUri, ERC20_SALT),
151
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
152
+ splitOperator: multisig(),
153
+ stageConfigurations: stageConfigurations
154
+ });
155
+
156
+ return FeeProjectConfig({
157
+ configuration: revnetConfiguration,
158
+ terminalConfigurations: terminalConfigurations,
159
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
160
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
161
+ })
162
+ });
163
+ }
164
+
165
+ function getSecondProjectConfig() internal view returns (FeeProjectConfig memory) {
166
+ // Define constants
167
+ string memory name = "NANA";
168
+ string memory symbol = "$NANA";
169
+ string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx";
170
+ uint8 decimals = 18;
171
+ uint256 decimalMultiplier = 10 ** decimals;
172
+
173
+ // The tokens that the project accepts and stores.
174
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
175
+
176
+ // Accept the chain's native currency through the multi terminal.
177
+ accountingContextsToAccept[0] = JBAccountingContext({
178
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
179
+ });
180
+
181
+ accountingContextsToAccept[1] =
182
+ JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
183
+
184
+ // The terminals that the project will accept funds through.
185
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
186
+ terminalConfigurations[0] =
187
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
188
+
189
+ JBSplit[] memory splits = new JBSplit[](1);
190
+ splits[0].beneficiary = payable(multisig());
191
+ splits[0].percent = 10_000;
192
+
193
+ // The project's revnet stage configurations.
194
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](3);
195
+
196
+ {
197
+ REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
198
+ issuanceConfs[0] = REVAutoIssuance({
199
+ chainId: uint32(block.chainid), count: uint104(70_000 * decimalMultiplier), beneficiary: multisig()
200
+ });
201
+
202
+ stageConfigurations[0] = REVStageConfig({
203
+ startsAtOrAfter: uint40(block.timestamp),
204
+ autoIssuances: issuanceConfs,
205
+ splitPercent: 2000, // 20%
206
+ splits: splits,
207
+ initialIssuance: uint112(1000 * decimalMultiplier),
208
+ issuanceCutFrequency: 90 days,
209
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
210
+ cashOutTaxRate: 0,
211
+ extraMetadata: 0
212
+ });
213
+ }
214
+
215
+ stageConfigurations[1] = REVStageConfig({
216
+ startsAtOrAfter: uint40(stageConfigurations[0].startsAtOrAfter + 720 days),
217
+ autoIssuances: new REVAutoIssuance[](0),
218
+ splitPercent: 2000, // 20%
219
+ splits: splits,
220
+ initialIssuance: 0, // inherit from previous cycle.
221
+ issuanceCutFrequency: 180 days,
222
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
223
+ cashOutTaxRate: 0,
224
+ extraMetadata: 0
225
+ });
226
+
227
+ stageConfigurations[2] = REVStageConfig({
228
+ startsAtOrAfter: uint40(stageConfigurations[1].startsAtOrAfter + (20 * 365 days)),
229
+ autoIssuances: new REVAutoIssuance[](0),
230
+ splitPercent: 0,
231
+ splits: splits,
232
+ initialIssuance: 1, // this is a special number that is as close to max price as we can get.
233
+ issuanceCutFrequency: 0,
234
+ issuanceCutPercent: 0,
235
+ cashOutTaxRate: 0,
236
+ extraMetadata: 0
237
+ });
238
+
239
+ // The project's revnet configuration
240
+ REVConfig memory revnetConfiguration = REVConfig({
241
+ description: REVDescription(name, symbol, projectUri, "NANA_TOKEN"),
242
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
243
+ splitOperator: multisig(),
244
+ stageConfigurations: stageConfigurations
245
+ });
246
+
247
+ return FeeProjectConfig({
248
+ configuration: revnetConfiguration,
249
+ terminalConfigurations: terminalConfigurations,
250
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
251
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
252
+ })
253
+ });
254
+ }
255
+
256
+ function setUp() public override {
257
+ super.setUp();
258
+
259
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
260
+
261
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
262
+
263
+ HOOK_STORE = new JB721TiersHookStore();
264
+
265
+ EXAMPLE_HOOK = new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, multisig());
266
+
267
+ ADDRESS_REGISTRY = new JBAddressRegistry();
268
+
269
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
270
+
271
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
272
+ MOCK_BUYBACK = new MockBuybackDataHook();
273
+
274
+ TOKEN = new MockERC20("1/2 ETH", "1/2");
275
+
276
+ // Configure a price feed for ETH/TOKEN.
277
+ // The token is worth 50% of the price of ETH.
278
+ MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
279
+ vm.label(address(priceFeed), "Token:Eth/PriceFeed");
280
+
281
+ // Configure the price feed for the pair.
282
+ vm.prank(multisig());
283
+ jbPrices()
284
+ .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
285
+
286
+ LOANS_CONTRACT = new REVLoans({
287
+ controller: jbController(),
288
+ projects: jbProjects(),
289
+ revId: FEE_PROJECT_ID,
290
+ owner: address(this),
291
+ permit2: permit2(),
292
+ trustedForwarder: TRUSTED_FORWARDER
293
+ });
294
+
295
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
296
+ jbController(),
297
+ SUCKER_REGISTRY,
298
+ FEE_PROJECT_ID,
299
+ HOOK_DEPLOYER,
300
+ PUBLISHER,
301
+ IJBRulesetDataHook(address(MOCK_BUYBACK)),
302
+ address(LOANS_CONTRACT),
303
+ TRUSTED_FORWARDER
304
+ );
305
+
306
+ // Approve the basic deployer to configure the project.
307
+ vm.prank(address(multisig()));
308
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
309
+
310
+ // Build the config.
311
+ FeeProjectConfig memory feeProjectConfig = getFeeProjectConfig();
312
+
313
+ vm.prank(address(multisig()));
314
+ // Configure the project.
315
+ REV_DEPLOYER.deployFor({
316
+ revnetId: FEE_PROJECT_ID, // Zero to deploy a new revnet
317
+ configuration: feeProjectConfig.configuration,
318
+ terminalConfigurations: feeProjectConfig.terminalConfigurations,
319
+ suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration
320
+ });
321
+
322
+ // Configure second revnet
323
+ FeeProjectConfig memory fee2Config = getSecondProjectConfig();
324
+
325
+ // Configure the project.
326
+ REVNET_ID = REV_DEPLOYER.deployFor({
327
+ revnetId: 0, // Zero to deploy a new revnet
328
+ configuration: fee2Config.configuration,
329
+ terminalConfigurations: fee2Config.terminalConfigurations,
330
+ suckerDeploymentConfiguration: fee2Config.suckerDeploymentConfiguration
331
+ });
332
+
333
+ // Give Eth for the user experience
334
+ vm.deal(USER, 100e18);
335
+ }
336
+
337
+ function test_Borrow_Duration(uint256 payableAmount) public {
338
+ vm.assume(payableAmount > 0 && payableAmount <= type(uint112).max);
339
+
340
+ // Upfront fee plus another month (25 min + 4)
341
+ uint256 prepaidFee = 29;
342
+
343
+ // Calculate the duration based upon the prepaidFee.
344
+ uint32 duration = uint32(mulDiv(3650 days, prepaidFee, LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()));
345
+
346
+ // Calculate the duration with a minimum prepaidFee.
347
+ uint32 minDuration = uint32(mulDiv(3650 days, 25, LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()));
348
+
349
+ // Deal the user some tokens.
350
+ deal(address(TOKEN), USER, payableAmount);
351
+
352
+ // Approve the terminal to spend the tokens.
353
+ vm.prank(USER);
354
+ TOKEN.approve(address(jbMultiTerminal()), payableAmount);
355
+
356
+ vm.prank(USER);
357
+ uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payableAmount, USER, 0, "", "");
358
+
359
+ uint256 loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 6, uint32(uint160(address(TOKEN))));
360
+ // If there is no loanable amount, we can't continue.
361
+ vm.assume(loanable > 0);
362
+
363
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
364
+ mockExpect(
365
+ address(jbPermissions()),
366
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
367
+ abi.encode(true)
368
+ );
369
+
370
+ REVLoanSource memory sauce = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
371
+
372
+ vm.prank(USER);
373
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), prepaidFee);
374
+
375
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
376
+ assertEq(loan.amount, loanable);
377
+ assertEq(loan.collateral, tokens);
378
+ assertEq(loan.createdAt, block.timestamp);
379
+ assertEq(loan.prepaidFeePercent, prepaidFee);
380
+ assertEq(loan.prepaidDuration, duration);
381
+ assertEq(loan.source.token, address(TOKEN));
382
+ assertEq(address(loan.source.terminal), address(jbMultiTerminal()));
383
+
384
+ // If we remove the minimum prepaid duration, we can see that we're still +- a day.
385
+ assertApproxEqAbs(loan.prepaidDuration - minDuration, 30 days, 1 days);
386
+
387
+ // Looks like its +- 2 days approx with this config.
388
+ // This is 6 months (25 as prepaidFee + 4 as a month extra prepaid).
389
+ assertApproxEqAbs(loan.prepaidDuration, 210 days, 2 days);
390
+ }
391
+
392
+ function test_Borrow_Duration_Max(uint256 payableAmount) public {
393
+ vm.assume(payableAmount > 0 && payableAmount <= type(uint112).max);
394
+
395
+ // Upfront fee plus another month
396
+ uint256 prepaidFee = 500;
397
+
398
+ // Calculate the duration based upon the prepaidFee.
399
+ uint32 duration = uint32(mulDiv(3650 days, prepaidFee, LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()));
400
+
401
+ // Deal the user some tokens.
402
+ deal(address(TOKEN), USER, payableAmount);
403
+
404
+ // Approve the terminal to spend the tokens.
405
+ vm.prank(USER);
406
+ TOKEN.approve(address(jbMultiTerminal()), payableAmount);
407
+
408
+ vm.prank(USER);
409
+ uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payableAmount, USER, 0, "", "");
410
+
411
+ uint256 loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 6, uint32(uint160(address(TOKEN))));
412
+ // If there is no loanable amount, we can't continue.
413
+ vm.assume(loanable > 0);
414
+
415
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
416
+ mockExpect(
417
+ address(jbPermissions()),
418
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
419
+ abi.encode(true)
420
+ );
421
+
422
+ REVLoanSource memory sauce = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
423
+
424
+ vm.prank(USER);
425
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), prepaidFee);
426
+
427
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
428
+ assertEq(loan.amount, loanable);
429
+ assertEq(loan.collateral, tokens);
430
+ assertEq(loan.createdAt, block.timestamp);
431
+ assertEq(loan.prepaidFeePercent, prepaidFee);
432
+ assertEq(loan.prepaidDuration, duration);
433
+ assertEq(loan.source.token, address(TOKEN));
434
+ assertEq(address(loan.source.terminal), address(jbMultiTerminal()));
435
+
436
+ // Max duration is correct at ten years.
437
+ assertEq(loan.prepaidDuration, 3650 days);
438
+ }
439
+
440
+ function test_Borrow_Duration_WorstCase_Repay(uint256 payableAmount) public {
441
+ // Seems like something in our test logic/math is incorrect.
442
+ _borrowAndRepay(LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT() - 1, payableAmount, 999);
443
+ }
444
+
445
+ function test_Borrow_Duration_MinPrepaid_MaxDuration_Repay(uint256 payableAmount) public {
446
+ // We prepay the minimum fee.
447
+ _borrowAndRepay(LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT(), payableAmount, 1000);
448
+ }
449
+
450
+ function test_Borrow_Duration_MaxPrepaid_MaxDuration_Repay(uint256 payableAmount) public {
451
+ // All fees are paid upfront, so there is no additional fee.
452
+ _borrowAndRepay(LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT(), payableAmount, 0);
453
+ }
454
+
455
+ function _borrowAndRepay(uint256 prepaidFee, uint256 payableAmount, uint16 expectedFeePercent) internal {
456
+ vm.assume(payableAmount > 1 gwei && payableAmount <= type(uint96).max);
457
+ vm.startPrank(USER);
458
+
459
+ // Deal the user some tokens.
460
+ deal(address(TOKEN), USER, payableAmount * 3);
461
+
462
+ // Approve the terminal to spend the tokens.
463
+ TOKEN.approve(address(jbMultiTerminal()), payableAmount);
464
+
465
+ uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payableAmount, USER, 0, "", "");
466
+ uint256 loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 6, uint32(uint160(address(TOKEN))));
467
+
468
+ // If there is no loanable amount, we can't continue.
469
+ vm.assume(loanable > 0);
470
+
471
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
472
+ mockExpect(
473
+ address(jbPermissions()),
474
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
475
+ abi.encode(true)
476
+ );
477
+
478
+ REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
479
+
480
+ // Get the balance before we receive the loan.
481
+ uint256 balanceBeforeLoan = TOKEN.balanceOf(USER);
482
+
483
+ // Create the new loan.
484
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, loanable, tokens, payable(USER), prepaidFee);
485
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
486
+
487
+ // Check what amount we actually received.
488
+ uint256 receivedFromLoan = TOKEN.balanceOf(USER) - balanceBeforeLoan;
489
+
490
+ // Check that we prepaid the expected percentage.
491
+ {
492
+ uint256 otherFees = LOANS_CONTRACT.REV_PREPAID_FEE_PERCENT() + 25; // 25 is jb protocol fee.
493
+ assertApproxEqAbs(loanable * (1000 - otherFees - prepaidFee) / 1000, receivedFromLoan, 10);
494
+ }
495
+
496
+ // Forward time to right before the loan reaches liquidation.
497
+ // Use 3650 days - 1 because liquidation triggers at >= LOAN_LIQUIDATION_DURATION.
498
+ vm.warp(block.timestamp + 3650 days - 1);
499
+
500
+ // Repay the loan.
501
+ uint256 balanceBefore = TOKEN.balanceOf(USER);
502
+ {
503
+ JBSingleAllowance memory allowance;
504
+ TOKEN.approve(address(LOANS_CONTRACT), type(uint256).max);
505
+ LOANS_CONTRACT.repayLoan(newLoanId, loan.amount * 2, loan.collateral, payable(USER), allowance);
506
+ }
507
+
508
+ // Track what amount we end up paying.
509
+ uint256 amountPaid = balanceBefore - TOKEN.balanceOf(USER);
510
+
511
+ // We expect the fee to be 100% for the min prepaid with the max duration.
512
+ uint256 expectedFee = (loan.amount * (1000 - prepaidFee) / 1000) * expectedFeePercent / 1000;
513
+
514
+ // The fee may deviate 1%.
515
+ assertApproxEqRel(amountPaid, loan.amount + expectedFee, 0.001 ether);
516
+
517
+ vm.stopPrank();
518
+ }
519
+
520
+ function test_Pay_ERC20_Borrow_With_Loan_Source(uint256 payableAmount, uint32 prepaidFee) public {
521
+ payableAmount = bound(payableAmount, 1e6, type(uint112).max);
522
+ vm.assume(
523
+ LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT() <= prepaidFee
524
+ && prepaidFee <= LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()
525
+ );
526
+
527
+ // Calculate the duration based upon the prepaidFee.
528
+ uint32 duration = uint32(mulDiv(3650 days, prepaidFee, LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()));
529
+
530
+ // Deal the user some tokens.
531
+ deal(address(TOKEN), USER, payableAmount);
532
+
533
+ // Approve the terminal to spend the tokens.
534
+ vm.prank(USER);
535
+ TOKEN.approve(address(jbMultiTerminal()), payableAmount);
536
+
537
+ vm.prank(USER);
538
+ uint256 tokens = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), payableAmount, USER, 0, "", "");
539
+
540
+ uint256 loanable = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 6, uint32(uint160(address(TOKEN))));
541
+ // If there is no loanable amount, we can't continue.
542
+ vm.assume(loanable > 0);
543
+
544
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
545
+ mockExpect(
546
+ address(jbPermissions()),
547
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
548
+ abi.encode(true)
549
+ );
550
+
551
+ REVLoanSource memory sauce = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
552
+
553
+ vm.prank(USER);
554
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), prepaidFee);
555
+
556
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
557
+ assertEq(loan.amount, loanable);
558
+ assertEq(loan.collateral, tokens);
559
+ assertEq(loan.createdAt, block.timestamp);
560
+ assertEq(loan.prepaidFeePercent, prepaidFee);
561
+ assertEq(loan.prepaidDuration, duration);
562
+ assertEq(loan.source.token, address(TOKEN));
563
+ assertEq(address(loan.source.terminal), address(jbMultiTerminal()));
564
+
565
+ // Ensure loans contract isn't hodling
566
+ assertEq(TOKEN.balanceOf(address(LOANS_CONTRACT)), 0);
567
+
568
+ // The fees to be paid to NANA.
569
+ uint256 allowance_fees = JBFees.feeAmountFrom({amountBeforeFee: loanable, feePercent: jbMultiTerminal().FEE()});
570
+ // The fees to be paid to REV.
571
+ uint256 rev_fees =
572
+ JBFees.feeAmountFrom({amountBeforeFee: loanable, feePercent: LOANS_CONTRACT.REV_PREPAID_FEE_PERCENT()});
573
+ // The fees to be paid to the Project we are taking a loan from.
574
+ uint256 source_fees = JBFees.feeAmountFrom({amountBeforeFee: loanable, feePercent: prepaidFee});
575
+ uint256 fees = allowance_fees + rev_fees + source_fees;
576
+
577
+ // Ensure we actually received the token from the borrow
578
+ // Subtract the fee for REV and for the source revnet.
579
+ assertEq(TOKEN.balanceOf(address(USER)), loanable - fees);
580
+ }
581
+
582
+ function test_Cashout(
583
+ bool useNative,
584
+ uint104 autoIssuance,
585
+ uint256 totalSupplyExcludingAutoMint,
586
+ uint256 nativeSurplus,
587
+ uint256 tokensToCashout,
588
+ uint16 cashOutTaxRate
589
+ )
590
+ public
591
+ {
592
+ // Since we don't actually mint the autoIssuance tokens, we don't have to worry about it exceeding the
593
+ // `SafeSupply`.
594
+ vm.assume(cashOutTaxRate <= JBConstants.MAX_FEE);
595
+ vm.assume(totalSupplyExcludingAutoMint > 0 && totalSupplyExcludingAutoMint <= type(uint208).max);
596
+ vm.assume(nativeSurplus <= type(uint104).max);
597
+ vm.assume(totalSupplyExcludingAutoMint > tokensToCashout);
598
+
599
+ address token = useNative ? JBConstants.NATIVE_TOKEN : address(TOKEN);
600
+
601
+ // Deploy a new REVNET, that has multiple stages where the fee decrease.
602
+ // This lets people refinance their loans to get a better rate.
603
+ uint256 revnetProjectId;
604
+ {
605
+ FeeProjectConfig memory projectConfig = getSecondProjectConfig();
606
+ REVAutoIssuance[] memory issuanceConfs;
607
+ issuanceConfs = new REVAutoIssuance[](1);
608
+ issuanceConfs[0] = REVAutoIssuance({
609
+ chainId: uint32(block.chainid), count: uint104(autoIssuance), beneficiary: multisig()
610
+ });
611
+
612
+ JBSplit[] memory splits = new JBSplit[](1);
613
+ splits[0].beneficiary = payable(multisig());
614
+ splits[0].percent = 10_000;
615
+
616
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
617
+ stageConfigurations[0] = REVStageConfig({
618
+ startsAtOrAfter: uint40(block.timestamp),
619
+ autoIssuances: issuanceConfs,
620
+ splitPercent: 2000, // 20%
621
+ splits: splits,
622
+ initialIssuance: 1000e18,
623
+ issuanceCutFrequency: 90 days,
624
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
625
+ cashOutTaxRate: cashOutTaxRate, // 20%
626
+ extraMetadata: 0
627
+ });
628
+
629
+ // Replace the configuration.
630
+ projectConfig.configuration.stageConfigurations = stageConfigurations;
631
+ projectConfig.configuration.description.salt = "FeeChange";
632
+
633
+ revnetProjectId = REV_DEPLOYER.deployFor({
634
+ revnetId: 0, // Zero to deploy a new revnet
635
+ configuration: projectConfig.configuration,
636
+ terminalConfigurations: projectConfig.terminalConfigurations,
637
+ suckerDeploymentConfiguration: projectConfig.suckerDeploymentConfiguration
638
+ });
639
+ }
640
+
641
+ // Add the surplus into the project.
642
+ if (useNative) {
643
+ vm.deal(USER, nativeSurplus);
644
+ } else {
645
+ deal(address(TOKEN), USER, nativeSurplus);
646
+
647
+ // Give allowance to spend our tokens.
648
+ vm.prank(USER);
649
+ TOKEN.approve(address(jbMultiTerminal()), nativeSurplus);
650
+ }
651
+
652
+ vm.prank(USER);
653
+ jbMultiTerminal().addToBalanceOf{value: useNative ? nativeSurplus : 0}(
654
+ revnetProjectId, token, nativeSurplus, false, string(""), bytes("")
655
+ );
656
+
657
+ // Mint the entire supply excluding automint to the user.
658
+ vm.prank(address(jbController()));
659
+ jbTokens().mintFor(USER, revnetProjectId, totalSupplyExcludingAutoMint);
660
+
661
+ // Check what a borrow would result in more.
662
+ uint256 loanable = LOANS_CONTRACT.borrowableAmountFrom(
663
+ revnetProjectId, tokensToCashout, useNative ? 18 : 6, uint32(uint160(token))
664
+ );
665
+
666
+ uint256 fullReclaimableSurplus = jbMultiTerminal().STORE()
667
+ .currentReclaimableSurplusOf({
668
+ projectId: revnetProjectId,
669
+ cashOutCount: tokensToCashout,
670
+ totalSupply: totalSupplyExcludingAutoMint,
671
+ surplus: nativeSurplus
672
+ });
673
+
674
+ assertGe(fullReclaimableSurplus, loanable);
675
+
676
+ uint256 feeTokenCount =
677
+ cashOutTaxRate == 0 ? 0 : mulDiv(tokensToCashout, jbMultiTerminal().FEE(), JBConstants.MAX_FEE);
678
+
679
+ uint256 reclaimableSurplus = jbMultiTerminal().STORE()
680
+ .currentReclaimableSurplusOf({
681
+ projectId: revnetProjectId,
682
+ cashOutCount: tokensToCashout - feeTokenCount,
683
+ totalSupply: totalSupplyExcludingAutoMint,
684
+ surplus: nativeSurplus
685
+ });
686
+
687
+ // In the `revFee` calculation we decrease the `nativeSurplus` by the `reclaimableSurplus`
688
+ // but due to a `stack too deep` we can't do that there, so we decrease it here.
689
+ // This is not the correct value for this variable, however in `revFee` is the last time we use this variable.
690
+ nativeSurplus -= reclaimableSurplus;
691
+
692
+ uint256 revFee = jbMultiTerminal().STORE()
693
+ .currentReclaimableSurplusOf({
694
+ projectId: revnetProjectId,
695
+ cashOutCount: feeTokenCount,
696
+ totalSupply: totalSupplyExcludingAutoMint - (tokensToCashout - feeTokenCount),
697
+ surplus: nativeSurplus
698
+ });
699
+
700
+ assertGe(fullReclaimableSurplus, mulDiv((reclaimableSurplus + revFee), 995, 1000)); // small marging for curve
701
+ // rounding.
702
+
703
+ uint256 balanceBefore = _balanceOf(token, USER);
704
+
705
+ // Ensure that the hook was called.
706
+ vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.beforeCashOutRecordedWith.selector));
707
+
708
+ // It only adds itself as a `after` cashoutHook if there is a cashout tax rate.
709
+ if (cashOutTaxRate > 0) {
710
+ vm.expectCall(address(REV_DEPLOYER), abi.encode(REVDeployer.afterCashOutRecordedWith.selector));
711
+ }
712
+
713
+ // Perform a cashout.
714
+ vm.prank(USER);
715
+ jbMultiTerminal().cashOutTokensOf(USER, revnetProjectId, tokensToCashout, token, 0, payable(USER), bytes(""));
716
+
717
+ // Make sure the contracts do not accidentally hold any tokens.
718
+ assertEq(_balanceOf(token, address(REV_DEPLOYER)), 0);
719
+ assertEq(_balanceOf(token, address(LOANS_CONTRACT)), 0);
720
+
721
+ // make sure the user has received tokens.
722
+ assertGe(_balanceOf(token, USER), balanceBefore);
723
+
724
+ uint256 balance = _balanceOf(token, USER) - balanceBefore;
725
+ uint256 nanaFee = cashOutTaxRate == 0
726
+ ? 0
727
+ : JBFees.feeAmountResultingIn({amountAfterFee: balance, feePercent: jbMultiTerminal().FEE()});
728
+
729
+ assertApproxEqAbs(balance, reclaimableSurplus - nanaFee, 1);
730
+
731
+ assertGe(reclaimableSurplus + revFee, mulDiv(loanable, 97, 100)); // small marging for curve rounding.
732
+ }
733
+
734
+ function test_Pay_Borrow_With_Loan_Source() public {
735
+ vm.prank(USER);
736
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
737
+
738
+ uint256 loanable =
739
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
740
+ assertGt(loanable, 0);
741
+
742
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
743
+ mockExpect(
744
+ address(jbPermissions()),
745
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
746
+ abi.encode(true)
747
+ );
748
+
749
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
750
+
751
+ // Check the balance of the user before the borrow.
752
+ uint256 balanceBefore = USER.balance;
753
+
754
+ vm.prank(USER);
755
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
756
+
757
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
758
+ assertEq(loan.amount, loanable);
759
+ assertEq(loan.collateral, tokens);
760
+ assertEq(loan.createdAt, block.timestamp);
761
+ assertEq(loan.prepaidFeePercent, 500);
762
+ assertEq(loan.prepaidDuration, mulDiv(500, 3650 days, 500));
763
+ assertEq(loan.source.token, JBConstants.NATIVE_TOKEN);
764
+ assertEq(address(loan.source.terminal), address(jbMultiTerminal()));
765
+
766
+ // Ensure loans contract isn't hodling
767
+ assertEq(address(LOANS_CONTRACT).balance, 0);
768
+
769
+ // Ensure we actually received ETH from the borrow
770
+ assertGt(USER.balance - balanceBefore, 0);
771
+ }
772
+
773
+ function testFuzz_Pay_Borrow_PayOff_With_Loan_Source(
774
+ uint256 percentOfCollateralToRemove,
775
+ uint256 prepaidFeePercent,
776
+ uint256 daysToWarp
777
+ )
778
+ public
779
+ {
780
+ ///
781
+ percentOfCollateralToRemove = bound(percentOfCollateralToRemove, 0, 10_000);
782
+ prepaidFeePercent = bound(prepaidFeePercent, 25, 500);
783
+ daysToWarp = bound(daysToWarp, 0, 3649);
784
+
785
+ daysToWarp = daysToWarp * 1 days;
786
+
787
+ vm.prank(USER);
788
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
789
+
790
+ uint256 loanable =
791
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
792
+
793
+ assertGt(loanable, 0);
794
+
795
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
796
+ mockExpect(
797
+ address(jbPermissions()),
798
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
799
+ abi.encode(true)
800
+ );
801
+
802
+ uint256 newLoanId;
803
+
804
+ {
805
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
806
+
807
+ vm.prank(USER);
808
+ (newLoanId,) =
809
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), prepaidFeePercent);
810
+ }
811
+
812
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
813
+
814
+ assertEq(loan.amount, loanable);
815
+ assertEq(loan.collateral, tokens);
816
+ assertEq(loan.createdAt, block.timestamp);
817
+ assertEq(loan.prepaidFeePercent, prepaidFeePercent);
818
+ assertEq(loan.prepaidDuration, mulDiv(prepaidFeePercent, 3650 days, 500));
819
+ assertEq(loan.source.token, JBConstants.NATIVE_TOKEN);
820
+ assertEq(address(loan.source.terminal), address(jbMultiTerminal()));
821
+
822
+ // warp forward
823
+ vm.warp(block.timestamp + daysToWarp);
824
+
825
+ uint256 collateralReturned = mulDiv(loan.collateral, percentOfCollateralToRemove, 10_000);
826
+
827
+ uint256 newCollateral = loan.collateral - collateralReturned;
828
+ uint256 borrowableFromNewCollateral = LOANS_CONTRACT.borrowableAmountFrom(
829
+ REVNET_ID, newCollateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
830
+ );
831
+
832
+ uint256 amountDiff = borrowableFromNewCollateral > loan.amount ? 0 : loan.amount - borrowableFromNewCollateral;
833
+
834
+ // Skip fuzz runs where both repay amount and collateral return are zero (M-27 fix rejects these).
835
+ vm.assume(amountDiff > 0 || collateralReturned > 0);
836
+
837
+ uint256 maxAmountPaidDown = loan.amount;
838
+
839
+ // Calculate the fee.
840
+ {
841
+ // Keep a reference to the time since the loan was created.
842
+ uint256 timeSinceLoanCreated = block.timestamp - loan.createdAt;
843
+
844
+ // If the loan period has passed the prepaid time frame, take a fee.
845
+ if (timeSinceLoanCreated > loan.prepaidDuration) {
846
+ // Calculate the prepaid fee for the amount being paid back.
847
+ uint256 prepaidAmount =
848
+ JBFees.feeAmountFrom({amountBeforeFee: amountDiff, feePercent: loan.prepaidFeePercent});
849
+
850
+ // Calculate the fee as a linear proportion given the amount of time that has passed.
851
+ // sourceFeeAmount = mulDiv(amount, timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION) - prepaidAmount;
852
+ maxAmountPaidDown += JBFees.feeAmountFrom({
853
+ amountBeforeFee: amountDiff - prepaidAmount,
854
+ feePercent: mulDiv(timeSinceLoanCreated, JBConstants.MAX_FEE, 3650 days)
855
+ });
856
+ }
857
+ }
858
+
859
+ // ensure we have the balance
860
+ vm.deal(USER, maxAmountPaidDown);
861
+
862
+ // empty allowance data
863
+ JBSingleAllowance memory allowance;
864
+
865
+ if (borrowableFromNewCollateral > loan.amount) {
866
+ vm.expectRevert(
867
+ abi.encodeWithSelector(
868
+ REVLoans.REVLoans_NewBorrowAmountGreaterThanLoanAmount.selector,
869
+ borrowableFromNewCollateral,
870
+ loan.amount
871
+ )
872
+ );
873
+ }
874
+
875
+ // call to pay-down the loan
876
+ vm.prank(USER);
877
+ (, REVLoan memory reducedLoan) = LOANS_CONTRACT.repayLoan{value: maxAmountPaidDown}(
878
+ newLoanId, maxAmountPaidDown, collateralReturned, payable(USER), allowance
879
+ );
880
+
881
+ if (borrowableFromNewCollateral > loan.amount) {
882
+ // End of the test, its not possible to `repay` a loan with such a small amount that the loan value goes up.
883
+ // The `collateralReturned` should be increased so the value of the loan goes down.
884
+ return;
885
+ }
886
+
887
+ assertApproxEqAbs(reducedLoan.amount, loan.amount - amountDiff, 1);
888
+ assertEq(reducedLoan.collateral, loan.collateral - collateralReturned);
889
+ assertEq(reducedLoan.createdAt, block.timestamp - daysToWarp);
890
+ assertEq(reducedLoan.prepaidFeePercent, prepaidFeePercent);
891
+ assertEq(reducedLoan.prepaidDuration, mulDiv(prepaidFeePercent, 3650 days, 500));
892
+ assertEq(reducedLoan.source.token, JBConstants.NATIVE_TOKEN);
893
+ assertEq(address(reducedLoan.source.terminal), address(jbMultiTerminal()));
894
+ }
895
+
896
+ function test_Refinance_Excess_Collateral() public {
897
+ // peform the auto issuance.
898
+ REV_DEPLOYER.autoIssueFor(REVNET_ID, block.timestamp, multisig());
899
+
900
+ vm.prank(USER);
901
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
902
+
903
+ uint256 loanable =
904
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
905
+ assertGt(loanable, 0);
906
+
907
+ mockExpect(
908
+ address(jbPermissions()),
909
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
910
+ abi.encode(true)
911
+ );
912
+
913
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
914
+
915
+ vm.prank(USER);
916
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
917
+
918
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
919
+
920
+ // Ensure loans contract isn't hodling
921
+ assertEq(address(LOANS_CONTRACT).balance, 0);
922
+
923
+ // Ensure we actually received ETH from the borrow
924
+ assertGt(USER.balance, 100e18 - 1e18);
925
+
926
+ // get the updated loanableFrom the same amount as earlier
927
+ uint256 loanableSecondStage = LOANS_CONTRACT.borrowableAmountFrom(
928
+ REVNET_ID, loan.collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
929
+ );
930
+
931
+ // loanable amount is slightly higher due to fee payment increasing the supply/assets ratio.
932
+ assertGt(loanableSecondStage, loanable);
933
+
934
+ // we should not have to add collateral
935
+ uint256 collateralToAdd = 0;
936
+
937
+ // this should be a 0.5% gain to be reallocated
938
+ uint256 collateralToTransfer = mulDiv(loan.collateral, 50, 10_000);
939
+
940
+ // get the new amount to borrow
941
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
942
+ REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
943
+ );
944
+
945
+ uint256 userBalanceBefore = USER.balance;
946
+
947
+ vm.prank(USER);
948
+ (,, REVLoan memory adjustedLoan, REVLoan memory newLoan) = LOANS_CONTRACT.reallocateCollateralFromLoan(
949
+ newLoanId, collateralToTransfer, sauce, newAmount, collateralToAdd, payable(USER), 25
950
+ );
951
+
952
+ uint256 userBalanceAfter = USER.balance;
953
+
954
+ // check we received funds period
955
+ assertGt(userBalanceAfter, userBalanceBefore);
956
+ // check we received ~newAmount with a 0.1% buffer
957
+ assertApproxEqRel(userBalanceBefore + newLoan.amount, userBalanceAfter, 1e15);
958
+
959
+ // Check the old loan has been adjusted
960
+ assertEq(adjustedLoan.amount, loan.amount); // Should match the old loan
961
+ assertEq(adjustedLoan.collateral, loan.collateral - collateralToTransfer); // should be reduced
962
+ assertEq(adjustedLoan.createdAt, loan.createdAt); // Should match the old loan
963
+ assertEq(adjustedLoan.prepaidFeePercent, loan.prepaidFeePercent); // Should match the old loan
964
+ assertEq(adjustedLoan.prepaidDuration, mulDiv(loan.prepaidFeePercent, 3650 days, 500));
965
+ assertEq(adjustedLoan.source.token, JBConstants.NATIVE_TOKEN);
966
+ assertEq(address(adjustedLoan.source.terminal), address(jbMultiTerminal()));
967
+
968
+ // Check the new loan with the excess from refinancing
969
+ assertEq(newLoan.amount, newAmount); // Excess from reallocateCollateral
970
+ assertEq(newLoan.collateral, collateralToTransfer); // Matches the amount transferred
971
+ assertEq(newLoan.createdAt, block.timestamp);
972
+ assertEq(newLoan.prepaidFeePercent, 25); // Configured as 25 (min) in reallocateCollateral call
973
+ assertEq(newLoan.prepaidDuration, mulDiv(25, 3650 days, 500)); // Configured as 25 in reallocateCollateral call
974
+ assertEq(newLoan.source.token, JBConstants.NATIVE_TOKEN);
975
+ assertEq(address(newLoan.source.terminal), address(jbMultiTerminal()));
976
+ }
977
+
978
+ function test_Refinance_Not_Enough_Collateral() public {
979
+ // peform the auto issuance.
980
+ REV_DEPLOYER.autoIssueFor(REVNET_ID, block.timestamp, multisig());
981
+
982
+ vm.prank(USER);
983
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
984
+
985
+ uint256 loanable =
986
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
987
+ assertGt(loanable, 0);
988
+
989
+ mockExpect(
990
+ address(jbPermissions()),
991
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
992
+ abi.encode(true)
993
+ );
994
+
995
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
996
+
997
+ vm.prank(USER);
998
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
999
+
1000
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
1001
+
1002
+ // Ensure loans contract isn't hodling
1003
+ assertEq(address(LOANS_CONTRACT).balance, 0);
1004
+
1005
+ // Ensure we actually received ETH from the borrow
1006
+ assertGt(USER.balance, 100e18 - 1e18);
1007
+
1008
+ // get the updated loanableFrom the same amount as earlier
1009
+ uint256 loanableSecondStage = LOANS_CONTRACT.borrowableAmountFrom(
1010
+ REVNET_ID, loan.collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1011
+ );
1012
+
1013
+ // loanable amount is slightly higher due to fee payment increasing the supply/assets ratio.
1014
+ assertGt(loanableSecondStage, loanable);
1015
+
1016
+ // we should not have to add collateral
1017
+ uint256 collateralToAdd = 0;
1018
+
1019
+ // this should be a 0.5% gain to be reallocated
1020
+ uint256 collateralToTransfer = mulDiv(loan.collateral, 50, 10_000);
1021
+
1022
+ // get the new amount to borrow
1023
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
1024
+ REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1025
+ );
1026
+
1027
+ vm.expectRevert(REVLoans.REVLoans_NotEnoughCollateral.selector);
1028
+ vm.prank(USER);
1029
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
1030
+ // collateral exceeds with + 1
1031
+ newLoanId,
1032
+ loan.collateral + 1,
1033
+ sauce,
1034
+ newAmount,
1035
+ collateralToAdd,
1036
+ payable(USER),
1037
+ 0
1038
+ );
1039
+ }
1040
+
1041
+ function test_Refinance_Unauthorized() public {
1042
+ // peform the auto issuance.
1043
+ REV_DEPLOYER.autoIssueFor(REVNET_ID, block.timestamp, multisig());
1044
+
1045
+ vm.prank(USER);
1046
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1047
+
1048
+ uint256 loanable =
1049
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1050
+ assertGt(loanable, 0);
1051
+
1052
+ mockExpect(
1053
+ address(jbPermissions()),
1054
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1055
+ abi.encode(true)
1056
+ );
1057
+
1058
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1059
+
1060
+ vm.prank(USER);
1061
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
1062
+
1063
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
1064
+
1065
+ // Ensure loans contract isn't hodling
1066
+ assertEq(address(LOANS_CONTRACT).balance, 0);
1067
+
1068
+ // Ensure we actually received ETH from the borrow
1069
+ assertGt(USER.balance, 100e18 - 1e18);
1070
+
1071
+ // get the updated loanableFrom the same amount as earlier
1072
+ uint256 loanableSecondStage = LOANS_CONTRACT.borrowableAmountFrom(
1073
+ REVNET_ID, loan.collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1074
+ );
1075
+
1076
+ // loanable amount is slightly higher due to fee payment increasing the supply/assets ratio.
1077
+ assertGt(loanableSecondStage, loanable);
1078
+
1079
+ // we should not have to add collateral
1080
+ uint256 collateralToAdd = 0;
1081
+
1082
+ // this should be a 0.5% gain to be reallocated
1083
+ uint256 collateralToTransfer = mulDiv(loan.collateral, 50, 10_000);
1084
+
1085
+ // get the new amount to borrow
1086
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
1087
+ REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1088
+ );
1089
+
1090
+ address unauthorized = address(1);
1091
+ vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, unauthorized, USER));
1092
+
1093
+ vm.prank(unauthorized);
1094
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
1095
+ newLoanId, collateralToTransfer, sauce, newAmount, collateralToAdd, payable(USER), 25
1096
+ );
1097
+ }
1098
+
1099
+ function test_BorrowWithFeeConverges() public {
1100
+ vm.skip(true);
1101
+
1102
+ // Config
1103
+ uint256 paymentPerBorrow = 0.2 ether;
1104
+ uint16 cashOutTaxRate = 6000;
1105
+ uint256 premint = 0;
1106
+ uint256 amountPaidBeforeFirstBorrow = 0.5 ether;
1107
+
1108
+ // Deploy a new REVNET, that has multiple stages where the fee decrease.
1109
+ // This lets people refinance their loans to get a better rate.
1110
+ uint256 revnetProjectId;
1111
+ {
1112
+ FeeProjectConfig memory projectConfig = getSecondProjectConfig();
1113
+ REVAutoIssuance[] memory issuanceConfs;
1114
+ if (premint > 0) {
1115
+ issuanceConfs = new REVAutoIssuance[](1);
1116
+ issuanceConfs[0] =
1117
+ REVAutoIssuance({chainId: uint32(block.chainid), count: uint104(premint), beneficiary: multisig()});
1118
+ }
1119
+
1120
+ JBSplit[] memory splits = new JBSplit[](1);
1121
+ splits[0].beneficiary = payable(multisig());
1122
+ splits[0].percent = 10_000;
1123
+
1124
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
1125
+ stageConfigurations[0] = REVStageConfig({
1126
+ startsAtOrAfter: uint40(block.timestamp),
1127
+ autoIssuances: new REVAutoIssuance[](0),
1128
+ splitPercent: 0, // 20%
1129
+ splits: splits,
1130
+ initialIssuance: 1000e18,
1131
+ issuanceCutFrequency: 180 days,
1132
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
1133
+ cashOutTaxRate: cashOutTaxRate, // 20%
1134
+ extraMetadata: 0
1135
+ });
1136
+
1137
+ // Replace the configuration.
1138
+ projectConfig.configuration.stageConfigurations = stageConfigurations;
1139
+ projectConfig.configuration.description.salt = "FeeChange";
1140
+
1141
+ revnetProjectId = REV_DEPLOYER.deployFor({
1142
+ revnetId: 0, // Zero to deploy a new revnet
1143
+ configuration: projectConfig.configuration,
1144
+ terminalConfigurations: projectConfig.terminalConfigurations,
1145
+ suckerDeploymentConfiguration: projectConfig.suckerDeploymentConfiguration
1146
+ });
1147
+ }
1148
+
1149
+ if (amountPaidBeforeFirstBorrow > 0) {
1150
+ vm.deal(USER, amountPaidBeforeFirstBorrow);
1151
+
1152
+ jbMultiTerminal().pay{value: amountPaidBeforeFirstBorrow}(
1153
+ revnetProjectId, JBConstants.NATIVE_TOKEN, amountPaidBeforeFirstBorrow, USER, 0, "", ""
1154
+ );
1155
+ }
1156
+
1157
+ // Pay the project, minting us tokens.
1158
+ vm.startPrank(USER);
1159
+
1160
+ {
1161
+ uint8[] memory permissionIds = new uint8[](1);
1162
+ permissionIds[0] = JBPermissionIds.BURN_TOKENS;
1163
+
1164
+ JBPermissionsData memory permissionsData = JBPermissionsData({
1165
+ operator: address(LOANS_CONTRACT), projectId: uint56(revnetProjectId), permissionIds: permissionIds
1166
+ });
1167
+
1168
+ // Give the loans contract permission to our tokens.
1169
+ jbPermissions().setPermissionsFor(address(USER), permissionsData);
1170
+ }
1171
+
1172
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1173
+
1174
+ uint256 initialBorrow = 0;
1175
+ uint256 prevBorrow = 0;
1176
+ uint256 i;
1177
+ while (prevBorrow != (paymentPerBorrow * (10_000 - cashOutTaxRate) / 10_000)) {
1178
+ vm.deal(USER, paymentPerBorrow);
1179
+
1180
+ uint256 tokens = jbMultiTerminal().pay{value: paymentPerBorrow}(
1181
+ revnetProjectId, JBConstants.NATIVE_TOKEN, paymentPerBorrow, USER, 0, "", ""
1182
+ );
1183
+
1184
+ (, REVLoan memory loan) = LOANS_CONTRACT.borrowFrom(revnetProjectId, source, 0, tokens, payable(USER), 500);
1185
+
1186
+ if (i == 0) {
1187
+ initialBorrow = loan.amount;
1188
+ }
1189
+
1190
+ console.log("Loan %s: %s", i, loan.amount);
1191
+ prevBorrow = loan.amount;
1192
+ i++;
1193
+ }
1194
+
1195
+ console.log("Initial Borrow: %s", initialBorrow);
1196
+ console.log("Final Borrow: %s", prevBorrow);
1197
+ }
1198
+
1199
+ function test_Refinance_DueTo_FeeChange() public {
1200
+ // Deploy a new REVNET, that has multiple stages where the fee decrease.
1201
+ // This lets people refinance their loans to get a better rate.
1202
+ FeeProjectConfig memory projectConfig = getSecondProjectConfig();
1203
+
1204
+ JBSplit[] memory splits = new JBSplit[](1);
1205
+ splits[0].beneficiary = payable(multisig());
1206
+ splits[0].percent = 10_000;
1207
+
1208
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](2);
1209
+ stageConfigurations[0] = REVStageConfig({
1210
+ startsAtOrAfter: uint40(block.timestamp),
1211
+ autoIssuances: new REVAutoIssuance[](0),
1212
+ splitPercent: 2000, // 20%
1213
+ splits: splits,
1214
+ initialIssuance: 1000e18,
1215
+ issuanceCutFrequency: 180 days,
1216
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
1217
+ cashOutTaxRate: 2000, // 20%
1218
+ extraMetadata: 0
1219
+ });
1220
+
1221
+ stageConfigurations[1] = REVStageConfig({
1222
+ startsAtOrAfter: uint40(block.timestamp + 720 days),
1223
+ autoIssuances: new REVAutoIssuance[](0),
1224
+ splitPercent: 2000, // 20%
1225
+ splits: splits,
1226
+ initialIssuance: 0, // inherit from previous cycle.
1227
+ issuanceCutFrequency: 180 days,
1228
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
1229
+ cashOutTaxRate: 0, // 40%
1230
+ extraMetadata: 0
1231
+ });
1232
+
1233
+ // Replace the configuration.
1234
+ projectConfig.configuration.stageConfigurations = stageConfigurations;
1235
+ projectConfig.configuration.description.salt = "FeeChange";
1236
+
1237
+ uint256 revnetProjectId = REV_DEPLOYER.deployFor({
1238
+ revnetId: 0, // Zero to deploy a new revnet
1239
+ configuration: projectConfig.configuration,
1240
+ terminalConfigurations: projectConfig.terminalConfigurations,
1241
+ suckerDeploymentConfiguration: projectConfig.suckerDeploymentConfiguration
1242
+ });
1243
+
1244
+ vm.startPrank(USER);
1245
+ uint256 tokens =
1246
+ jbMultiTerminal().pay{value: 1e18}(revnetProjectId, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1247
+
1248
+ // Makes it so the borrow is closer to the cashoutTaxRate.
1249
+ // Without this the borrow would be (near) feeless.
1250
+ jbMultiTerminal().pay{value: 99e18}(revnetProjectId, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1251
+
1252
+ uint256 loanable =
1253
+ LOANS_CONTRACT.borrowableAmountFrom(revnetProjectId, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1254
+ assertGt(loanable, 0);
1255
+
1256
+ mockExpect(
1257
+ address(jbPermissions()),
1258
+ abi.encodeCall(
1259
+ IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, revnetProjectId, 11, true, true)
1260
+ ),
1261
+ abi.encode(true)
1262
+ );
1263
+
1264
+ uint256 balanceBefore = USER.balance;
1265
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1266
+ (uint256 newLoanId, REVLoan memory loan) =
1267
+ LOANS_CONTRACT.borrowFrom(revnetProjectId, source, loanable, tokens, payable(USER), 500);
1268
+
1269
+ // Ensure loans contract isn't hodling
1270
+ assertEq(address(LOANS_CONTRACT).balance, 0);
1271
+
1272
+ // Ensure we actually received ETH from the borrow
1273
+ uint256 balanceAfterIntitialBorrow = USER.balance;
1274
+ assertGt(balanceAfterIntitialBorrow, balanceBefore);
1275
+
1276
+ // Warp to after the cash out tax rate is lower in the second ruleset.
1277
+ vm.warp(stageConfigurations[1].startsAtOrAfter);
1278
+
1279
+ // get the updated loanableFrom the same amount as earlier
1280
+ uint256 loanableSecondStage = LOANS_CONTRACT.borrowableAmountFrom(
1281
+ revnetProjectId, loan.collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1282
+ );
1283
+
1284
+ // loanable amount is (slightly) higher due to fee payment increasing the supply/assets ratio.
1285
+ assertGt(loanableSecondStage, loanable);
1286
+
1287
+ // we should not have to add collateral
1288
+ uint256 collateralToAdd = 0;
1289
+
1290
+ // this should be a .5% gain to be reallocated
1291
+ uint256 collateralToTransfer = mulDiv(loan.collateral, 50, 10_000);
1292
+
1293
+ // get the new amount to borrow
1294
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
1295
+ revnetProjectId, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1296
+ );
1297
+
1298
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
1299
+ newLoanId, collateralToTransfer, source, newAmount, collateralToAdd, payable(USER), 25
1300
+ );
1301
+
1302
+ // Since we refinanced we should have received additional funds, as the tokens are now worth more.
1303
+ assertGt(USER.balance, balanceAfterIntitialBorrow);
1304
+ }
1305
+
1306
+ function test_Refinance_Collateral_Required() public {
1307
+ // peform the auto issuance.
1308
+ REV_DEPLOYER.autoIssueFor(REVNET_ID, block.timestamp, multisig());
1309
+
1310
+ vm.prank(USER);
1311
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1312
+
1313
+ uint256 loanable =
1314
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1315
+ assertGt(loanable, 0);
1316
+
1317
+ mockExpect(
1318
+ address(jbPermissions()),
1319
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1320
+ abi.encode(true)
1321
+ );
1322
+
1323
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1324
+
1325
+ vm.prank(USER);
1326
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
1327
+
1328
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
1329
+
1330
+ // Ensure loans contract isn't hodling
1331
+ assertEq(address(LOANS_CONTRACT).balance, 0);
1332
+
1333
+ // Ensure we actually received ETH from the borrow
1334
+ assertGt(USER.balance, 100e18 - 1e18);
1335
+
1336
+ // warp to after cash out tax rate is lower in the second ruleset
1337
+ vm.warp(block.timestamp + 721 days);
1338
+
1339
+ // get the updated loanableFrom the same amount as earlier
1340
+ uint256 loanableSecondStage = LOANS_CONTRACT.borrowableAmountFrom(
1341
+ REVNET_ID, loan.collateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1342
+ );
1343
+
1344
+ // loanable amount is slightly higher due to fee payment increasing the supply/assets ratio.
1345
+ assertGt(loanableSecondStage, loanable);
1346
+
1347
+ // we should not have to add collateral
1348
+ uint256 collateralToAdd = 0;
1349
+
1350
+ // this should be a 0.5% gain to be reallocated
1351
+ uint256 collateralToTransfer = mulDiv(loan.collateral, 50, 10_000);
1352
+
1353
+ // get the new amount to borrow
1354
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
1355
+ REVNET_ID, collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1356
+ );
1357
+
1358
+ vm.expectRevert(
1359
+ abi.encodeWithSelector(
1360
+ REVLoans.REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows.selector, 0, loan.amount
1361
+ )
1362
+ );
1363
+ vm.prank(USER);
1364
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
1365
+ // attempt moving the total collateral
1366
+ newLoanId,
1367
+ loan.collateral,
1368
+ sauce,
1369
+ newAmount,
1370
+ collateralToAdd,
1371
+ payable(USER),
1372
+ 0
1373
+ );
1374
+ }
1375
+
1376
+ function testFuzz_Refinance(
1377
+ uint256 payAmount,
1378
+ uint256 collateralPercentToTransfer,
1379
+ uint256 secondPayAmount,
1380
+ uint256 prepaidFeePercent,
1381
+ uint256 daysToWarp
1382
+ )
1383
+ public
1384
+ {
1385
+ payAmount = bound(payAmount, 1e18, 100e18);
1386
+ secondPayAmount = bound(secondPayAmount, 1e18, 10e18);
1387
+ prepaidFeePercent = bound(prepaidFeePercent, 25, 500);
1388
+ daysToWarp = bound(daysToWarp, 0, 3650);
1389
+ daysToWarp = daysToWarp * 1 days;
1390
+ collateralPercentToTransfer = bound(collateralPercentToTransfer, 1, 1000);
1391
+
1392
+ // pay once first to receive tokens for the borrow call
1393
+ vm.prank(USER);
1394
+ uint256 tokens =
1395
+ jbMultiTerminal().pay{value: payAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1396
+
1397
+ uint256 loanable =
1398
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1399
+ assertGt(loanable, 0);
1400
+
1401
+ // mock call spoofing permissions of REVLoans otherwise called by user before borrow.
1402
+ mockExpect(
1403
+ address(jbPermissions()),
1404
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1405
+ abi.encode(true)
1406
+ );
1407
+
1408
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1409
+
1410
+ vm.prank(USER);
1411
+ (uint256 newLoanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 500);
1412
+
1413
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(newLoanId);
1414
+
1415
+ // warp to after cash out tax rate is lower in the second ruleset
1416
+ vm.warp(block.timestamp + daysToWarp);
1417
+
1418
+ // pay again to have balance for the refinance
1419
+ uint256 tokens2 =
1420
+ jbMultiTerminal().pay{value: secondPayAmount}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1421
+
1422
+ // bound up to 1% reallocated
1423
+ uint256 collateralToTransfer = mulDiv(loan.collateral, collateralPercentToTransfer, 10_000);
1424
+
1425
+ // get the new amount to borrow
1426
+ uint256 newAmount = LOANS_CONTRACT.borrowableAmountFrom(
1427
+ REVNET_ID, collateralToTransfer + tokens2, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1428
+ );
1429
+
1430
+ uint256 reallocatedLoanValue = LOANS_CONTRACT.borrowableAmountFrom(
1431
+ REVNET_ID, loan.collateral - collateralToTransfer, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1432
+ );
1433
+
1434
+ if (reallocatedLoanValue < loan.amount) {
1435
+ vm.expectRevert(
1436
+ abi.encodeWithSelector(
1437
+ REVLoans.REVLoans_ReallocatingMoreCollateralThanBorrowedAmountAllows.selector,
1438
+ reallocatedLoanValue,
1439
+ loan.amount
1440
+ )
1441
+ );
1442
+ }
1443
+
1444
+ uint256 userBalanceBefore = USER.balance;
1445
+
1446
+ vm.prank(USER);
1447
+ LOANS_CONTRACT.reallocateCollateralFromLoan(
1448
+ newLoanId, collateralToTransfer, sauce, newAmount, tokens2, payable(USER), 25
1449
+ );
1450
+
1451
+ if (reallocatedLoanValue < loan.amount) {
1452
+ return;
1453
+ }
1454
+
1455
+ uint256 userBalanceAfter = USER.balance;
1456
+
1457
+ // check we received funds period
1458
+ assertGt(userBalanceAfter, userBalanceBefore);
1459
+ }
1460
+
1461
+ function test_loanSourcesOfAndDetermineSourceFeeAmount() external {
1462
+ // it will add the loan source upon first borrow
1463
+ vm.prank(USER);
1464
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1465
+
1466
+ uint256 loanable =
1467
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1468
+ assertGt(loanable, 0);
1469
+
1470
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1471
+ mockExpect(
1472
+ address(jbPermissions()),
1473
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1474
+ abi.encode(true)
1475
+ );
1476
+
1477
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1478
+
1479
+ // Before a borrow the source does not exist
1480
+ REVLoanSource[] memory sources = LOANS_CONTRACT.loanSourcesOf(REVNET_ID);
1481
+ assertEq(sources.length, 0);
1482
+
1483
+ vm.prank(USER);
1484
+ (, REVLoan memory loan) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 100);
1485
+
1486
+ // Source should exist after a borrow
1487
+ REVLoanSource[] memory sourcesUpdated = LOANS_CONTRACT.loanSourcesOf(REVNET_ID);
1488
+ assertEq(sourcesUpdated.length, 1);
1489
+ assertEq(sourcesUpdated[0].token, JBConstants.NATIVE_TOKEN);
1490
+ assertEq(address(sourcesUpdated[0].terminal), address(jbMultiTerminal()));
1491
+
1492
+ // Check the fee amount after warping forward past the prepaid duration
1493
+ vm.warp(block.timestamp + loan.prepaidDuration + 100 days);
1494
+ uint256 feeAmount = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
1495
+ assertGt(feeAmount, 0);
1496
+
1497
+ // Warp further than the loan liquidation duration to revert.
1498
+ vm.warp(block.timestamp + 3650 days);
1499
+ vm.expectRevert(
1500
+ abi.encodeWithSelector(
1501
+ REVLoans.REVLoans_LoanExpired.selector, loan.prepaidDuration + 100 days + 3650 days, 3650 days
1502
+ )
1503
+ );
1504
+
1505
+ LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
1506
+ }
1507
+
1508
+ function test_InvalidPrepaidFeePercent(uint16 feePercentage) external {
1509
+ vm.assume(
1510
+ feePercentage < LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT()
1511
+ || feePercentage > LOANS_CONTRACT.MAX_PREPAID_FEE_PERCENT()
1512
+ );
1513
+
1514
+ vm.prank(USER);
1515
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1516
+
1517
+ uint256 loanable =
1518
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1519
+ assertGt(loanable, 0);
1520
+
1521
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1522
+
1523
+ vm.prank(USER);
1524
+ vm.expectRevert(
1525
+ abi.encodeWithSelector(REVLoans.REVLoans_InvalidPrepaidFeePercent.selector, feePercentage, 25, 500)
1526
+ );
1527
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, 1, tokens, payable(USER), feePercentage);
1528
+ }
1529
+
1530
+ function test_liquidateLoans() external {
1531
+ vm.prank(USER);
1532
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1533
+
1534
+ uint256 loanable =
1535
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1536
+ assertGt(loanable, 0);
1537
+
1538
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1539
+ mockExpect(
1540
+ address(jbPermissions()),
1541
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1542
+ abi.encode(true)
1543
+ );
1544
+
1545
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1546
+
1547
+ vm.prank(USER);
1548
+ (uint256 loanId, REVLoan memory loan) =
1549
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 100);
1550
+
1551
+ // Take out another loan
1552
+ vm.prank(USER);
1553
+ uint256 tokens2 = jbMultiTerminal().pay{value: 2e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1554
+
1555
+ uint256 loanable2 =
1556
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1557
+
1558
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1559
+ mockExpect(
1560
+ address(jbPermissions()),
1561
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1562
+ abi.encode(true)
1563
+ );
1564
+
1565
+ vm.prank(USER);
1566
+ (uint256 loanId2, REVLoan memory loan2) =
1567
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable2, tokens2, payable(USER), 50);
1568
+
1569
+ // Warp further than the loan liquidation duration.
1570
+ vm.warp(block.timestamp + 10_000 days);
1571
+
1572
+ // Check topics one and two
1573
+ vm.expectEmit(true, true, false, false);
1574
+ emit IREVLoans.Liquidate(loanId, REVNET_ID, loan, address(0));
1575
+
1576
+ // Check for the second liquidation
1577
+ // Check topics one and two
1578
+ vm.expectEmit(true, true, false, false);
1579
+ emit IREVLoans.Liquidate(loanId2, REVNET_ID, loan2, address(0));
1580
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 1, 2);
1581
+
1582
+ // Call again to trigger the first break (loan.createdAt = 0)
1583
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 1, 2);
1584
+ }
1585
+
1586
+ function test_liquidationRevertsContinued() external {
1587
+ vm.prank(USER);
1588
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1589
+
1590
+ uint256 loanable =
1591
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1592
+ assertGt(loanable, 0);
1593
+
1594
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1595
+ mockExpect(
1596
+ address(jbPermissions()),
1597
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1598
+ abi.encode(true)
1599
+ );
1600
+
1601
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1602
+
1603
+ vm.prank(USER);
1604
+ (uint256 loanId, REVLoan memory loan) =
1605
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 100);
1606
+
1607
+ // Attempt to liquidate before the loan is expired and loop will break
1608
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 0, 2);
1609
+
1610
+ // Repay the loan, adjusting the previous loan.
1611
+ uint256 collateralReturned = mulDiv(loan.collateral, 1000, 10_000);
1612
+
1613
+ uint256 newCollateral = loan.collateral - collateralReturned;
1614
+ uint256 borrowableFromNewCollateral = LOANS_CONTRACT.borrowableAmountFrom(
1615
+ REVNET_ID, newCollateral, 18, uint32(uint160(JBConstants.NATIVE_TOKEN))
1616
+ );
1617
+
1618
+ // Needed for edge case seeds like 17721, 11407, 334
1619
+ if (borrowableFromNewCollateral > 0) borrowableFromNewCollateral -= 1;
1620
+
1621
+ uint256 amountDiff = borrowableFromNewCollateral > loan.amount ? 0 : loan.amount - borrowableFromNewCollateral;
1622
+
1623
+ uint256 amountPaidDown = amountDiff;
1624
+
1625
+ // Calculate the fee.
1626
+ {
1627
+ // Keep a reference to the time since the loan was created.
1628
+ uint256 timeSinceLoanCreated = block.timestamp - loan.createdAt;
1629
+
1630
+ // If the loan period has passed the prepaid time frame, take a fee.
1631
+ if (timeSinceLoanCreated > loan.prepaidDuration) {
1632
+ // Calculate the prepaid fee for the amount being paid back.
1633
+ uint256 prepaidAmount =
1634
+ JBFees.feeAmountFrom({amountBeforeFee: amountDiff, feePercent: loan.prepaidFeePercent});
1635
+
1636
+ // Calculate the fee as a linear proportion given the amount of time that has passed.
1637
+ // sourceFeeAmount = mulDiv(amount, timeSinceLoanCreated, LOAN_LIQUIDATION_DURATION) - prepaidAmount;
1638
+ amountPaidDown += JBFees.feeAmountFrom({
1639
+ amountBeforeFee: amountDiff - prepaidAmount,
1640
+ feePercent: mulDiv(timeSinceLoanCreated, JBConstants.MAX_FEE, 3650 days)
1641
+ });
1642
+ }
1643
+ }
1644
+
1645
+ // ensure we have the balance
1646
+ vm.deal(USER, amountPaidDown);
1647
+
1648
+ // empty allowance data
1649
+ JBSingleAllowance memory allowance;
1650
+
1651
+ // call to pay-down the loan
1652
+ vm.prank(USER);
1653
+ LOANS_CONTRACT.repayLoan{value: amountPaidDown}(
1654
+ loanId, amountPaidDown, collateralReturned, payable(USER), allowance
1655
+ );
1656
+
1657
+ LOANS_CONTRACT.liquidateExpiredLoansFrom(REVNET_ID, 0, 2);
1658
+ }
1659
+
1660
+ function test_repay_unauthorized() external {
1661
+ vm.prank(USER);
1662
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1663
+
1664
+ uint256 loanable =
1665
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1666
+ assertGt(loanable, 0);
1667
+
1668
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1669
+ mockExpect(
1670
+ address(jbPermissions()),
1671
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1672
+ abi.encode(true)
1673
+ );
1674
+
1675
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1676
+
1677
+ vm.prank(USER);
1678
+ (uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 100);
1679
+
1680
+ // empty allowance data
1681
+ JBSingleAllowance memory allowance;
1682
+
1683
+ // call to pay-down the loan
1684
+ /* vm.prank(USER); */
1685
+ vm.expectRevert(abi.encodeWithSelector(REVLoans.REVLoans_Unauthorized.selector, address(this), USER));
1686
+ LOANS_CONTRACT.repayLoan{value: 0}(loanId, 0, 0, payable(USER), allowance);
1687
+ }
1688
+
1689
+ function test_repay_return_invalid_collateral() external {
1690
+ vm.prank(USER);
1691
+ uint256 tokens = jbMultiTerminal().pay{value: 1e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 1e18, USER, 0, "", "");
1692
+
1693
+ uint256 loanable =
1694
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokens, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
1695
+ assertGt(loanable, 0);
1696
+
1697
+ // User must give the loans contract permission, similar to an "approve" call, we're just spoofing to save time.
1698
+ mockExpect(
1699
+ address(jbPermissions()),
1700
+ abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), USER, 2, 11, true, true)),
1701
+ abi.encode(true)
1702
+ );
1703
+
1704
+ REVLoanSource memory sauce = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
1705
+
1706
+ vm.prank(USER);
1707
+ (uint256 loanId, REVLoan memory loan) =
1708
+ LOANS_CONTRACT.borrowFrom(REVNET_ID, sauce, loanable, tokens, payable(USER), 100);
1709
+
1710
+ // empty allowance data
1711
+ JBSingleAllowance memory allowance;
1712
+
1713
+ // call to pay-down the loan
1714
+ vm.prank(USER);
1715
+ vm.expectRevert(
1716
+ abi.encodeWithSelector(
1717
+ REVLoans.REVLoans_CollateralExceedsLoan.selector, loan.collateral + 1, loan.collateral
1718
+ )
1719
+ );
1720
+ LOANS_CONTRACT.repayLoan{value: 0}( // collateral exceeds with + 1
1721
+ loanId, 0, loan.collateral + 1, payable(USER), allowance
1722
+ );
1723
+ }
1724
+
1725
+ function _balanceOf(address token, address user) internal view returns (uint256) {
1726
+ if (token == JBConstants.NATIVE_TOKEN) {
1727
+ return user.balance;
1728
+ }
1729
+
1730
+ return IERC20(token).balanceOf(user);
1731
+ }
1732
+ }