@rev-net/core-v6 0.0.36 → 0.0.39

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