@rev-net/core-v6 0.0.34 → 0.0.36

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,12 @@ 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.
90
+
91
+ ### 8.6 Cross-chain surplus staleness
92
+
93
+ `REVLoans._borrowableAmountFrom` and `REVOwner.beforeCashOutRecordedWith` add `remoteSurplusOf()` and `remoteTotalSupplyOf()` to local values. These remote values update only when `toRemote()` is called on the peer chain -- no heartbeat or staleness check. Stale data can inflate per-token borrowable amounts when remote supply has grown since the last bridge message. Primary safeguard: borrowable is capped at `localSurplus` (REVLoans line 386-387), preventing extraction beyond what the local terminal holds.
94
+
95
+ ### 8.7 REVLoans CEI violation in `_adjust`
96
+
97
+ In `REVLoans._adjust`, `totalCollateralOf[revnetId]` is incremented after external calls (`useAllowanceOf`, fee payment). A reentrant `borrowFrom` would see a lower `totalCollateralOf`. This is documented inline (lines 1128-1132) and requires an adversarial pay hook on the revnet's own terminal -- a trust-level configuration that is not realistic in standard deployments.
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.36",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -798,7 +798,7 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
798
798
  if (shouldDeployNewRevnet) {
799
799
  // If we're deploying a new revnet, launch a Juicebox project for it.
800
800
  // Sanity check that we deployed the `revnetId` that we expected to deploy.
801
- // slither-disable-next-line reentrancy-benign,reentrancy-events
801
+ // slither-disable-next-line incorrect-equality,reentrancy-benign,reentrancy-events
802
802
  assert(
803
803
  CONTROLLER.launchProjectFor({
804
804
  owner: address(this),
@@ -956,10 +956,26 @@ 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});
962
972
 
973
+ // Track the previous stage's effective start time for ordering validation.
974
+ // When stage 0 uses `startsAtOrAfter == 0`, the effective value is `block.timestamp`.
975
+ // Subsequent stages must be validated against this normalized value, not the raw calldata,
976
+ // so that cross-chain deployments can reproduce the same encoded configuration hash.
977
+ uint256 previousStageStart;
978
+
963
979
  // Iterate through each stage to set up its ruleset.
964
980
  for (uint256 i; i < configuration.stageConfigurations.length;) {
965
981
  // Set the stage being iterated on.
@@ -971,12 +987,19 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
971
987
  revert REVDeployer_MustHaveSplits();
972
988
  }
973
989
 
990
+ // Compute the effective start time for this stage.
991
+ uint256 effectiveStart = (i == 0 && stageConfiguration.startsAtOrAfter == 0)
992
+ ? block.timestamp
993
+ : stageConfiguration.startsAtOrAfter;
994
+
974
995
  // If the stage's start time is not after the previous stage's start time, revert.
975
- if (i > 0 && stageConfiguration.startsAtOrAfter <= configuration.stageConfigurations[i - 1].startsAtOrAfter)
976
- {
996
+ if (i > 0 && effectiveStart <= previousStageStart) {
977
997
  revert REVDeployer_StageTimesMustIncrease();
978
998
  }
979
999
 
1000
+ // Store for the next iteration's ordering check.
1001
+ previousStageStart = effectiveStart;
1002
+
980
1003
  // Make sure the revnet doesn't prevent cashouts all together.
981
1004
  if (stageConfiguration.cashOutTaxRate >= JBConstants.MAX_CASH_OUT_TAX_RATE) {
982
1005
  revert REVDeployer_CashOutsCantBeTurnedOffCompletely(
@@ -994,13 +1017,9 @@ contract REVDeployer is ERC2771Context, IREVDeployer, IERC721Receiver {
994
1017
  // Add the stage's properties to the byte-encoded configuration.
995
1018
  encodedConfiguration = abi.encode(
996
1019
  encodedConfiguration,
997
- // If no start time is provided for the first stage, use the current block's timestamp.
998
- // In the future, revnets deployed on other networks can match this revnet's encoded stage by specifying
999
- // the
1000
- // same start time.
1001
- (i == 0 && stageConfiguration.startsAtOrAfter == 0)
1002
- ? block.timestamp
1003
- : stageConfiguration.startsAtOrAfter,
1020
+ // Use the effective start time (normalized from 0 to block.timestamp for the first stage).
1021
+ // Cross-chain deployments reproduce the hash by specifying the origin chain's timestamp.
1022
+ effectiveStart,
1004
1023
  stageConfiguration.splitPercent,
1005
1024
  stageConfiguration.initialIssuance,
1006
1025
  stageConfiguration.issuanceCutFrequency,
package/src/REVLoans.sol CHANGED
@@ -543,17 +543,18 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
543
543
  // Get a reference to the token being iterated on.
544
544
  REVLoanSource storage source = sources[i];
545
545
 
546
- // Get a reference to the accounting context for the source.
547
- // slither-disable-next-line calls-loop
548
- JBAccountingContext memory accountingContext =
549
- source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
550
-
551
546
  // Get a reference to the amount of tokens loaned out.
552
547
  uint256 tokensLoaned = totalBorrowedFrom[revnetId][source.terminal][source.token];
553
548
 
554
- // Skip if no tokens are loaned from this source.
549
+ // Skip if no tokens are loaned from this source. Checked before the external call below to avoid
550
+ // reverting on stale sources whose terminals may no longer support this token.
555
551
  if (tokensLoaned == 0) continue;
556
552
 
553
+ // Get a reference to the accounting context for the source.
554
+ // slither-disable-next-line calls-loop
555
+ JBAccountingContext memory accountingContext =
556
+ source.terminal.accountingContextForTokenOf({projectId: revnetId, token: source.token});
557
+
557
558
  // Normalize the token amount from the source's decimals to the target decimals.
558
559
  uint256 normalizedTokens;
559
560
  if (accountingContext.decimals > decimals) {
@@ -1225,6 +1226,14 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1225
1226
  return 0;
1226
1227
  }
1227
1228
 
1229
+ /// @notice Clears any token allowance granted by `_beforeTransferTo`.
1230
+ /// @param to The address that was granted the allowance.
1231
+ /// @param token The token whose allowance should be cleared.
1232
+ function _afterTransferTo(address to, address token) internal {
1233
+ if (token == JBConstants.NATIVE_TOKEN) return;
1234
+ IERC20(token).forceApprove({spender: to, value: 0});
1235
+ }
1236
+
1228
1237
  /// @notice Reallocates collateral from a loan by making a new loan based on the original, with reduced collateral.
1229
1238
  /// @param loanId The ID of the loan to reallocate collateral from.
1230
1239
  /// @param revnetId The ID of the revnet the loan is from.
@@ -1334,6 +1343,8 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1334
1343
  memo: "",
1335
1344
  metadata: bytes(abi.encodePacked(REV_ID))
1336
1345
  });
1346
+
1347
+ _afterTransferTo({to: address(loan.source.terminal), token: loan.source.token});
1337
1348
  }
1338
1349
 
1339
1350
  /// @notice Pays down a loan.
@@ -1532,6 +1543,7 @@ contract REVLoans is ERC721, ERC2771Context, JBPermissioned, Ownable, IREVLoans
1532
1543
  metadata: bytes(abi.encodePacked(metadataProjectId))
1533
1544
  }) {
1534
1545
  success = true;
1546
+ _afterTransferTo({to: address(terminal), token: token});
1535
1547
  } catch (bytes memory) {
1536
1548
  if (token != JBConstants.NATIVE_TOKEN) {
1537
1549
  IERC20(token).safeDecreaseAllowance({spender: address(terminal), requestedDecrease: amount});
package/src/REVOwner.sol CHANGED
@@ -16,6 +16,7 @@ import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashO
16
16
  import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
17
17
  import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
18
18
  import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
19
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
19
20
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
20
21
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
21
22
  import {mulDiv} from "@prb/math/src/Common.sol";
@@ -458,7 +459,8 @@ contract REVOwner is IJBRulesetDataHook, IJBCashOutHook {
458
459
  /// @dev See `IERC165.supportsInterface`.
459
460
  /// @return A flag indicating if the provided interface ID is supported.
460
461
  function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
461
- return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IJBCashOutHook).interfaceId;
462
+ return interfaceId == type(IERC165).interfaceId || interfaceId == type(IJBRulesetDataHook).interfaceId
463
+ || interfaceId == type(IJBCashOutHook).interfaceId;
462
464
  }
463
465
 
464
466
  //*********************************************************************//
@@ -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
+ }