@rev-net/core-v6 0.0.37 → 0.0.40

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 (112) 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 +69 -67
  9. package/src/REVHiddenTokens.sol +2 -2
  10. package/src/REVLoans.sol +26 -22
  11. package/src/REVOwner.sol +147 -29
  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/src/structs/REVAutoIssuance.sol +4 -2
  16. package/src/structs/REVConfig.sol +8 -5
  17. package/src/structs/REVDescription.sol +6 -5
  18. package/src/structs/REVLoan.sol +8 -5
  19. package/src/structs/REVStageConfig.sol +14 -16
  20. package/ADMINISTRATION.md +0 -73
  21. package/ARCHITECTURE.md +0 -116
  22. package/AUDIT_INSTRUCTIONS.md +0 -90
  23. package/RISKS.md +0 -107
  24. package/SKILLS.md +0 -46
  25. package/STYLE_GUIDE.md +0 -610
  26. package/USER_JOURNEYS.md +0 -195
  27. package/foundry.lock +0 -11
  28. package/slither-ci.config.json +0 -10
  29. package/sphinx.lock +0 -507
  30. package/test/REV.integrations.t.sol +0 -573
  31. package/test/REVAutoIssuanceFuzz.t.sol +0 -328
  32. package/test/REVDeployerRegressions.t.sol +0 -396
  33. package/test/REVInvincibility.t.sol +0 -1371
  34. package/test/REVInvincibilityHandler.sol +0 -387
  35. package/test/REVLifecycle.t.sol +0 -420
  36. package/test/REVLoans.invariants.t.sol +0 -724
  37. package/test/REVLoansAttacks.t.sol +0 -816
  38. package/test/REVLoansFeeRecovery.t.sol +0 -783
  39. package/test/REVLoansFindings.t.sol +0 -711
  40. package/test/REVLoansRegressions.t.sol +0 -364
  41. package/test/REVLoansSourceFeeRecovery.t.sol +0 -517
  42. package/test/REVLoansSourced.t.sol +0 -1839
  43. package/test/REVLoansUnSourced.t.sol +0 -409
  44. package/test/TestAuditFixVerification.t.sol +0 -675
  45. package/test/TestBurnHeldTokens.t.sol +0 -394
  46. package/test/TestCEIPattern.t.sol +0 -508
  47. package/test/TestCashOutCallerValidation.t.sol +0 -452
  48. package/test/TestConversionDocumentation.t.sol +0 -365
  49. package/test/TestCrossCurrencyReclaim.t.sol +0 -610
  50. package/test/TestCrossSourceReallocation.t.sol +0 -361
  51. package/test/TestERC2771MetaTx.t.sol +0 -585
  52. package/test/TestEmptyBuybackSpecs.t.sol +0 -300
  53. package/test/TestFlashLoanSurplus.t.sol +0 -365
  54. package/test/TestHiddenTokens.t.sol +0 -474
  55. package/test/TestHookArrayOOB.t.sol +0 -278
  56. package/test/TestLiquidationBehavior.t.sol +0 -398
  57. package/test/TestLoanSourceRotation.t.sol +0 -553
  58. package/test/TestLoansCashOutDelay.t.sol +0 -493
  59. package/test/TestLongTailEconomics.t.sol +0 -677
  60. package/test/TestLowFindings.t.sol +0 -677
  61. package/test/TestMixedFixes.t.sol +0 -593
  62. package/test/TestPermit2Signatures.t.sol +0 -683
  63. package/test/TestReallocationSandwich.t.sol +0 -412
  64. package/test/TestRevnetRegressions.t.sol +0 -350
  65. package/test/TestSplitWeightAdjustment.t.sol +0 -527
  66. package/test/TestSplitWeightE2E.t.sol +0 -605
  67. package/test/TestSplitWeightFork.t.sol +0 -855
  68. package/test/TestStageTransitionBorrowable.t.sol +0 -301
  69. package/test/TestSwapTerminalPermission.t.sol +0 -262
  70. package/test/TestTerminalEncodingInHash.t.sol +0 -326
  71. package/test/TestUint112Overflow.t.sol +0 -311
  72. package/test/TestZeroAmountLoanGuard.t.sol +0 -378
  73. package/test/TestZeroRepayment.t.sol +0 -354
  74. package/test/audit/CrossChainBuybackRouteMismatch.t.sol +0 -184
  75. package/test/audit/HiddenSupplyCashout.t.sol +0 -61
  76. package/test/audit/LoanIdOverflowGuard.t.sol +0 -523
  77. package/test/audit/NemesisVerification.t.sol +0 -97
  78. package/test/audit/OperatorDelegation.t.sol +0 -356
  79. package/test/audit/PhantomSurplusTerminal.t.sol +0 -367
  80. package/test/audit/REVOwnerCurrencyMismatch.t.sol +0 -188
  81. package/test/audit/REVOwnerRemoteSurplusCurrencyMismatch.t.sol +0 -140
  82. package/test/audit/ReallocatePermission.t.sol +0 -363
  83. package/test/audit/RemoteLoanAccountingGap.t.sol +0 -74
  84. package/test/audit/SupportsInterfaceTest.t.sol +0 -51
  85. package/test/audit/TestFeeAllowanceLeak.t.sol +0 -197
  86. package/test/audit/TestLoansAndDeployerFixes.t.sol +0 -576
  87. package/test/fork/ForkTestBase.sol +0 -727
  88. package/test/fork/TestAutoIssuanceFork.t.sol +0 -148
  89. package/test/fork/TestCashOutFork.t.sol +0 -253
  90. package/test/fork/TestIssuanceDecayFork.t.sol +0 -158
  91. package/test/fork/TestLoanAdversarialFork.t.sol +0 -744
  92. package/test/fork/TestLoanBorrowFork.t.sol +0 -163
  93. package/test/fork/TestLoanCrossRulesetFork.t.sol +0 -308
  94. package/test/fork/TestLoanERC20Fork.t.sol +0 -459
  95. package/test/fork/TestLoanLiquidationFork.t.sol +0 -135
  96. package/test/fork/TestLoanReallocateFork.t.sol +0 -113
  97. package/test/fork/TestLoanRepayFork.t.sol +0 -188
  98. package/test/fork/TestLoanTransferFork.t.sol +0 -143
  99. package/test/fork/TestPermit2PaymentFork.t.sol +0 -300
  100. package/test/fork/TestSplitWeightFork.t.sol +0 -189
  101. package/test/helpers/MaliciousContracts.sol +0 -247
  102. package/test/helpers/REVEmpty721Config.sol +0 -45
  103. package/test/mock/MockBuybackCashOutRecorder.sol +0 -84
  104. package/test/mock/MockBuybackDataHook.sol +0 -112
  105. package/test/mock/MockBuybackDataHookMintPath.sol +0 -68
  106. package/test/mock/MockSuckerRegistry.sol +0 -17
  107. package/test/regression/TestBurnPermissionRequired.t.sol +0 -294
  108. package/test/regression/TestCashOutBuybackFeeLeak.t.sol +0 -232
  109. package/test/regression/TestCrossRevnetLiquidation.t.sol +0 -255
  110. package/test/regression/TestCumulativeLoanCounter.t.sol +0 -361
  111. package/test/regression/TestLiquidateGapHandling.t.sol +0 -394
  112. 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
- }