@rev-net/core-v6 0.0.12 → 0.0.13

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 (78) hide show
  1. package/AUDIT_INSTRUCTIONS.md +295 -0
  2. package/CHANGE_LOG.md +316 -0
  3. package/README.md +2 -2
  4. package/RISKS.md +180 -35
  5. package/SKILLS.md +1 -1
  6. package/USER_JOURNEYS.md +489 -0
  7. package/package.json +9 -9
  8. package/script/Deploy.s.sol +40 -6
  9. package/script/helpers/RevnetCoreDeploymentLib.sol +7 -1
  10. package/src/REVDeployer.sol +63 -47
  11. package/src/REVLoans.sol +51 -15
  12. package/src/interfaces/IREVDeployer.sol +0 -1
  13. package/src/structs/REV721TiersHookFlags.sol +1 -0
  14. package/src/structs/REVAutoIssuance.sol +1 -0
  15. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  16. package/src/structs/REVConfig.sol +1 -0
  17. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  18. package/src/structs/REVDeploy721TiersHookConfig.sol +1 -0
  19. package/src/structs/REVDescription.sol +1 -0
  20. package/src/structs/REVLoan.sol +1 -0
  21. package/src/structs/REVLoanSource.sol +1 -0
  22. package/src/structs/REVStageConfig.sol +1 -0
  23. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  24. package/test/REV.integrations.t.sol +132 -12
  25. package/test/REVAutoIssuanceFuzz.t.sol +23 -3
  26. package/test/REVDeployerRegressions.t.sol +35 -4
  27. package/test/REVInvincibility.t.sol +58 -8
  28. package/test/REVInvincibilityHandler.sol +29 -0
  29. package/test/REVLifecycle.t.sol +28 -3
  30. package/test/REVLoans.invariants.t.sol +52 -5
  31. package/test/REVLoansAttacks.t.sol +43 -5
  32. package/test/REVLoansFeeRecovery.t.sol +50 -11
  33. package/test/REVLoansFindings.t.sol +27 -3
  34. package/test/REVLoansRegressions.t.sol +25 -3
  35. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  36. package/test/REVLoansSourced.t.sol +56 -7
  37. package/test/REVLoansUnSourced.t.sol +49 -5
  38. package/test/TestBurnHeldTokens.t.sol +32 -5
  39. package/test/TestCEIPattern.t.sol +26 -2
  40. package/test/TestCashOutCallerValidation.t.sol +30 -4
  41. package/test/TestConversionDocumentation.t.sol +26 -5
  42. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  43. package/test/TestCrossSourceReallocation.t.sol +26 -2
  44. package/test/TestERC2771MetaTx.t.sol +557 -0
  45. package/test/TestEmptyBuybackSpecs.t.sol +23 -3
  46. package/test/TestFlashLoanSurplus.t.sol +28 -3
  47. package/test/TestHookArrayOOB.t.sol +24 -4
  48. package/test/TestLiquidationBehavior.t.sol +26 -3
  49. package/test/TestLoanSourceRotation.t.sol +525 -0
  50. package/test/TestLongTailEconomics.t.sol +651 -0
  51. package/test/TestLowFindings.t.sol +65 -2
  52. package/test/TestMixedFixes.t.sol +28 -3
  53. package/test/TestPermit2Signatures.t.sol +657 -0
  54. package/test/TestReallocationSandwich.t.sol +384 -0
  55. package/test/TestRevnetRegressions.t.sol +324 -0
  56. package/test/TestSplitWeightAdjustment.t.sol +24 -2
  57. package/test/TestSplitWeightE2E.t.sol +29 -2
  58. package/test/TestSplitWeightFork.t.sol +46 -7
  59. package/test/TestStageTransitionBorrowable.t.sol +24 -2
  60. package/test/TestSwapTerminalPermission.t.sol +23 -3
  61. package/test/TestUint112Overflow.t.sol +28 -2
  62. package/test/TestZeroRepayment.t.sol +26 -2
  63. package/test/fork/ForkTestBase.sol +46 -3
  64. package/test/fork/TestCashOutFork.t.sol +1 -1
  65. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  66. package/test/fork/TestLoanCrossRulesetFork.t.sol +3 -1
  67. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  68. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  69. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  70. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  71. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  72. package/test/helpers/REVEmpty721Config.sol +1 -0
  73. package/test/mock/MockBuybackDataHook.sol +1 -0
  74. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  75. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  76. package/test/regression/TestCumulativeLoanCounter.t.sol +27 -4
  77. package/test/regression/TestLiquidateGapHandling.t.sol +29 -4
  78. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
@@ -0,0 +1,557 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
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 {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
42
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
43
+ import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
44
+ import {ERC2771Forwarder} from "@openzeppelin/contracts/metatx/ERC2771Forwarder.sol";
45
+ import {ERC2771ForwarderMock, ForwardRequest} from "@bananapus/core-v6/test/mock/ERC2771ForwarderMock.sol";
46
+
47
+ struct MetaTxProjectConfig {
48
+ REVConfig configuration;
49
+ JBTerminalConfig[] terminalConfigurations;
50
+ REVSuckerDeploymentConfig suckerDeploymentConfiguration;
51
+ }
52
+
53
+ /// @title TestERC2771MetaTx
54
+ /// @notice Tests that REVLoans and REVDeployer correctly use ERC2771Context,
55
+ /// ensuring _msgSender() returns the actual user when called through a trusted forwarder,
56
+ /// and falls back to msg.sender when called through an untrusted one.
57
+ contract TestERC2771MetaTx is TestBaseWorkflow {
58
+ // forge-lint: disable-next-line(mixed-case-variable)
59
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
60
+ // forge-lint: disable-next-line(mixed-case-variable)
61
+ bytes32 ERC20_SALT = "REV_TOKEN";
62
+
63
+ // forge-lint: disable-next-line(mixed-case-variable)
64
+ REVDeployer REV_DEPLOYER;
65
+ // forge-lint: disable-next-line(mixed-case-variable)
66
+ JB721TiersHook EXAMPLE_HOOK;
67
+ // forge-lint: disable-next-line(mixed-case-variable)
68
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
69
+ // forge-lint: disable-next-line(mixed-case-variable)
70
+ IJB721TiersHookStore HOOK_STORE;
71
+ // forge-lint: disable-next-line(mixed-case-variable)
72
+ IJBAddressRegistry ADDRESS_REGISTRY;
73
+ // forge-lint: disable-next-line(mixed-case-variable)
74
+ REVLoans LOANS_CONTRACT;
75
+ // forge-lint: disable-next-line(mixed-case-variable)
76
+ MockERC20 TOKEN;
77
+ // forge-lint: disable-next-line(mixed-case-variable)
78
+ IJBSuckerRegistry SUCKER_REGISTRY;
79
+ // forge-lint: disable-next-line(mixed-case-variable)
80
+ CTPublisher PUBLISHER;
81
+ // forge-lint: disable-next-line(mixed-case-variable)
82
+ MockBuybackDataHook MOCK_BUYBACK;
83
+
84
+ // forge-lint: disable-next-line(mixed-case-variable)
85
+ uint256 FEE_PROJECT_ID;
86
+ // forge-lint: disable-next-line(mixed-case-variable)
87
+ uint256 REVNET_ID;
88
+
89
+ // The trusted forwarder mock deployed at a specific address.
90
+ ERC2771ForwarderMock internal erc2771Forwarder;
91
+ address internal constant FORWARDER_ADDRESS = address(123_456);
92
+
93
+ // Meta-tx signer and relayer.
94
+ uint256 internal signerPrivateKey;
95
+ uint256 internal relayerPrivateKey;
96
+ address internal signerAddr;
97
+ address internal relayerAddr;
98
+
99
+ function _getFeeProjectConfig() internal view returns (MetaTxProjectConfig memory) {
100
+ string memory name = "Revnet";
101
+ string memory symbol = "$REV";
102
+ string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNcosvuhX21wkF3tx";
103
+ uint8 decimals = 18;
104
+ uint256 decimalMultiplier = 10 ** decimals;
105
+
106
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
107
+ accountingContextsToAccept[0] = JBAccountingContext({
108
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
109
+ });
110
+ accountingContextsToAccept[1] =
111
+ JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
112
+
113
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
114
+ terminalConfigurations[0] =
115
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
116
+
117
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
118
+ JBSplit[] memory splits = new JBSplit[](1);
119
+ splits[0].beneficiary = payable(multisig());
120
+ splits[0].percent = 10_000;
121
+
122
+ REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
123
+ issuanceConfs[0] = REVAutoIssuance({
124
+ // forge-lint: disable-next-line(unsafe-typecast)
125
+ chainId: uint32(block.chainid),
126
+ // forge-lint: disable-next-line(unsafe-typecast)
127
+ count: uint104(70_000 * decimalMultiplier),
128
+ beneficiary: multisig()
129
+ });
130
+
131
+ stageConfigurations[0] = REVStageConfig({
132
+ startsAtOrAfter: uint40(block.timestamp),
133
+ autoIssuances: issuanceConfs,
134
+ splitPercent: 2000,
135
+ splits: splits,
136
+ // forge-lint: disable-next-line(unsafe-typecast)
137
+ initialIssuance: uint112(1000 * decimalMultiplier),
138
+ issuanceCutFrequency: 90 days,
139
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
140
+ cashOutTaxRate: 6000,
141
+ extraMetadata: 0
142
+ });
143
+
144
+ REVConfig memory revnetConfiguration = REVConfig({
145
+ // forge-lint: disable-next-line(named-struct-fields)
146
+ description: REVDescription(name, symbol, projectUri, ERC20_SALT),
147
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
148
+ splitOperator: multisig(),
149
+ stageConfigurations: stageConfigurations
150
+ });
151
+
152
+ return MetaTxProjectConfig({
153
+ configuration: revnetConfiguration,
154
+ terminalConfigurations: terminalConfigurations,
155
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
156
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("REV"))
157
+ })
158
+ });
159
+ }
160
+
161
+ function _getRevnetConfig() internal view returns (MetaTxProjectConfig memory) {
162
+ string memory name = "NANA";
163
+ string memory symbol = "$NANA";
164
+ string memory projectUri = "ipfs://QmNRHT91HcDgMcenebYX7rJigt77cgNxosvuhX21wkF3tx";
165
+ uint8 decimals = 18;
166
+ uint256 decimalMultiplier = 10 ** decimals;
167
+
168
+ JBAccountingContext[] memory accountingContextsToAccept = new JBAccountingContext[](2);
169
+ accountingContextsToAccept[0] = JBAccountingContext({
170
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
171
+ });
172
+ accountingContextsToAccept[1] =
173
+ JBAccountingContext({token: address(TOKEN), decimals: 6, currency: uint32(uint160(address(TOKEN)))});
174
+
175
+ JBTerminalConfig[] memory terminalConfigurations = new JBTerminalConfig[](1);
176
+ terminalConfigurations[0] =
177
+ JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: accountingContextsToAccept});
178
+
179
+ JBSplit[] memory splits = new JBSplit[](1);
180
+ splits[0].beneficiary = payable(multisig());
181
+ splits[0].percent = 10_000;
182
+
183
+ REVStageConfig[] memory stageConfigurations = new REVStageConfig[](1);
184
+ REVAutoIssuance[] memory issuanceConfs = new REVAutoIssuance[](1);
185
+ issuanceConfs[0] = REVAutoIssuance({
186
+ // forge-lint: disable-next-line(unsafe-typecast)
187
+ chainId: uint32(block.chainid),
188
+ // forge-lint: disable-next-line(unsafe-typecast)
189
+ count: uint104(70_000 * decimalMultiplier),
190
+ beneficiary: multisig()
191
+ });
192
+
193
+ stageConfigurations[0] = REVStageConfig({
194
+ startsAtOrAfter: uint40(block.timestamp),
195
+ autoIssuances: issuanceConfs,
196
+ splitPercent: 2000,
197
+ splits: splits,
198
+ // forge-lint: disable-next-line(unsafe-typecast)
199
+ initialIssuance: uint112(1000 * decimalMultiplier),
200
+ issuanceCutFrequency: 90 days,
201
+ issuanceCutPercent: JBConstants.MAX_WEIGHT_CUT_PERCENT / 2,
202
+ cashOutTaxRate: 6000,
203
+ extraMetadata: 0
204
+ });
205
+
206
+ REVConfig memory revnetConfiguration = REVConfig({
207
+ // forge-lint: disable-next-line(named-struct-fields)
208
+ description: REVDescription(name, symbol, projectUri, "NANA_TOKEN"),
209
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
210
+ splitOperator: multisig(),
211
+ stageConfigurations: stageConfigurations
212
+ });
213
+
214
+ return MetaTxProjectConfig({
215
+ configuration: revnetConfiguration,
216
+ terminalConfigurations: terminalConfigurations,
217
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
218
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("NANA"))
219
+ })
220
+ });
221
+ }
222
+
223
+ // =========================================================================
224
+ // Helper: construct a ForwardRequestData from a ForwardRequest
225
+ // =========================================================================
226
+
227
+ function _forgeRequestData(
228
+ uint256 value,
229
+ uint256 nonce,
230
+ uint48 deadline,
231
+ bytes memory data,
232
+ address target
233
+ )
234
+ internal
235
+ view
236
+ returns (ERC2771Forwarder.ForwardRequestData memory)
237
+ {
238
+ ForwardRequest memory request = ForwardRequest({
239
+ from: signerAddr, to: target, value: value, gas: 3_000_000, nonce: nonce, deadline: deadline, data: data
240
+ });
241
+
242
+ bytes32 digest = erc2771Forwarder.structHash(request);
243
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest);
244
+ bytes memory signature = abi.encodePacked(r, s, v);
245
+
246
+ return ERC2771Forwarder.ForwardRequestData({
247
+ from: request.from,
248
+ to: request.to,
249
+ value: request.value,
250
+ gas: request.gas,
251
+ deadline: request.deadline,
252
+ data: request.data,
253
+ signature: signature
254
+ });
255
+ }
256
+
257
+ function setUp() public override {
258
+ super.setUp();
259
+
260
+ signerPrivateKey = 0xA11CE;
261
+ relayerPrivateKey = 0xB0B;
262
+ signerAddr = vm.addr(signerPrivateKey);
263
+ relayerAddr = vm.addr(relayerPrivateKey);
264
+
265
+ // Deploy ERC2771ForwarderMock at the FORWARDER_ADDRESS using deployCodeTo.
266
+ deployCodeTo("ERC2771ForwarderMock.sol", abi.encode("ERC2771Forwarder"), FORWARDER_ADDRESS);
267
+ erc2771Forwarder = ERC2771ForwarderMock(FORWARDER_ADDRESS);
268
+
269
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
270
+
271
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
272
+ HOOK_STORE = new JB721TiersHookStore();
273
+ EXAMPLE_HOOK = new JB721TiersHook(
274
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
275
+ );
276
+ ADDRESS_REGISTRY = new JBAddressRegistry();
277
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
278
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
279
+ MOCK_BUYBACK = new MockBuybackDataHook();
280
+ TOKEN = new MockERC20("1/2 ETH", "1/2");
281
+
282
+ MockPriceFeed priceFeed = new MockPriceFeed(1e21, 6);
283
+ vm.prank(multisig());
284
+ jbPrices()
285
+ .addPriceFeedFor(0, uint32(uint160(address(TOKEN))), uint32(uint160(JBConstants.NATIVE_TOKEN)), priceFeed);
286
+
287
+ // Deploy LOANS_CONTRACT with the forwarder as trusted forwarder.
288
+ LOANS_CONTRACT = new REVLoans({
289
+ controller: jbController(),
290
+ projects: jbProjects(),
291
+ revId: FEE_PROJECT_ID,
292
+ owner: address(this),
293
+ permit2: permit2(),
294
+ trustedForwarder: FORWARDER_ADDRESS
295
+ });
296
+
297
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
298
+ jbController(),
299
+ SUCKER_REGISTRY,
300
+ FEE_PROJECT_ID,
301
+ HOOK_DEPLOYER,
302
+ PUBLISHER,
303
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
304
+ address(LOANS_CONTRACT),
305
+ FORWARDER_ADDRESS
306
+ );
307
+
308
+ // Approve the deployer to configure the project.
309
+ vm.prank(address(multisig()));
310
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
311
+
312
+ // Deploy fee project.
313
+ MetaTxProjectConfig memory feeProjectConfig = _getFeeProjectConfig();
314
+ vm.prank(address(multisig()));
315
+ REV_DEPLOYER.deployFor({
316
+ revnetId: FEE_PROJECT_ID,
317
+ configuration: feeProjectConfig.configuration,
318
+ terminalConfigurations: feeProjectConfig.terminalConfigurations,
319
+ suckerDeploymentConfiguration: feeProjectConfig.suckerDeploymentConfiguration,
320
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
321
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
322
+ });
323
+
324
+ // Deploy second revnet.
325
+ MetaTxProjectConfig memory revnetConfig = _getRevnetConfig();
326
+ (REVNET_ID,) = REV_DEPLOYER.deployFor({
327
+ revnetId: 0,
328
+ configuration: revnetConfig.configuration,
329
+ terminalConfigurations: revnetConfig.terminalConfigurations,
330
+ suckerDeploymentConfiguration: revnetConfig.suckerDeploymentConfiguration,
331
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
332
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
333
+ });
334
+
335
+ // Fund the signer and relayer.
336
+ vm.deal(signerAddr, 1000e18);
337
+ vm.deal(relayerAddr, 1000e18);
338
+ }
339
+
340
+ // =========================================================================
341
+ // Test: ERC2771 trusted forwarder is correctly configured
342
+ // =========================================================================
343
+
344
+ /// @notice Verifies that the trusted forwarder is set correctly on REVLoans.
345
+ function test_erc2771_trustedForwarderIsSet() public view {
346
+ assertTrue(LOANS_CONTRACT.isTrustedForwarder(FORWARDER_ADDRESS), "Forwarder should be trusted");
347
+ assertFalse(LOANS_CONTRACT.isTrustedForwarder(address(0x999)), "Random address should not be trusted");
348
+ }
349
+
350
+ // =========================================================================
351
+ // Test: borrow via trusted forwarder — loan owned by signer, not relayer
352
+ // =========================================================================
353
+
354
+ /// @notice When borrowFrom() is called through the trusted forwarder, the loan NFT
355
+ /// should be minted to the actual signer (from the appended calldata),
356
+ /// not the relayer who submitted the transaction.
357
+ function test_erc2771_borrowViaForwarder() public {
358
+ // First, signer pays into the revnet to get tokens.
359
+ vm.prank(signerAddr);
360
+ uint256 tokenCount =
361
+ jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
362
+ assertTrue(tokenCount > 0, "Should receive tokens from payment");
363
+
364
+ // Check borrowable amount.
365
+ uint256 borrowable =
366
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
367
+ vm.assume(borrowable > 0);
368
+
369
+ // Mock permission for loans contract to burn the signer's tokens.
370
+ mockExpect(
371
+ address(jbPermissions()),
372
+ abi.encodeCall(
373
+ IJBPermissions.hasPermission, (address(LOANS_CONTRACT), signerAddr, REVNET_ID, 11, true, true)
374
+ ),
375
+ abi.encode(true)
376
+ );
377
+
378
+ // Encode the borrowFrom call.
379
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
380
+
381
+ bytes memory borrowData = abi.encodeWithSelector(
382
+ IREVLoans.borrowFrom.selector,
383
+ REVNET_ID,
384
+ source,
385
+ 0, // minBorrowAmount
386
+ tokenCount,
387
+ payable(signerAddr),
388
+ uint256(25) // MIN_PREPAID_FEE_PERCENT
389
+ );
390
+
391
+ // Build the forwarded request signed by the signer.
392
+ ERC2771Forwarder.ForwardRequestData memory requestData = _forgeRequestData({
393
+ value: 0, nonce: 0, deadline: uint48(block.timestamp + 1), data: borrowData, target: address(LOANS_CONTRACT)
394
+ });
395
+
396
+ // Relayer submits the meta-tx.
397
+ vm.prank(relayerAddr);
398
+ erc2771Forwarder.execute{value: 0}(requestData);
399
+
400
+ // Verify the loan was created and is owned by the signer (not the relayer).
401
+ uint256 loansBalance = LOANS_CONTRACT.balanceOf(signerAddr);
402
+ assertTrue(loansBalance > 0, "Signer should own the loan NFT");
403
+
404
+ uint256 relayerLoans = LOANS_CONTRACT.balanceOf(relayerAddr);
405
+ assertEq(relayerLoans, 0, "Relayer should not own any loan NFTs");
406
+ }
407
+
408
+ // =========================================================================
409
+ // Test: repay via trusted forwarder
410
+ // =========================================================================
411
+
412
+ /// @notice When repayLoan() is called through the trusted forwarder, the loan owner
413
+ /// check should use _msgSender() (the signer), not msg.sender (the forwarder).
414
+ function test_erc2771_repayViaForwarder() public {
415
+ // Signer pays to get tokens and creates a loan.
416
+ vm.prank(signerAddr);
417
+ uint256 tokenCount =
418
+ jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
419
+
420
+ uint256 borrowable =
421
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
422
+ vm.assume(borrowable > 0);
423
+
424
+ // Mock permission.
425
+ mockExpect(
426
+ address(jbPermissions()),
427
+ abi.encodeCall(
428
+ IJBPermissions.hasPermission, (address(LOANS_CONTRACT), signerAddr, REVNET_ID, 11, true, true)
429
+ ),
430
+ abi.encode(true)
431
+ );
432
+
433
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
434
+
435
+ vm.prank(signerAddr);
436
+ (uint256 loanId,) = LOANS_CONTRACT.borrowFrom(REVNET_ID, source, 0, tokenCount, payable(signerAddr), 25);
437
+
438
+ REVLoan memory loan = LOANS_CONTRACT.loanOf(loanId);
439
+ assertTrue(loan.amount > 0, "Loan should exist");
440
+
441
+ // Calculate repay amount.
442
+ uint256 sourceFee = LOANS_CONTRACT.determineSourceFeeAmount(loan, loan.amount);
443
+ uint256 totalRepay = loan.amount + sourceFee;
444
+
445
+ // Encode the repayLoan call.
446
+ bytes memory repayData = abi.encodeWithSelector(
447
+ IREVLoans.repayLoan.selector,
448
+ loanId,
449
+ totalRepay * 2, // maxRepayBorrowAmount
450
+ loan.collateral,
451
+ payable(signerAddr),
452
+ JBSingleAllowance({sigDeadline: 0, amount: 0, expiration: 0, nonce: 0, signature: ""})
453
+ );
454
+
455
+ // Build the forwarded request signed by the signer.
456
+ ERC2771Forwarder.ForwardRequestData memory requestData = _forgeRequestData({
457
+ value: totalRepay * 2,
458
+ nonce: 0,
459
+ deadline: uint48(block.timestamp + 1),
460
+ data: repayData,
461
+ target: address(LOANS_CONTRACT)
462
+ });
463
+
464
+ // Relayer submits the meta-tx with ETH.
465
+ vm.prank(relayerAddr);
466
+ erc2771Forwarder.execute{value: totalRepay * 2}(requestData);
467
+
468
+ // Verify loan was repaid: collateral should be zero.
469
+ uint256 totalCollateral = LOANS_CONTRACT.totalCollateralOf(REVNET_ID);
470
+ assertEq(totalCollateral, 0, "All collateral should be returned after repay via forwarder");
471
+ }
472
+
473
+ // =========================================================================
474
+ // Test: untrusted forwarder uses msg.sender, not appended address
475
+ // =========================================================================
476
+
477
+ /// @notice When a call is forwarded through a forwarder that is NOT the trusted one,
478
+ /// OpenZeppelin's ERC2771Forwarder checks `isTrustedForwarder` on the target
479
+ /// and reverts with `ERC2771UntrustfulTarget`. This prevents identity spoofing
480
+ /// at the forwarder level itself.
481
+ function test_erc2771_untrustedForwarder_usesMsgSender() public {
482
+ // Deploy a different forwarder at a different address (NOT the trusted one).
483
+ address untrustedForwarderAddr = address(789_012);
484
+ deployCodeTo("ERC2771ForwarderMock.sol", abi.encode("UntrustedForwarder"), untrustedForwarderAddr);
485
+ ERC2771ForwarderMock untrustedForwarder = ERC2771ForwarderMock(untrustedForwarderAddr);
486
+
487
+ // Verify the untrusted forwarder is not trusted by the LOANS_CONTRACT.
488
+ assertFalse(
489
+ LOANS_CONTRACT.isTrustedForwarder(untrustedForwarderAddr), "Untrusted forwarder should not be trusted"
490
+ );
491
+
492
+ // Signer pays to get tokens.
493
+ vm.prank(signerAddr);
494
+ uint256 tokenCount =
495
+ jbMultiTerminal().pay{value: 10e18}(REVNET_ID, JBConstants.NATIVE_TOKEN, 10e18, signerAddr, 0, "", "");
496
+
497
+ uint256 borrowable =
498
+ LOANS_CONTRACT.borrowableAmountFrom(REVNET_ID, tokenCount, 18, uint32(uint160(JBConstants.NATIVE_TOKEN)));
499
+ vm.assume(borrowable > 0);
500
+
501
+ // Encode the borrowFrom call.
502
+ REVLoanSource memory source = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
503
+
504
+ bytes memory borrowData = abi.encodeWithSelector(
505
+ IREVLoans.borrowFrom.selector, REVNET_ID, source, 0, tokenCount, payable(signerAddr), uint256(25)
506
+ );
507
+
508
+ // Build a forwarded request using the signer's key via the UNTRUSTED forwarder.
509
+ ForwardRequest memory request = ForwardRequest({
510
+ from: signerAddr,
511
+ to: address(LOANS_CONTRACT),
512
+ value: 0,
513
+ gas: 3_000_000,
514
+ nonce: 0,
515
+ deadline: uint48(block.timestamp + 1),
516
+ data: borrowData
517
+ });
518
+
519
+ bytes32 digest = untrustedForwarder.structHash(request);
520
+ (uint8 v, bytes32 r, bytes32 s) = vm.sign(signerPrivateKey, digest);
521
+ bytes memory signature = abi.encodePacked(r, s, v);
522
+
523
+ ERC2771Forwarder.ForwardRequestData memory requestData = ERC2771Forwarder.ForwardRequestData({
524
+ from: request.from,
525
+ to: request.to,
526
+ value: request.value,
527
+ gas: request.gas,
528
+ deadline: request.deadline,
529
+ data: request.data,
530
+ signature: signature
531
+ });
532
+
533
+ // OpenZeppelin's ERC2771Forwarder.execute() checks isTrustedForwarder on the
534
+ // target contract. Since the untrusted forwarder is not the trusted one,
535
+ // it reverts with ERC2771UntrustfulTarget(target, forwarder).
536
+ vm.prank(relayerAddr);
537
+ vm.expectRevert(
538
+ abi.encodeWithSelector(
539
+ ERC2771Forwarder.ERC2771UntrustfulTarget.selector, address(LOANS_CONTRACT), untrustedForwarderAddr
540
+ )
541
+ );
542
+ untrustedForwarder.execute{value: 0}(requestData);
543
+
544
+ // Verify no loan was created for the signer.
545
+ uint256 signerLoansBalance = LOANS_CONTRACT.balanceOf(signerAddr);
546
+ assertEq(signerLoansBalance, 0, "No loan should exist since untrusted forwarder was rejected");
547
+ }
548
+
549
+ // =========================================================================
550
+ // Test: forwarder is correctly deployed and functional
551
+ // =========================================================================
552
+
553
+ /// @notice Sanity check that the forwarder mock deployed correctly.
554
+ function test_erc2771_forwarderDeployed() public view {
555
+ assertTrue(erc2771Forwarder.deployed(), "Forwarder should report as deployed");
556
+ }
557
+ }
@@ -1,22 +1,29 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity 0.8.26;
3
3
 
4
+ // forge-lint: disable-next-line(unaliased-plain-import)
4
5
  import "forge-std/Test.sol";
6
+ // forge-lint: disable-next-line(unaliased-plain-import)
5
7
  import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
8
+ // forge-lint: disable-next-line(unaliased-plain-import)
6
9
  import /* {*} from */ "./../src/REVDeployer.sol";
10
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
11
  import "@croptop/core-v6/src/CTPublisher.sol";
8
12
  import {MockBuybackDataHookMintPath} from "./mock/MockBuybackDataHookMintPath.sol";
9
- import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
13
+ // forge-lint: disable-next-line(unaliased-plain-import)
10
14
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
15
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
16
  import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
17
+ // forge-lint: disable-next-line(unaliased-plain-import)
12
18
  import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
19
+ // forge-lint: disable-next-line(unaliased-plain-import)
13
20
  import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
21
+ // forge-lint: disable-next-line(unaliased-plain-import)
14
22
  import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
15
23
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
24
  import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
17
25
  import {REVLoans} from "../src/REVLoans.sol";
18
26
  import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
19
- import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
20
27
  import {REVDescription} from "../src/structs/REVDescription.sol";
21
28
  import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
22
29
  import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
@@ -30,28 +37,39 @@ import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBefor
30
37
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
31
38
  import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
32
39
  import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
33
- import {REVCroptopAllowedPost} from "../src/structs/REVCroptopAllowedPost.sol";
34
40
 
35
41
  /// @notice Regression tests for the empty buyback hook specifications fix.
36
42
  /// When JBBuybackHook determines minting is cheaper than swapping, it returns an empty
37
43
  /// hookSpecifications array. Before the fix, REVDeployer.beforePayRecordedWith would
38
44
  /// Panic(0x32) (array out-of-bounds) when accessing buybackHookSpecifications[0].
39
45
  contract TestEmptyBuybackSpecs is TestBaseWorkflow {
46
+ // forge-lint: disable-next-line(mixed-case-variable)
40
47
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
41
48
 
49
+ // forge-lint: disable-next-line(mixed-case-variable)
42
50
  REVDeployer REV_DEPLOYER;
51
+ // forge-lint: disable-next-line(mixed-case-variable)
43
52
  JB721TiersHook EXAMPLE_HOOK;
53
+ // forge-lint: disable-next-line(mixed-case-variable)
44
54
  IJB721TiersHookDeployer HOOK_DEPLOYER;
55
+ // forge-lint: disable-next-line(mixed-case-variable)
45
56
  IJB721TiersHookStore HOOK_STORE;
57
+ // forge-lint: disable-next-line(mixed-case-variable)
46
58
  IJBAddressRegistry ADDRESS_REGISTRY;
59
+ // forge-lint: disable-next-line(mixed-case-variable)
47
60
  IREVLoans LOANS_CONTRACT;
61
+ // forge-lint: disable-next-line(mixed-case-variable)
48
62
  IJBSuckerRegistry SUCKER_REGISTRY;
63
+ // forge-lint: disable-next-line(mixed-case-variable)
49
64
  CTPublisher PUBLISHER;
65
+ // forge-lint: disable-next-line(mixed-case-variable)
50
66
  MockBuybackDataHookMintPath MOCK_BUYBACK_MINT_PATH;
51
67
 
68
+ // forge-lint: disable-next-line(mixed-case-variable)
52
69
  uint256 FEE_PROJECT_ID;
53
70
 
54
71
  address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
72
+ // forge-lint: disable-next-line(mixed-case-variable)
55
73
  address USER = makeAddr("user");
56
74
 
57
75
  function setUp() public override {
@@ -117,6 +135,7 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow {
117
135
  });
118
136
 
119
137
  cfg = REVConfig({
138
+ // forge-lint: disable-next-line(named-struct-fields)
120
139
  description: REVDescription("Test", "TST", "ipfs://test", "TEST_SALT"),
121
140
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
122
141
  splitOperator: multisig(),
@@ -144,6 +163,7 @@ contract TestEmptyBuybackSpecs is TestBaseWorkflow {
144
163
 
145
164
  (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
146
165
  _buildMinimalConfig();
166
+ // forge-lint: disable-next-line(named-struct-fields)
147
167
  cfg.description = REVDescription("Test2", "TS2", "ipfs://test2", "TEST_SALT_2");
148
168
 
149
169
  (revnetId,) = REV_DEPLOYER.deployFor({