@rev-net/core-v6 0.0.11 → 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 (81) hide show
  1. package/ADMINISTRATION.md +7 -7
  2. package/ARCHITECTURE.md +11 -11
  3. package/AUDIT_INSTRUCTIONS.md +295 -0
  4. package/CHANGE_LOG.md +316 -0
  5. package/README.md +9 -6
  6. package/RISKS.md +180 -35
  7. package/SKILLS.md +9 -11
  8. package/STYLE_GUIDE.md +14 -1
  9. package/USER_JOURNEYS.md +489 -0
  10. package/package.json +9 -9
  11. package/script/Deploy.s.sol +124 -40
  12. package/script/helpers/RevnetCoreDeploymentLib.sol +19 -6
  13. package/src/REVDeployer.sol +183 -175
  14. package/src/REVLoans.sol +65 -28
  15. package/src/interfaces/IREVDeployer.sol +25 -23
  16. package/src/structs/REV721TiersHookFlags.sol +1 -0
  17. package/src/structs/REVAutoIssuance.sol +1 -0
  18. package/src/structs/REVBaseline721HookConfig.sol +1 -0
  19. package/src/structs/REVConfig.sol +1 -0
  20. package/src/structs/REVCroptopAllowedPost.sol +1 -0
  21. package/src/structs/REVDeploy721TiersHookConfig.sol +13 -14
  22. package/src/structs/REVDescription.sol +1 -0
  23. package/src/structs/REVLoan.sol +1 -0
  24. package/src/structs/REVLoanSource.sol +1 -0
  25. package/src/structs/REVStageConfig.sol +1 -0
  26. package/src/structs/REVSuckerDeploymentConfig.sol +1 -0
  27. package/test/REV.integrations.t.sol +148 -19
  28. package/test/REVAutoIssuanceFuzz.t.sol +31 -6
  29. package/test/REVDeployerRegressions.t.sol +47 -9
  30. package/test/REVInvincibility.t.sol +83 -19
  31. package/test/REVInvincibilityHandler.sol +29 -0
  32. package/test/REVLifecycle.t.sol +36 -6
  33. package/test/REVLoans.invariants.t.sol +64 -10
  34. package/test/REVLoansAttacks.t.sol +54 -9
  35. package/test/REVLoansFeeRecovery.t.sol +61 -15
  36. package/test/REVLoansFindings.t.sol +42 -9
  37. package/test/REVLoansRegressions.t.sol +33 -6
  38. package/test/REVLoansSourceFeeRecovery.t.sol +491 -0
  39. package/test/REVLoansSourced.t.sol +79 -17
  40. package/test/REVLoansUnSourced.t.sol +61 -10
  41. package/test/TestBurnHeldTokens.t.sol +47 -11
  42. package/test/TestCEIPattern.t.sol +37 -6
  43. package/test/TestCashOutCallerValidation.t.sol +41 -8
  44. package/test/TestConversionDocumentation.t.sol +50 -13
  45. package/test/TestCrossCurrencyReclaim.t.sol +584 -0
  46. package/test/TestCrossSourceReallocation.t.sol +37 -6
  47. package/test/TestERC2771MetaTx.t.sol +557 -0
  48. package/test/TestEmptyBuybackSpecs.t.sol +45 -10
  49. package/test/TestFlashLoanSurplus.t.sol +39 -7
  50. package/test/TestHookArrayOOB.t.sol +42 -13
  51. package/test/TestLiquidationBehavior.t.sol +37 -7
  52. package/test/TestLoanSourceRotation.t.sol +525 -0
  53. package/test/TestLongTailEconomics.t.sol +651 -0
  54. package/test/TestLowFindings.t.sol +80 -8
  55. package/test/TestMixedFixes.t.sol +43 -9
  56. package/test/TestPermit2Signatures.t.sol +657 -0
  57. package/test/TestReallocationSandwich.t.sol +384 -0
  58. package/test/TestRevnetRegressions.t.sol +324 -0
  59. package/test/TestSplitWeightAdjustment.t.sol +52 -13
  60. package/test/TestSplitWeightE2E.t.sol +53 -18
  61. package/test/TestSplitWeightFork.t.sol +66 -21
  62. package/test/TestStageTransitionBorrowable.t.sol +38 -6
  63. package/test/TestSwapTerminalPermission.t.sol +37 -7
  64. package/test/TestUint112Overflow.t.sol +39 -6
  65. package/test/TestZeroRepayment.t.sol +37 -6
  66. package/test/fork/ForkTestBase.sol +66 -17
  67. package/test/fork/TestCashOutFork.t.sol +9 -3
  68. package/test/fork/TestLoanBorrowFork.t.sol +1 -0
  69. package/test/fork/TestLoanCrossRulesetFork.t.sol +11 -3
  70. package/test/fork/TestLoanLiquidationFork.t.sol +1 -0
  71. package/test/fork/TestLoanReallocateFork.t.sol +1 -0
  72. package/test/fork/TestLoanRepayFork.t.sol +1 -0
  73. package/test/fork/TestLoanTransferFork.t.sol +133 -0
  74. package/test/fork/TestSplitWeightFork.t.sol +3 -0
  75. package/test/helpers/REVEmpty721Config.sol +46 -0
  76. package/test/mock/MockBuybackDataHook.sol +1 -0
  77. package/test/regression/TestBurnPermissionRequired.t.sol +267 -0
  78. package/test/regression/TestCrossRevnetLiquidation.t.sol +228 -0
  79. package/test/regression/TestCumulativeLoanCounter.t.sol +38 -8
  80. package/test/regression/TestLiquidateGapHandling.t.sol +40 -8
  81. package/test/regression/TestZeroPriceFeed.t.sol +396 -0
@@ -1,16 +1,25 @@
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";
6
- import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
8
+ // import /* {*} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
9
+ // forge-lint: disable-next-line(unaliased-plain-import)
7
10
  import /* {*} from */ "./../src/REVDeployer.sol";
11
+ // forge-lint: disable-next-line(unaliased-plain-import)
8
12
  import "@croptop/core-v6/src/CTPublisher.sol";
9
13
  import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
14
+ // forge-lint: disable-next-line(unaliased-plain-import)
10
15
  import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
16
+ // forge-lint: disable-next-line(unaliased-plain-import)
11
17
  import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
18
+ // forge-lint: disable-next-line(unaliased-plain-import)
12
19
  import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
20
+ // forge-lint: disable-next-line(unaliased-plain-import)
13
21
  import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
22
+ // forge-lint: disable-next-line(unaliased-plain-import)
14
23
  import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
15
24
  import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
16
25
  import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
@@ -29,25 +38,40 @@ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
29
38
  import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
30
39
  import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
31
40
  import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
41
+ import {REVEmpty721Config} from "./helpers/REVEmpty721Config.sol";
32
42
 
33
43
  /// @notice Tests for PR #13: cross-source reallocation prevention.
34
44
  contract TestCrossSourceReallocation is TestBaseWorkflow {
45
+ // forge-lint: disable-next-line(mixed-case-variable)
35
46
  bytes32 REV_DEPLOYER_SALT = "REVDeployer";
36
47
 
48
+ // forge-lint: disable-next-line(mixed-case-variable)
37
49
  REVDeployer REV_DEPLOYER;
50
+ // forge-lint: disable-next-line(mixed-case-variable)
38
51
  JB721TiersHook EXAMPLE_HOOK;
52
+ // forge-lint: disable-next-line(mixed-case-variable)
39
53
  IJB721TiersHookDeployer HOOK_DEPLOYER;
54
+ // forge-lint: disable-next-line(mixed-case-variable)
40
55
  IJB721TiersHookStore HOOK_STORE;
56
+ // forge-lint: disable-next-line(mixed-case-variable)
41
57
  IJBAddressRegistry ADDRESS_REGISTRY;
58
+ // forge-lint: disable-next-line(mixed-case-variable)
42
59
  IREVLoans LOANS_CONTRACT;
60
+ // forge-lint: disable-next-line(mixed-case-variable)
43
61
  MockERC20 TOKEN;
62
+ // forge-lint: disable-next-line(mixed-case-variable)
44
63
  IJBSuckerRegistry SUCKER_REGISTRY;
64
+ // forge-lint: disable-next-line(mixed-case-variable)
45
65
  CTPublisher PUBLISHER;
66
+ // forge-lint: disable-next-line(mixed-case-variable)
46
67
  MockBuybackDataHook MOCK_BUYBACK;
47
68
 
69
+ // forge-lint: disable-next-line(mixed-case-variable)
48
70
  uint256 FEE_PROJECT_ID;
71
+ // forge-lint: disable-next-line(mixed-case-variable)
49
72
  uint256 REVNET_ID;
50
73
 
74
+ // forge-lint: disable-next-line(mixed-case-variable)
51
75
  address USER = makeAddr("user");
52
76
 
53
77
  address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
@@ -57,8 +81,9 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
57
81
  FEE_PROJECT_ID = jbProjects().createFor(multisig());
58
82
  SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
59
83
  HOOK_STORE = new JB721TiersHookStore();
60
- EXAMPLE_HOOK =
61
- new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
84
+ EXAMPLE_HOOK = new JB721TiersHook(
85
+ jbDirectory(), jbPermissions(), jbPrices(), jbRulesets(), HOOK_STORE, jbSplits(), multisig()
86
+ );
62
87
  ADDRESS_REGISTRY = new JBAddressRegistry();
63
88
  HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
64
89
  PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
@@ -119,6 +144,7 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
119
144
  extraMetadata: 0
120
145
  });
121
146
  REVConfig memory cfg = REVConfig({
147
+ // forge-lint: disable-next-line(named-struct-fields)
122
148
  description: REVDescription("Revnet", "$REV", "ipfs://test", "REV_TOKEN"),
123
149
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
124
150
  splitOperator: multisig(),
@@ -131,7 +157,9 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
131
157
  terminalConfigurations: tc,
132
158
  suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
133
159
  deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
134
- })
160
+ }),
161
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
162
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
135
163
  });
136
164
  }
137
165
 
@@ -163,18 +191,21 @@ contract TestCrossSourceReallocation is TestBaseWorkflow {
163
191
  REVLoanSource[] memory ls = new REVLoanSource[](1);
164
192
  ls[0] = REVLoanSource({token: JBConstants.NATIVE_TOKEN, terminal: jbMultiTerminal()});
165
193
  REVConfig memory cfg = REVConfig({
194
+ // forge-lint: disable-next-line(named-struct-fields)
166
195
  description: REVDescription("NANA", "$NANA", "ipfs://test2", "NANA_TOKEN"),
167
196
  baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
168
197
  splitOperator: multisig(),
169
198
  stageConfigurations: stages
170
199
  });
171
- REVNET_ID = REV_DEPLOYER.deployFor({
200
+ (REVNET_ID,) = REV_DEPLOYER.deployFor({
172
201
  revnetId: 0,
173
202
  configuration: cfg,
174
203
  terminalConfigurations: tc,
175
204
  suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
176
205
  deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("NANA")
177
- })
206
+ }),
207
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
208
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
178
209
  });
179
210
  }
180
211
 
@@ -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
+ }