@rev-net/core-v6 0.0.34 → 0.0.35

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.
package/ARCHITECTURE.md CHANGED
@@ -82,6 +82,18 @@ The repo does not replace core treasury accounting. Its critical economic logic
82
82
  - If you change borrowability, re-check cash-out-delay gating, omnichain surplus inputs, and local-surplus caps together.
83
83
  - If you change hook composition, re-check 721 split handling, buyback assumptions, and mint-permission flows.
84
84
 
85
+ ## Cross-Chain Configuration Hash
86
+
87
+ `REVDeployer` produces an `encodedConfigurationHash` for each revnet that determines sucker deployment salts. This hash commits the revnet's identity across chains. It includes:
88
+
89
+ - `baseCurrency`, `description.name`, `description.ticker`, `description.salt`
90
+ - Terminal addresses (order-sensitive)
91
+ - Stage parameters (timing, issuance, splits, tax rates, auto-issuances)
92
+
93
+ Terminal addresses are included because they are deployed deterministically at the same address across chains. Accounting contexts (token addresses) are excluded because tokens like USDC legitimately differ per chain.
94
+
95
+ This means a revnet can only expand to a new chain if it uses the exact same terminal contract it used on the host chain. Different terminal addresses produce a different hash, preventing accidental cross-chain mismatches in sucker deployments.
96
+
85
97
  ## Canonical Checks
86
98
 
87
99
  - cash-out-delay interaction with loans:
@@ -90,6 +102,8 @@ The repo does not replace core treasury accounting. Its critical economic logic
90
102
  `test/TestStageTransitionBorrowable.t.sol`
91
103
  - omnichain or phantom-surplus edge cases:
92
104
  `test/audit/CodexPhantomSurplusTerminal.t.sol`
105
+ - terminal encoding in configuration hash:
106
+ `test/TestTerminalEncodingInHash.t.sol`
93
107
 
94
108
  ## Source Map
95
109
 
package/RISKS.md CHANGED
@@ -86,4 +86,4 @@ The model assumes that attempts to inflate surplus through donations are not pro
86
86
 
87
87
  ### 8.5 Omnichain terminal expansion inherits remote-chain trust
88
88
 
89
- A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is accepted because revnet terminals are set and fixed on deploy for the initial chain, but expansion to new chains inherently requires trust in the new chain's terminal infrastructure, bridge integrity, and deployment configuration. Project operators should treat each chain expansion as a trust-boundary decision.
89
+ A project that expands to a new chain can register additional terminals on that chain. Because borrowability calculations aggregate surplus from all registered terminals across all chains, a compromised or misconfigured terminal on a remote chain can corrupt the project's surplus accounting globally. This is mitigated by including terminal addresses in the `encodedConfigurationHash` cross-chain expansions via suckers must use the exact same terminal address as the host chain. Terminal addresses are deterministic across chains (same CREATE2 deployment), so this prevents expansions from silently using a different terminal. Project operators should still treat each chain expansion as a trust-boundary decision since bridge integrity and network assumptions remain outside protocol control.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rev-net/core-v6",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -956,6 +956,16 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
956
956
  configuration.description.salt
957
957
  );
958
958
 
959
+ // Include terminal addresses in the hash so cross-chain expansions must use the same terminals.
960
+ // Terminal addresses are deterministic across chains. Accounting contexts are excluded because
961
+ // token addresses (e.g. USDC) legitimately differ per chain.
962
+ for (uint256 i; i < terminalConfigurations.length;) {
963
+ encodedConfiguration = abi.encode(encodedConfiguration, terminalConfigurations[i].terminal);
964
+ unchecked {
965
+ ++i;
966
+ }
967
+ }
968
+
959
969
  // Initialize fund access limit groups for the loan contract.
960
970
  JBFundAccessLimitGroup[] memory fundAccessLimitGroups =
961
971
  _makeLoanFundAccessLimits({terminalConfigurations: terminalConfigurations});
@@ -0,0 +1,326 @@
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 {REVEmpty721Config} from "./helpers/REVEmpty721Config.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 {REVLoans} from "../src/REVLoans.sol";
28
+ import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
29
+ import {REVDescription} from "../src/structs/REVDescription.sol";
30
+ import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
31
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
32
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
33
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
34
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
35
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
36
+ import {JB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/JB721CheckpointsDeployer.sol";
37
+ import {IJB721CheckpointsDeployer} from "@bananapus/721-hook-v6/src/interfaces/IJB721CheckpointsDeployer.sol";
38
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
39
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
40
+ import {REVOwner} from "../src/REVOwner.sol";
41
+ import {IREVDeployer} from "../src/interfaces/IREVDeployer.sol";
42
+ import {MockSuckerRegistry} from "./mock/MockSuckerRegistry.sol";
43
+ import {MockBuybackDataHook} from "./mock/MockBuybackDataHook.sol";
44
+
45
+ /// @notice Tests that terminal addresses are included in the encoded configuration hash.
46
+ contract TestTerminalEncodingInHash is TestBaseWorkflow {
47
+ using JBRulesetMetadataResolver for JBRuleset;
48
+
49
+ // forge-lint: disable-next-line(mixed-case-variable)
50
+ bytes32 REV_DEPLOYER_SALT = "REVDeployer";
51
+
52
+ // forge-lint: disable-next-line(mixed-case-variable)
53
+ REVDeployer REV_DEPLOYER;
54
+ // forge-lint: disable-next-line(mixed-case-variable)
55
+ REVOwner REV_OWNER;
56
+ // forge-lint: disable-next-line(mixed-case-variable)
57
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
58
+ // forge-lint: disable-next-line(mixed-case-variable)
59
+ IJB721TiersHookStore HOOK_STORE;
60
+ // forge-lint: disable-next-line(mixed-case-variable)
61
+ IJBAddressRegistry ADDRESS_REGISTRY;
62
+ // forge-lint: disable-next-line(mixed-case-variable)
63
+ IREVLoans LOANS_CONTRACT;
64
+ // forge-lint: disable-next-line(mixed-case-variable)
65
+ IJBSuckerRegistry SUCKER_REGISTRY;
66
+ // forge-lint: disable-next-line(mixed-case-variable)
67
+ CTPublisher PUBLISHER;
68
+ // forge-lint: disable-next-line(mixed-case-variable)
69
+ MockBuybackDataHook MOCK_BUYBACK;
70
+
71
+ // forge-lint: disable-next-line(mixed-case-variable)
72
+ uint256 FEE_PROJECT_ID;
73
+
74
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
75
+
76
+ function setUp() public override {
77
+ super.setUp();
78
+
79
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
80
+
81
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
82
+ HOOK_STORE = new JB721TiersHookStore();
83
+ JB721TiersHook exampleHook = new JB721TiersHook(
84
+ jbDirectory(),
85
+ jbPermissions(),
86
+ jbPrices(),
87
+ jbRulesets(),
88
+ HOOK_STORE,
89
+ jbSplits(),
90
+ IJB721CheckpointsDeployer(address(new JB721CheckpointsDeployer())),
91
+ multisig()
92
+ );
93
+ ADDRESS_REGISTRY = new JBAddressRegistry();
94
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(exampleHook, HOOK_STORE, ADDRESS_REGISTRY, multisig());
95
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
96
+ MOCK_BUYBACK = new MockBuybackDataHook();
97
+
98
+ LOANS_CONTRACT = new REVLoans({
99
+ controller: jbController(),
100
+ suckerRegistry: IJBSuckerRegistry(address(new MockSuckerRegistry())),
101
+ revId: FEE_PROJECT_ID,
102
+ owner: address(this),
103
+ permit2: permit2(),
104
+ trustedForwarder: TRUSTED_FORWARDER
105
+ });
106
+
107
+ REV_OWNER = new REVOwner(
108
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
109
+ jbDirectory(),
110
+ FEE_PROJECT_ID,
111
+ SUCKER_REGISTRY,
112
+ address(LOANS_CONTRACT),
113
+ address(0)
114
+ );
115
+
116
+ REV_DEPLOYER = new REVDeployer{salt: REV_DEPLOYER_SALT}(
117
+ jbController(),
118
+ SUCKER_REGISTRY,
119
+ FEE_PROJECT_ID,
120
+ HOOK_DEPLOYER,
121
+ PUBLISHER,
122
+ IJBBuybackHookRegistry(address(MOCK_BUYBACK)),
123
+ address(LOANS_CONTRACT),
124
+ TRUSTED_FORWARDER,
125
+ address(REV_OWNER)
126
+ );
127
+
128
+ REV_OWNER.setDeployer(REV_DEPLOYER);
129
+
130
+ vm.prank(multisig());
131
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
132
+
133
+ // Deploy fee project.
134
+ _deployFeeProject();
135
+ }
136
+
137
+ /// @notice Two revnets with identical base config but different terminals produce different hashes.
138
+ function test_differentTerminals_produceDifferentHashes() public {
139
+ // Deploy revnet A with the primary multi-terminal (same description salt for both).
140
+ (uint256 revnetA,) = REV_DEPLOYER.deployFor({
141
+ revnetId: 0,
142
+ configuration: _baseRevConfig("DIFF_TERM"),
143
+ terminalConfigurations: _terminalConfigs(jbMultiTerminal()),
144
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
145
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("A")
146
+ }),
147
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
148
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
149
+ });
150
+
151
+ // Deploy revnet B with the secondary multi-terminal (same config, only terminal differs).
152
+ (uint256 revnetB,) = REV_DEPLOYER.deployFor({
153
+ revnetId: 0,
154
+ configuration: _baseRevConfig("DIFF_TERM"),
155
+ terminalConfigurations: _terminalConfigs(jbMultiTerminal2()),
156
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
157
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("B")
158
+ }),
159
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
160
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
161
+ });
162
+
163
+ bytes32 hashA = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetA);
164
+ bytes32 hashB = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetB);
165
+
166
+ assertNotEq(hashA, hashB, "Different terminals must produce different configuration hashes");
167
+ }
168
+
169
+ /// @notice The hash includes the terminal address — verify by computing it off-chain.
170
+ function test_hashIncludesTerminalAddress() public {
171
+ // Deploy a revnet.
172
+ (uint256 revnetId,) = REV_DEPLOYER.deployFor({
173
+ revnetId: 0,
174
+ configuration: _baseRevConfig("VERIFY"),
175
+ terminalConfigurations: _terminalConfigs(jbMultiTerminal()),
176
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
177
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("VERIFY")
178
+ }),
179
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
180
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
181
+ });
182
+
183
+ // Recompute the expected hash manually.
184
+ bytes memory encodedConfiguration = abi.encode(
185
+ uint32(uint160(JBConstants.NATIVE_TOKEN)), // baseCurrency
186
+ "Terminal Test", // name
187
+ "TERM", // ticker
188
+ bytes32("VERIFY") // salt
189
+ );
190
+ // Terminal address encoding.
191
+ encodedConfiguration = abi.encode(encodedConfiguration, jbMultiTerminal());
192
+ // Stage encoding.
193
+ encodedConfiguration = abi.encode(
194
+ encodedConfiguration,
195
+ block.timestamp, // startsAtOrAfter
196
+ uint256(0), // splitPercent
197
+ uint112(1000e18), // initialIssuance
198
+ uint256(0), // issuanceCutFrequency
199
+ uint256(0), // issuanceCutPercent
200
+ uint256(5000) // cashOutTaxRate
201
+ );
202
+ bytes32 expectedHash = keccak256(encodedConfiguration);
203
+
204
+ assertEq(
205
+ REV_DEPLOYER.hashedEncodedConfigurationOf(revnetId),
206
+ expectedHash,
207
+ "On-chain hash must match off-chain computation including terminal address"
208
+ );
209
+ }
210
+
211
+ /// @notice Terminal ordering matters — [A, B] != [B, A].
212
+ function test_terminalOrder_affectsHash() public {
213
+ // Deploy revnet with terminals in order [main, alt].
214
+ JBTerminalConfig[] memory tcAB = new JBTerminalConfig[](2);
215
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
216
+ acc[0] = JBAccountingContext({
217
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
218
+ });
219
+ tcAB[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
220
+ tcAB[1] = JBTerminalConfig({terminal: jbMultiTerminal2(), accountingContextsToAccept: acc});
221
+
222
+ (uint256 revnetAB,) = REV_DEPLOYER.deployFor({
223
+ revnetId: 0,
224
+ configuration: _baseRevConfig("ORDER"),
225
+ terminalConfigurations: tcAB,
226
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
227
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("AB")
228
+ }),
229
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
230
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
231
+ });
232
+
233
+ // Deploy revnet with terminals in order [alt, main].
234
+ JBTerminalConfig[] memory tcBA = new JBTerminalConfig[](2);
235
+ tcBA[0] = JBTerminalConfig({terminal: jbMultiTerminal2(), accountingContextsToAccept: acc});
236
+ tcBA[1] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
237
+
238
+ (uint256 revnetBA,) = REV_DEPLOYER.deployFor({
239
+ revnetId: 0,
240
+ configuration: _baseRevConfig("ORDER"),
241
+ terminalConfigurations: tcBA,
242
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
243
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("BA")
244
+ }),
245
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
246
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
247
+ });
248
+
249
+ bytes32 hashAB = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetAB);
250
+ bytes32 hashBA = REV_DEPLOYER.hashedEncodedConfigurationOf(revnetBA);
251
+
252
+ assertNotEq(hashAB, hashBA, "Terminal order must affect the configuration hash");
253
+ }
254
+
255
+ // ─── Helpers
256
+ // ───────────────────────────────────────────────────────────────
257
+ // //
258
+
259
+ function _baseRevConfig(bytes32 salt) internal view returns (REVConfig memory) {
260
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
261
+ stages[0] = REVStageConfig({
262
+ startsAtOrAfter: uint48(block.timestamp),
263
+ autoIssuances: new REVAutoIssuance[](0),
264
+ splitPercent: 0,
265
+ splits: new JBSplit[](0),
266
+ initialIssuance: uint112(1000e18),
267
+ issuanceCutFrequency: 0,
268
+ issuanceCutPercent: 0,
269
+ cashOutTaxRate: 5000,
270
+ extraMetadata: 0
271
+ });
272
+ return REVConfig({
273
+ description: REVDescription("Terminal Test", "TERM", "", salt),
274
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
275
+ splitOperator: multisig(),
276
+ stageConfigurations: stages
277
+ });
278
+ }
279
+
280
+ function _terminalConfigs(IJBMultiTerminal terminal) internal pure returns (JBTerminalConfig[] memory tc) {
281
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
282
+ acc[0] = JBAccountingContext({
283
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
284
+ });
285
+ tc = new JBTerminalConfig[](1);
286
+ tc[0] = JBTerminalConfig({terminal: terminal, accountingContextsToAccept: acc});
287
+ }
288
+
289
+ function _deployFeeProject() internal {
290
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
291
+ acc[0] = JBAccountingContext({
292
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
293
+ });
294
+ JBTerminalConfig[] memory tc = new JBTerminalConfig[](1);
295
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
296
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
297
+ stages[0] = REVStageConfig({
298
+ startsAtOrAfter: uint48(block.timestamp),
299
+ autoIssuances: new REVAutoIssuance[](0),
300
+ splitPercent: 0,
301
+ splits: new JBSplit[](0),
302
+ initialIssuance: uint112(1000e18),
303
+ issuanceCutFrequency: 0,
304
+ issuanceCutPercent: 0,
305
+ cashOutTaxRate: 0,
306
+ extraMetadata: 0
307
+ });
308
+ REVConfig memory feeConfig = REVConfig({
309
+ description: REVDescription("Fee Project", "FEE", "", bytes32("FEE")),
310
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
311
+ splitOperator: multisig(),
312
+ stageConfigurations: stages
313
+ });
314
+ vm.prank(multisig());
315
+ REV_DEPLOYER.deployFor({
316
+ revnetId: FEE_PROJECT_ID,
317
+ configuration: feeConfig,
318
+ terminalConfigurations: tc,
319
+ suckerDeploymentConfiguration: REVSuckerDeploymentConfig({
320
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256("FEE")
321
+ }),
322
+ tiered721HookConfiguration: REVEmpty721Config.empty721Config(uint32(uint160(JBConstants.NATIVE_TOKEN))),
323
+ allowedPosts: REVEmpty721Config.emptyAllowedPosts()
324
+ });
325
+ }
326
+ }