@rev-net/core-v6 0.0.37 → 0.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +2 -2
  2. package/README.md +6 -7
  3. package/foundry.toml +1 -1
  4. package/package.json +23 -16
  5. package/references/operations.md +1 -1
  6. package/references/runtime.md +1 -1
  7. package/script/Deploy.s.sol +12 -9
  8. package/src/REVDeployer.sol +60 -65
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +17 -10
  11. package/src/REVOwner.sol +121 -14
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/interfaces/IREVHiddenTokens.sol +4 -1
  14. package/src/interfaces/IREVOwner.sol +5 -0
  15. package/ADMINISTRATION.md +0 -73
  16. package/ARCHITECTURE.md +0 -116
  17. package/AUDIT_INSTRUCTIONS.md +0 -90
  18. package/RISKS.md +0 -107
  19. package/SKILLS.md +0 -46
  20. package/STYLE_GUIDE.md +0 -610
  21. package/USER_JOURNEYS.md +0 -195
  22. package/foundry.lock +0 -11
  23. package/slither-ci.config.json +0 -10
  24. package/sphinx.lock +0 -507
  25. package/test/REV.integrations.t.sol +0 -573
  26. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  27. package/test/REVDeployerRegressions.t.sol +0 -396
  28. package/test/REVInvincibility.t.sol +0 -1371
  29. package/test/REVInvincibilityHandler.sol +0 -387
  30. package/test/REVLifecycle.t.sol +0 -420
  31. package/test/REVLoans.invariants.t.sol +0 -724
  32. package/test/REVLoansAttacks.t.sol +0 -816
  33. package/test/REVLoansFeeRecovery.t.sol +0 -783
  34. package/test/REVLoansFindings.t.sol +0 -711
  35. package/test/REVLoansRegressions.t.sol +0 -364
  36. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  37. package/test/REVLoansSourced.t.sol +0 -1839
  38. package/test/REVLoansUnSourced.t.sol +0 -409
  39. package/test/TestAuditFixVerification.t.sol +0 -675
  40. package/test/TestBurnHeldTokens.t.sol +0 -394
  41. package/test/TestCEIPattern.t.sol +0 -508
  42. package/test/TestCashOutCallerValidation.t.sol +0 -452
  43. package/test/TestConversionDocumentation.t.sol +0 -365
  44. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  45. package/test/TestCrossSourceReallocation.t.sol +0 -361
  46. package/test/TestERC2771MetaTx.t.sol +0 -585
  47. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  48. package/test/TestFlashLoanSurplus.t.sol +0 -365
  49. package/test/TestHiddenTokens.t.sol +0 -474
  50. package/test/TestHookArrayOOB.t.sol +0 -278
  51. package/test/TestLiquidationBehavior.t.sol +0 -398
  52. package/test/TestLoanSourceRotation.t.sol +0 -553
  53. package/test/TestLoansCashOutDelay.t.sol +0 -493
  54. package/test/TestLongTailEconomics.t.sol +0 -677
  55. package/test/TestLowFindings.t.sol +0 -677
  56. package/test/TestMixedFixes.t.sol +0 -593
  57. package/test/TestPermit2Signatures.t.sol +0 -683
  58. package/test/TestReallocationSandwich.t.sol +0 -412
  59. package/test/TestRevnetRegressions.t.sol +0 -350
  60. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  61. package/test/TestSplitWeightE2E.t.sol +0 -605
  62. package/test/TestSplitWeightFork.t.sol +0 -855
  63. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  64. package/test/TestSwapTerminalPermission.t.sol +0 -262
  65. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  66. package/test/TestUint112Overflow.t.sol +0 -311
  67. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  68. package/test/TestZeroRepayment.t.sol +0 -354
  69. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  70. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  71. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  72. package/test/audit/NemesisVerification.t.sol +0 -97
  73. package/test/audit/OperatorDelegation.t.sol +0 -356
  74. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  75. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  76. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  77. package/test/audit/ReallocatePermission.t.sol +0 -363
  78. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  79. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  80. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  81. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  82. package/test/fork/ForkTestBase.sol +0 -727
  83. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  84. package/test/fork/TestCashOutFork.t.sol +0 -253
  85. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  86. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  87. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  88. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  89. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  90. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  91. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  92. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  93. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  94. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  95. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  96. package/test/helpers/MaliciousContracts.sol +0 -247
  97. package/test/helpers/REVEmpty721Config.sol +0 -45
  98. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  99. package/test/mock/MockBuybackDataHook.sol +0 -112
  100. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  101. package/test/mock/MockSuckerRegistry.sol +0 -17
  102. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  103. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  104. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  105. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  106. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  107. package/test/regression/TestZeroPriceFeed.t.sol +0 -422
@@ -1,683 +0,0 @@
1
- // SPDX-License-Identifier: MIT
2
- pragma solidity 0.8.28;
3
-
4
- // forge-lint: disable-next-line(unaliased-plain-import)
5
- import "forge-std/Test.sol";
6
- // forge-lint: disable-next-line(unaliased-plain-import)
7
- import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
- // forge-lint: disable-next-line(unaliased-plain-import)
9
- import /* {*} from */ "./../src/REVDeployer.sol";
10
- // forge-lint: disable-next-line(unaliased-plain-import)
11
- import "@croptop/core-v6/src/CTPublisher.sol";
12
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
-
14
- // forge-lint: disable-next-line(unaliased-plain-import)
15
- import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
16
- // forge-lint: disable-next-line(unaliased-plain-import)
17
- import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
18
- // forge-lint: disable-next-line(unaliased-plain-import)
19
- import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
20
- // forge-lint: disable-next-line(unaliased-plain-import)
21
- import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
22
- // forge-lint: disable-next-line(unaliased-plain-import)
23
- import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
24
-
25
- import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
26
- import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
27
- import {JBSingleAllowance} from "@bananapus/core-v6/src/structs/JBSingleAllowance.sol";
28
- import {MockPriceFeed} from "@bananapus/core-v6/test/mock/MockPriceFeed.sol";
29
- import {MockERC20} from "@bananapus/core-v6/test/mock/MockERC20.sol";
30
- import {REVLoans} from "../src/REVLoans.sol";
31
- import {REVLoan} from "../src/structs/REVLoan.sol";
32
- import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
33
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
34
- import {REVDescription} from "../src/structs/REVDescription.sol";
35
- import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
36
- import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
37
- import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
38
- import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
39
- import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
40
- import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
41
- import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
42
- import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
43
- import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
44
- import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
45
- import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
46
- import {IAllowanceTransfer} from "@uniswap/permit2/src/interfaces/IAllowanceTransfer.sol";
47
- import {IPermit2} from "@uniswap/permit2/src/interfaces/IPermit2.sol";
48
- import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
49
- import {JBFees} from "@bananapus/core-v6/src/libraries/JBFees.sol";
50
- import {REVOwner} from "../src/REVOwner.sol";
51
- import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
52
- import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
53
-
54
- struct Permit2ProjectConfig {
55
- REVConfig configuration;
56
- JBTerminalConfig[] terminalConfigurations;
57
- REVSuckerDeploymentConfig suckerDeploymentConfiguration;
58
- }
59
-
60
- /// @title TestPermit2Signatures
61
- /// @notice Tests that REVLoans.repayLoan() works correctly with real Permit2 signatures,
62
- /// including happy-path repayment, expired signatures, and wrong-signer scenarios.
63
- contract TestPermit2Signatures is TestBaseWorkflow {
64
- // forge-lint: disable-next-line(mixed-case-variable)
65
- bytes32 REV_DEPLOYER_SALT = "REVDeployer";
66
- // forge-lint: disable-next-line(mixed-case-variable)
67
- bytes32 ERC20_SALT = "REV_TOKEN";
68
-
69
- // forge-lint: disable-next-line(mixed-case-variable)
70
- REVDeployer REV_DEPLOYER;
71
- // forge-lint: disable-next-line(mixed-case-variable)
72
- REVOwner REV_OWNER;
73
- // forge-lint: disable-next-line(mixed-case-variable)
74
- JB721TiersHook EXAMPLE_HOOK;
75
- // forge-lint: disable-next-line(mixed-case-variable)
76
- IJB721TiersHookDeployer HOOK_DEPLOYER;
77
- // forge-lint: disable-next-line(mixed-case-variable)
78
- IJB721TiersHookStore HOOK_STORE;
79
- // forge-lint: disable-next-line(mixed-case-variable)
80
- IJBAddressRegistry ADDRESS_REGISTRY;
81
- // forge-lint: disable-next-line(mixed-case-variable)
82
- IREVLoans LOANS_CONTRACT;
83
- // forge-lint: disable-next-line(mixed-case-variable)
84
- MockERC20 TOKEN;
85
- // forge-lint: disable-next-line(mixed-case-variable)
86
- IJBSuckerRegistry SUCKER_REGISTRY;
87
- // forge-lint: disable-next-line(mixed-case-variable)
88
- CTPublisher PUBLISHER;
89
- // forge-lint: disable-next-line(mixed-case-variable)
90
- MockBuybackDataHook MOCK_BUYBACK;
91
-
92
- // forge-lint: disable-next-line(mixed-case-variable)
93
- uint256 FEE_PROJECT_ID;
94
- // forge-lint: disable-next-line(mixed-case-variable)
95
- uint256 REVNET_ID;
96
-
97
- address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
98
-
99
- // Permit2 signature type hashes (from nana-core-v6 TestPermit2Terminal).
100
- bytes32 public constant _PERMIT_DETAILS_TYPEHASH =
101
- keccak256("PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)");
102
-
103
- bytes32 public constant _PERMIT_SINGLE_TYPEHASH = keccak256(
104
- "PermitSingle(PermitDetails details,address spender,uint256 sigDeadline)PermitDetails(address token,uint160 amount,uint48 expiration,uint48 nonce)"
105
- );
106
-
107
- // Permit2 domain separator, set in setUp.
108
- // forge-lint: disable-next-line(mixed-case-variable)
109
- bytes32 DOMAIN_SEPARATOR;
110
-
111
- // Private key and derived address for signing.
112
- uint256 private signerPrivateKey = 0x12341234;
113
- address private signer;
114
-
115
- function _getFeeProjectConfig() internal view returns (Permit2ProjectConfig memory) {
116
- string memory name = "Revnet";
117
- string memory symbol = "$REV";
118
- string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx";
119
- uint8 decimals = 18;
120
- uint256 decimalMultiplier = 10 ** decimals;
121
-
122
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
123
- accountingContextsToAccept[0] = JBAccountingContext({
124
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
125
- });
126
- accountingContextsToAccept[1] =
127
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
128
-
129
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
130
- terminalConfigurations[0] =
131
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
132
-
133
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
134
- JBSplit[] memory splits = new JBSplit[](1);
135
- splits[0].beneficiary = payable(multisig());
136
- splits[0].percent = 10_000;
137
-
138
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
139
- issuanceConfs[0] = REVAutoIssuance({
140
- // forge-lint: disable-next-line(unsafe-typecast)
141
- chainId: uint32(block.chainid),
142
- // forge-lint: disable-next-line(unsafe-typecast)
143
- count: uint104(70_000 * decimalMultiplier),
144
- beneficiary: multisig()
145
- });
146
-
147
- stageConfigurations[0] = REVStageConfig({
148
- startsAtOrAfter: uint40(block.timestamp),
149
- autoIssuances: issuanceConfs,
150
- splitPercent: 2000,
151
- splits: splits,
152
- // forge-lint: disable-next-line(unsafe-typecast)
153
- initialIssuance: uint112(1000 * decimalMultiplier),
154
- issuanceCutFrequency: 90 days,
155
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
156
- cashOutTaxRate: 6000,
157
- extraMetadata: 0
158
- });
159
-
160
- REVConfig memory revnetConfiguration = REVConfig({
161
- // forge-lint: disable-next-line(named-struct-fields)
162
- description: REVDescription(name, symbol, projectUri, ERC20_SALT),
163
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
164
- splitOperator: multisig(),
165
- stageConfigurations: stageConfigurations
166
- });
167
-
168
- return Permit2ProjectConfig({
169
- configuration: revnetConfiguration,
170
- terminalConfigurations: terminalConfigurations,
171
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
172
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
173
- })
174
- });
175
- }
176
-
177
- function _getRevnetConfig() internal view returns (Permit2ProjectConfig memory) {
178
- string memory name = "NANA";
179
- string memory symbol = "$NANA";
180
- string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx";
181
- uint8 decimals = 18;
182
- uint256 decimalMultiplier = 10 ** decimals;
183
-
184
- JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
185
- accountingContextsToAccept[0] = JBAccountingContext({
186
- token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
187
- });
188
- accountingContextsToAccept[1] =
189
- JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
190
-
191
- JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
192
- terminalConfigurations[0] =
193
- JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
194
-
195
- JBSplit[] memory splits = new JBSplit[](1);
196
- splits[0].beneficiary = payable(multisig());
197
- splits[0].percent = 10_000;
198
-
199
- REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
200
- REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
201
- issuanceConfs[0] = REVAutoIssuance({
202
- // forge-lint: disable-next-line(unsafe-typecast)
203
- chainId: uint32(block.chainid),
204
- // forge-lint: disable-next-line(unsafe-typecast)
205
- count: uint104(70_000 * decimalMultiplier),
206
- beneficiary: multisig()
207
- });
208
-
209
- stageConfigurations[0] = REVStageConfig({
210
- startsAtOrAfter: uint40(block.timestamp),
211
- autoIssuances: issuanceConfs,
212
- splitPercent: 2000,
213
- splits: splits,
214
- // forge-lint: disable-next-line(unsafe-typecast)
215
- initialIssuance: uint112(1000 * decimalMultiplier),
216
- issuanceCutFrequency: 90 days,
217
- issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
218
- cashOutTaxRate: 6000,
219
- extraMetadata: 0
220
- });
221
-
222
- REVConfig memory revnetConfiguration = REVConfig({
223
- // forge-lint: disable-next-line(named-struct-fields)
224
- description: REVDescription(name, symbol, projectUri, "NANA_TOKEN"),
225
- baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
226
- splitOperator: multisig(),
227
- stageConfigurations: stageConfigurations
228
- });
229
-
230
- return Permit2ProjectConfig({
231
- configuration: revnetConfiguration,
232
- terminalConfigurations: terminalConfigurations,
233
- suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
234
- deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
235
- })
236
- });
237
- }
238
-
239
- function setUp() public override {
240
- super.setUp();
241
-
242
- signer = vm.addr(signerPrivateKey);
243
-
244
- FEE_PROJECT_ID = jbProjects().createFor(multisig());
245
-
246
- SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
247
- HOOK_STORE = new JB721TiersHookStore();
248
- EXAMPLE_HOOK = new JB721TiersHook(
249
- jbDirectory(),
250
- jbPermissions(),
251
- jbPrices(),
252
- jbRulesets(),
253
- HOOK_STORE,
254
- jbSplits(),
255
- IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
256
- multisig()
257
- );
258
- ADDRESS_REGISTRY = new JBAddressRegistry();
259
- HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
260
- PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
261
- MOCK_BUYBACK = new MockBuybackDataHook();
262
- TOKEN = new MockERC20("1/2 ETH", "1/2");
263
-
264
- MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
265
- vm.prank(multisig());
266
- jbPrices()
267
- .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
268
-
269
- LOANS_CONTRACT = new REVLoans({
270
- controller: jbController(),
271
- suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
272
- revId: FEE_PROJECT_ID,
273
- owner: address(this),
274
- permit2: permit2(),
275
- trustedForwarder: TRUSTED_FORWARDER
276
- });
277
-
278
- REV_OWNER = new REVOwner(
279
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
280
- jbDirectory(),
281
- FEE_PROJECT_ID,
282
- SUCKER_REGISTRY,
283
- address(LOANS_CONTRACT),
284
- address(0)
285
- );
286
-
287
- REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
288
- jbController(),
289
- SUCKER_REGISTRY,
290
- FEE_PROJECT_ID,
291
- HOOK_DEPLOYER,
292
- PUBLISHER,
293
- IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
294
- address(LOANS_CONTRACT),
295
- TRUSTED_FORWARDER,
296
- address(REV_OWNER)
297
- );
298
-
299
- REV_OWNER.setDeployer(REV_DEPLOYER);
300
-
301
- // Approve the basic deployer to configure the project.
302
- vm.prank(address(multisig()));
303
- jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
304
-
305
- // Deploy fee project.
306
- Permit2ProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
307
- vm.prank(address(multisig()));
308
- REV_DEPLOYER.deployFor({
309
- revnetId: FEE_PROJECT_ID,
310
- configuration: feeProjectConfig.configuration,
311
- terminalConfigurations: feeProjectConfig.terminalConfigurations,
312
- suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
313
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
314
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
315
- });
316
-
317
- // Deploy second revnet with loans enabled.
318
- Permit2ProjectConfig memory revnetConfig = _getRevnetConfig();
319
- (REVNET_ID,) = REV_DEPLOYER.deployFor({
320
- revnetId: 0,
321
- configuration: revnetConfig.configuration,
322
- terminalConfigurations: revnetConfig.terminalConfigurations,
323
- suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration,
324
- tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
325
- allowedPosts: REVEmpty721Config.emptyAllowedPosts()
326
- });
327
-
328
- // Set the Permit2 domain separator.
329
- DOMAIN_SEPARATOR = permit2().DOMAIN_SEPARATOR();
330
-
331
- // Fund the signer.
332
- vm.deal(signer, 1000e18);
333
- }
334
-
335
- // =========================================================================
336
- // Permit2 signature helpers (from nana-core-v6/test/TestPermit2Terminal.sol)
337
- // =========================================================================
338
-
339
- function _getPermitSignatureRaw(
340
- IAllowanceTransfer.PermitSingle memory permitSingle,
341
- uint256 privateKey,
342
- bytes32 domainSeparator
343
- )
344
- internal
345
- pure
346
- returns (uint8 v, bytes32 r, bytes32 s)
347
- {
348
- bytes32 permitHash = keccak256(abi.encode(_PERMIT_DETAILS_TYPEHASH, permitSingle.details));
349
-
350
- bytes32 msgHash = keccak256(
351
- abi.encodePacked(
352
- "\x19\x01",
353
- domainSeparator,
354
- keccak256(
355
- abi.encode(_PERMIT_SINGLE_TYPEHASH, permitHash, permitSingle.spender, permitSingle.sigDeadline)
356
- )
357
- )
358
- );
359
-
360
- (v, r, s) = vm.sign(privateKey, msgHash);
361
- }
362
-
363
- function _getPermitSignature(
364
- IAllowanceTransfer.PermitSingle memory permitSingle,
365
- uint256 privateKey,
366
- bytes32 domainSeparator
367
- )
368
- internal
369
- pure
370
- returns (bytes memory sig)
371
- {
372
- (uint8 v, bytes32 r, bytes32 s) = _getPermitSignatureRaw(permitSingle, privateKey, domainSeparator);
373
- return bytes.concat(r, s, bytes1(v));
374
- }
375
-
376
- // =========================================================================
377
- // Helper: create a loan from ERC20 tokens (not native)
378
- // =========================================================================
379
-
380
- function _setupERC20Loan(
381
- address user,
382
- uint256 tokenAmount,
383
- uint256 prepaidFee
384
- )
385
- internal
386
- returns (uint256 loanId, uint256 tokenCount, uint256 borrowAmount)
387
- {
388
- // Deal the user ERC20 tokens and approve the terminal.
389
- deal(address(TOKEN), user, tokenAmount);
390
- vm.prank(user);
391
- TOKEN.approve(address(jbMultiTerminal()), tokenAmount);
392
-
393
- // Pay into revnet to get project tokens.
394
- vm.prank(user);
395
- tokenCount = jbMultiTerminal().pay(REVNET_ID, address(TOKEN), tokenAmount, user, 0, "", "");
396
-
397
- // Check borrowable amount.
398
- borrowAmount = LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 6, uint32(uint160(address(TOKEN))));
399
-
400
- if (borrowAmount == 0) return (0, tokenCount, 0);
401
-
402
- // Mock permission for loans contract to burn tokens.
403
- mockExpect(
404
- address(jbPermissions()),
405
- abi.encodeCall(IJBPermissions.hasPermission, (address(LOANS_CONTRACT), user, REVNET_ID, 11, true, true)),
406
- abi.encode(true)
407
- );
408
-
409
- REVLoanSource memory source = REVLoanSource({token: address(TOKEN), terminal: jbMultiTerminal()});
410
-
411
- vm.prank(user);
412
- (loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(user), prepaidFee, user);
413
- }
414
-
415
- // =========================================================================
416
- // Test: repay loan with a real Permit2 signature
417
- // =========================================================================
418
-
419
- /// @notice Repays a loan using a real Permit2 signature instead of direct ERC20 approval.
420
- /// @dev Verifies end-to-end Permit2 flow: sign -> repayLoan -> funds transferred.
421
- function test_permit2_realSignature_repayLoan() public {
422
- // Create a loan from the signer's ERC20 tokens.
423
- uint256 payAmount = 100e6; // 100 USDC-like tokens (6 decimals).
424
- (uint256 loanId, uint256 tokenCount, uint256 borrowAmount) =
425
- _setupERC20Loan(signer, payAmount, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
426
-
427
- // Skip if borrowable amount was zero.
428
- vm.assume(borrowAmount > 0);
429
- vm.assume(tokenCount > 0);
430
- vm.assume(loanId > 0);
431
-
432
- // Get loan details.
433
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
434
- assertTrue(loan.amount > 0, "Loan amount should be non-zero");
435
-
436
- // Calculate repay amount (loan amount + source fee for immediate repay).
437
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
438
- uint256 totalRepay = loan.amount + sourceFee;
439
-
440
- // Give the signer enough ERC20 tokens to repay.
441
- deal(address(TOKEN), signer, totalRepay * 2);
442
-
443
- // Approve Permit2 contract to spend the signer's tokens (step 1 of Permit2 flow).
444
- vm.prank(signer);
445
- TOKEN.approve(address(permit2()), type(uint256).max);
446
-
447
- // Build and sign a Permit2 allowance for REVLoans to spend tokens.
448
- uint48 expiration = uint48(block.timestamp + 1 hours);
449
- uint256 sigDeadline = block.timestamp + 1 hours;
450
- // forge-lint: disable-next-line(unsafe-typecast)
451
- uint160 permitAmount = uint160(totalRepay * 2);
452
-
453
- IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
454
- details: IAllowanceTransfer.PermitDetails({
455
- token: address(TOKEN), amount: permitAmount, expiration: expiration, nonce: 0
456
- }),
457
- spender: address(LOANS_CONTRACT),
458
- sigDeadline: sigDeadline
459
- });
460
-
461
- bytes memory sig = _getPermitSignature(permitSingle, signerPrivateKey, DOMAIN_SEPARATOR);
462
-
463
- // Build the JBSingleAllowance with the real signature.
464
- JBSingleAllowance memory allowance = JBSingleAllowance({
465
- sigDeadline: sigDeadline, amount: permitAmount, expiration: expiration, nonce: uint48(0), signature: sig
466
- });
467
-
468
- // Record balances before repayment.
469
- uint256 signerBalanceBefore = TOKEN.balanceOf(signer);
470
- uint256 totalCollateralBefore = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
471
-
472
- // Repay the loan using the Permit2 signature (no direct ERC20 approval to LOANS_CONTRACT).
473
- vm.prank(signer);
474
- LOANS_CONTRACT.repayLoan({
475
- loanId: loanId,
476
- maxRepayBorrowAmount: totalRepay * 2,
477
- collateralCountToReturn: loan.collateral,
478
- beneficiary: payable(signer),
479
- allowance: allowance
480
- });
481
-
482
- // Verify repayment succeeded: signer spent tokens.
483
- uint256 signerBalanceAfter = TOKEN.balanceOf(signer);
484
- assertTrue(signerBalanceAfter < signerBalanceBefore, "Signer should have spent tokens on repayment");
485
-
486
- // Verify collateral was returned.
487
- uint256 totalCollateralAfter = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
488
- assertEq(totalCollateralAfter, totalCollateralBefore - loan.collateral, "Collateral should be returned");
489
- }
490
-
491
- // =========================================================================
492
- // Test: expired Permit2 signature reverts
493
- // =========================================================================
494
-
495
- /// @notice Verifies that an expired Permit2 signature causes the repayLoan call to revert.
496
- /// @dev The Permit2 contract rejects signatures whose sigDeadline has passed.
497
- function test_permit2_expiredSignature_reverts() public {
498
- // Create a loan.
499
- uint256 payAmount = 100e6;
500
- (uint256 loanId, uint256 tokenCount, uint256 borrowAmount) =
501
- _setupERC20Loan(signer, payAmount, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
502
-
503
- vm.assume(borrowAmount > 0);
504
- vm.assume(tokenCount > 0);
505
- vm.assume(loanId > 0);
506
-
507
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
508
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
509
- uint256 totalRepay = loan.amount + sourceFee;
510
-
511
- // Give tokens and approve Permit2.
512
- deal(address(TOKEN), signer, totalRepay * 2);
513
- vm.prank(signer);
514
- TOKEN.approve(address(permit2()), type(uint256).max);
515
-
516
- // Sign with a deadline that is already expired.
517
- uint256 expiredDeadline = block.timestamp - 1;
518
- uint48 expiration = uint48(block.timestamp + 1 hours);
519
- // forge-lint: disable-next-line(unsafe-typecast)
520
- uint160 permitAmount = uint160(totalRepay * 2);
521
-
522
- IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
523
- details: IAllowanceTransfer.PermitDetails({
524
- token: address(TOKEN), amount: permitAmount, expiration: expiration, nonce: 0
525
- }),
526
- spender: address(LOANS_CONTRACT),
527
- sigDeadline: expiredDeadline
528
- });
529
-
530
- bytes memory sig = _getPermitSignature(permitSingle, signerPrivateKey, DOMAIN_SEPARATOR);
531
-
532
- JBSingleAllowance memory allowance = JBSingleAllowance({
533
- sigDeadline: expiredDeadline, amount: permitAmount, expiration: expiration, nonce: uint48(0), signature: sig
534
- });
535
-
536
- // The Permit2 try-catch in _acceptFundsFor swallows the permit error,
537
- // so the permit call itself does not revert. However, the subsequent _transferFrom
538
- // will attempt PERMIT2.transferFrom which requires the allowance to have been set.
539
- // Since the expired permit did not set the allowance, _transferFrom will revert.
540
- vm.prank(signer);
541
- vm.expectRevert();
542
- LOANS_CONTRACT.repayLoan({
543
- loanId: loanId,
544
- maxRepayBorrowAmount: totalRepay * 2,
545
- collateralCountToReturn: loan.collateral,
546
- beneficiary: payable(signer),
547
- allowance: allowance
548
- });
549
- }
550
-
551
- // =========================================================================
552
- // Test: wrong signer — signature signed by A, call from B
553
- // =========================================================================
554
-
555
- /// @notice Verifies that a Permit2 signature signed by one key cannot be used by a different address.
556
- /// @dev The permit2.permit() call checks that the signature matches the `owner` parameter,
557
- /// which is _msgSender() in _acceptFundsFor. If they do not match, the permit fails,
558
- /// and the subsequent transfer also fails since no allowance was set.
559
- function test_permit2_wrongSigner_reverts() public {
560
- // Create a loan owned by the signer.
561
- uint256 payAmount = 100e6;
562
- (uint256 loanId, uint256 tokenCount, uint256 borrowAmount) =
563
- _setupERC20Loan(signer, payAmount, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
564
-
565
- vm.assume(borrowAmount > 0);
566
- vm.assume(tokenCount > 0);
567
- vm.assume(loanId > 0);
568
-
569
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
570
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
571
- uint256 totalRepay = loan.amount + sourceFee;
572
-
573
- // Use a completely different private key for signing.
574
- uint256 wrongPrivateKey = 0xDEADBEEF;
575
- address wrongSigner = vm.addr(wrongPrivateKey);
576
-
577
- // Transfer the loan NFT to the wrongSigner so they can call repayLoan.
578
- vm.prank(signer);
579
- REVLoans(payable(address(LOANS_CONTRACT))).transferFrom(signer, wrongSigner, loanId);
580
-
581
- // Give tokens to the wrongSigner and approve Permit2.
582
- deal(address(TOKEN), wrongSigner, totalRepay * 2);
583
- vm.prank(wrongSigner);
584
- TOKEN.approve(address(permit2()), type(uint256).max);
585
-
586
- // Sign the permit with the ORIGINAL signer's key (not wrongSigner's key).
587
- uint48 expiration = uint48(block.timestamp + 1 hours);
588
- uint256 sigDeadline = block.timestamp + 1 hours;
589
- // forge-lint: disable-next-line(unsafe-typecast)
590
- uint160 permitAmount = uint160(totalRepay * 2);
591
-
592
- IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
593
- details: IAllowanceTransfer.PermitDetails({
594
- token: address(TOKEN), amount: permitAmount, expiration: expiration, nonce: 0
595
- }),
596
- spender: address(LOANS_CONTRACT),
597
- sigDeadline: sigDeadline
598
- });
599
-
600
- // Sign with the original signer's key, but call will come from wrongSigner.
601
- bytes memory sig = _getPermitSignature(permitSingle, signerPrivateKey, DOMAIN_SEPARATOR);
602
-
603
- JBSingleAllowance memory allowance = JBSingleAllowance({
604
- sigDeadline: sigDeadline, amount: permitAmount, expiration: expiration, nonce: uint48(0), signature: sig
605
- });
606
-
607
- // The permit2.permit() call will fail because the signature was signed for
608
- // the original signer's address, but _msgSender() is wrongSigner.
609
- // The try-catch swallows this, then _transferFrom fails since no allowance was set.
610
- vm.prank(wrongSigner);
611
- vm.expectRevert();
612
- LOANS_CONTRACT.repayLoan({
613
- loanId: loanId,
614
- maxRepayBorrowAmount: totalRepay * 2,
615
- collateralCountToReturn: loan.collateral,
616
- beneficiary: payable(wrongSigner),
617
- allowance: allowance
618
- });
619
- }
620
-
621
- // =========================================================================
622
- // Test: repay with correct signer matches permit owner
623
- // =========================================================================
624
-
625
- /// @notice Verifies that a properly signed Permit2 allowance where the signer matches
626
- /// _msgSender() succeeds, while the same signature fails when called from a
627
- /// different address. This validates the signer-caller binding.
628
- function test_permit2_signerMatchesCaller() public {
629
- // Create a loan from the signer.
630
- uint256 payAmount = 100e6;
631
- (uint256 loanId, uint256 tokenCount, uint256 borrowAmount) =
632
- _setupERC20Loan(signer, payAmount, LOANS_CONTRACT.MIN_PREPAID_FEE_PERCENT());
633
-
634
- vm.assume(borrowAmount > 0);
635
- vm.assume(tokenCount > 0);
636
- vm.assume(loanId > 0);
637
-
638
- REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
639
- uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
640
- uint256 totalRepay = loan.amount + sourceFee;
641
-
642
- // Give signer enough tokens to repay.
643
- deal(address(TOKEN), signer, totalRepay * 2);
644
-
645
- // Approve Permit2 to spend tokens.
646
- vm.prank(signer);
647
- TOKEN.approve(address(permit2()), type(uint256).max);
648
-
649
- // Build the Permit2 signature where spender = LOANS_CONTRACT, signed by signer.
650
- uint48 expiration = uint48(block.timestamp + 1 hours);
651
- uint256 sigDeadline = block.timestamp + 1 hours;
652
- // forge-lint: disable-next-line(unsafe-typecast)
653
- uint160 permitAmount = uint160(totalRepay * 2);
654
-
655
- IAllowanceTransfer.PermitSingle memory permitSingle = IAllowanceTransfer.PermitSingle({
656
- details: IAllowanceTransfer.PermitDetails({
657
- token: address(TOKEN), amount: permitAmount, expiration: expiration, nonce: 0
658
- }),
659
- spender: address(LOANS_CONTRACT),
660
- sigDeadline: sigDeadline
661
- });
662
-
663
- bytes memory sig = _getPermitSignature(permitSingle, signerPrivateKey, DOMAIN_SEPARATOR);
664
-
665
- JBSingleAllowance memory allowance = JBSingleAllowance({
666
- sigDeadline: sigDeadline, amount: permitAmount, expiration: expiration, nonce: uint48(0), signature: sig
667
- });
668
-
669
- // When called by the signer (who signed the permit), it should succeed.
670
- vm.prank(signer);
671
- LOANS_CONTRACT.repayLoan({
672
- loanId: loanId,
673
- maxRepayBorrowAmount: totalRepay * 2,
674
- collateralCountToReturn: loan.collateral,
675
- beneficiary: payable(signer),
676
- allowance: allowance
677
- });
678
-
679
- // Verify collateral was returned.
680
- uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
681
- assertEq(totalCollateral, 0, "All collateral should be returned after full repay");
682
- }
683
- }