@rev-net/core-v6 0.0.33 → 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.
@@ -171,6 +171,8 @@ contract TestHiddenTokens is TestBaseWorkflow {
171
171
 
172
172
  uint256 totalSupplyBefore = jbController().TOKENS().totalSupplyOf(REVNET_ID);
173
173
 
174
+ _allowHolderToHide(USER, REVNET_ID);
175
+
174
176
  // Hide half the tokens.
175
177
  uint256 hideCount = userTokens / 2;
176
178
  vm.prank(USER);
@@ -202,23 +204,23 @@ contract TestHiddenTokens is TestBaseWorkflow {
202
204
  uint256 userTokensBefore = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
203
205
  uint256 totalSupplyBefore = jbController().TOKENS().totalSupplyOf(REVNET_ID);
204
206
 
207
+ _allowHolderToHide(USER, REVNET_ID);
208
+
205
209
  // Hide tokens.
206
210
  uint256 hideCount = userTokensBefore / 2;
207
211
  vm.prank(USER);
208
212
  HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
209
213
 
210
- // Reveal tokens to beneficiary.
214
+ // Reveal tokens back to USER.
211
215
  vm.prank(USER);
212
- HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount, BENEFICIARY, USER);
216
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount, USER);
213
217
 
214
218
  uint256 totalSupplyAfter = jbController().TOKENS().totalSupplyOf(REVNET_ID);
215
219
  assertEq(totalSupplyAfter, totalSupplyBefore, "Total supply should be restored");
216
220
  assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), 0, "Hidden balance should be zero");
217
221
  assertEq(HIDDEN_TOKENS.totalHiddenOf(REVNET_ID), 0, "Total hidden should be zero");
218
222
  assertEq(
219
- jbController().TOKENS().totalBalanceOf(BENEFICIARY, REVNET_ID),
220
- hideCount,
221
- "Beneficiary should receive tokens"
223
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID), userTokensBefore, "User should receive tokens"
222
224
  );
223
225
  }
224
226
 
@@ -242,6 +244,8 @@ contract TestHiddenTokens is TestBaseWorkflow {
242
244
  uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
243
245
  uint256 hideCount = userTokens / 4;
244
246
 
247
+ _allowHolderToHide(USER, REVNET_ID);
248
+
245
249
  // Hide some tokens.
246
250
  vm.prank(USER);
247
251
  HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
@@ -253,7 +257,7 @@ contract TestHiddenTokens is TestBaseWorkflow {
253
257
  REVHiddenTokens.REVHiddenTokens_InsufficientHiddenBalance.selector, hideCount, hideCount + 1
254
258
  )
255
259
  );
256
- HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount + 1, USER, USER);
260
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount + 1, USER);
257
261
  }
258
262
 
259
263
  // ──────────────────── Test: Hidden tokens inflate cash out rate
@@ -275,6 +279,8 @@ contract TestHiddenTokens is TestBaseWorkflow {
275
279
 
276
280
  uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
277
281
 
282
+ _allowHolderToHide(USER, REVNET_ID);
283
+
278
284
  // Hide half the user's tokens.
279
285
  uint256 hideCount = userTokens / 2;
280
286
  vm.prank(USER);
@@ -305,6 +311,8 @@ contract TestHiddenTokens is TestBaseWorkflow {
305
311
 
306
312
  uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
307
313
 
314
+ _allowHolderToHide(USER, REVNET_ID);
315
+
308
316
  vm.prank(USER);
309
317
  vm.expectEmit(true, false, false, true);
310
318
  emit IREVHiddenTokens.HideTokens(REVNET_ID, userTokens, USER, USER);
@@ -326,13 +334,43 @@ contract TestHiddenTokens is TestBaseWorkflow {
326
334
 
327
335
  uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
328
336
 
337
+ _allowHolderToHide(USER, REVNET_ID);
338
+
329
339
  vm.prank(USER);
330
340
  HIDDEN_TOKENS.hideTokensOf(REVNET_ID, userTokens, USER);
331
341
 
332
342
  vm.prank(USER);
333
343
  vm.expectEmit(true, false, false, true);
334
- emit IREVHiddenTokens.RevealTokens(REVNET_ID, userTokens, BENEFICIARY, USER, USER);
335
- HIDDEN_TOKENS.revealTokensOf(REVNET_ID, userTokens, BENEFICIARY, USER);
344
+ emit IREVHiddenTokens.RevealTokens(REVNET_ID, userTokens, USER, USER);
345
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, userTokens, USER);
346
+ }
347
+
348
+ function test_setTokenHidingAllowedFor_allowsHolderToHide() public {
349
+ uint256 payAmount = 10e18;
350
+
351
+ vm.prank(USER);
352
+ jbMultiTerminal().pay{value: payAmount}({
353
+ projectId: REVNET_ID,
354
+ token: JBConstants.NATIVE_TOKEN,
355
+ amount: payAmount,
356
+ beneficiary: USER,
357
+ minReturnedTokens: 0,
358
+ memo: "",
359
+ metadata: ""
360
+ });
361
+
362
+ uint256 userTokens = jbController().TOKENS().totalBalanceOf(USER, REVNET_ID);
363
+ uint256 hideCount = userTokens / 2;
364
+
365
+ _allowHolderToHide(USER, REVNET_ID);
366
+
367
+ vm.prank(USER);
368
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hideCount, USER);
369
+
370
+ vm.prank(USER);
371
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount, USER);
372
+
373
+ assertEq(jbController().TOKENS().totalBalanceOf(USER, REVNET_ID), userTokens, "User balance should be restored");
336
374
  }
337
375
 
338
376
  // ──────────────────── Internal helpers
@@ -351,6 +389,11 @@ contract TestHiddenTokens is TestBaseWorkflow {
351
389
  jbPermissions().setPermissionsFor(account, permissionsData);
352
390
  }
353
391
 
392
+ function _allowHolderToHide(address holder, uint256 revnetId) internal {
393
+ vm.prank(address(REV_DEPLOYER));
394
+ HIDDEN_TOKENS.setTokenHidingAllowedFor(revnetId, holder, true);
395
+ }
396
+
354
397
  function _deployFeeProject() internal {
355
398
  JBAccountingContext[] memory acc = new JBAccountingContext[](1);
356
399
  acc[0] = JBAccountingContext({
@@ -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
+ }
@@ -0,0 +1,184 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.28;
3
+
4
+ import "forge-std/Test.sol";
5
+ import "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
7
+ import {IJBCashOutHook} from "@bananapus/core-v6/src/interfaces/IJBCashOutHook.sol";
8
+ import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
9
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
10
+ import {JBCashOuts} from "@bananapus/core-v6/src/libraries/JBCashOuts.sol";
11
+ import {JBBeforeCashOutRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforeCashOutRecordedContext.sol";
12
+ import {JBCashOutHookSpecification} from "@bananapus/core-v6/src/structs/JBCashOutHookSpecification.sol";
13
+ import {JBBeforePayRecordedContext} from "@bananapus/core-v6/src/structs/JBBeforePayRecordedContext.sol";
14
+ import {JBPayHookSpecification} from "@bananapus/core-v6/src/structs/JBPayHookSpecification.sol";
15
+ import {JBTokenAmount} from "@bananapus/core-v6/src/structs/JBTokenAmount.sol";
16
+ import {JBRuleset} from "@bananapus/core-v6/src/structs/JBRuleset.sol";
17
+ import {IJBSuckerRegistry} from "@bananapus/suckers-v6/src/interfaces/IJBSuckerRegistry.sol";
18
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
19
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
20
+
21
+ import {REVOwner} from "../../src/REVOwner.sol";
22
+
23
+ contract ConfigurableSuckerRegistry {
24
+ uint256 public remoteSupply;
25
+ uint256 public remoteSurplus;
26
+
27
+ function setRemoteTotals(uint256 supply, uint256 surplus) external {
28
+ remoteSupply = supply;
29
+ remoteSurplus = surplus;
30
+ }
31
+
32
+ function isSuckerOf(uint256, address) external pure returns (bool) {
33
+ return false;
34
+ }
35
+
36
+ function remoteTotalSupplyOf(uint256) external view returns (uint256) {
37
+ return remoteSupply;
38
+ }
39
+
40
+ function remoteSurplusOf(uint256, uint256, uint256) external view returns (uint256) {
41
+ return remoteSurplus;
42
+ }
43
+ }
44
+
45
+ contract ThresholdBuybackRegistry is IJBRulesetDataHook {
46
+ uint256 public immutable minimumSwapAmountOut;
47
+
48
+ constructor(uint256 minAmountOut) {
49
+ minimumSwapAmountOut = minAmountOut;
50
+ }
51
+
52
+ function beforeCashOutRecordedWith(JBBeforeCashOutRecordedContext calldata context)
53
+ external
54
+ view
55
+ returns (
56
+ uint256 cashOutTaxRate,
57
+ uint256 cashOutCount,
58
+ uint256 totalSupply,
59
+ uint256 effectiveSurplusValue,
60
+ JBCashOutHookSpecification[] memory hookSpecifications
61
+ )
62
+ {
63
+ uint256 directCashOutAmount = JBCashOuts.cashOutFrom({
64
+ surplus: context.surplus.value,
65
+ cashOutCount: context.cashOutCount,
66
+ totalSupply: context.totalSupply,
67
+ cashOutTaxRate: context.cashOutTaxRate
68
+ });
69
+
70
+ hookSpecifications = new JBCashOutHookSpecification[](1);
71
+ hookSpecifications[0] = JBCashOutHookSpecification({
72
+ hook: IJBCashOutHook(address(this)),
73
+ noop: directCashOutAmount >= minimumSwapAmountOut,
74
+ amount: 0,
75
+ metadata: ""
76
+ });
77
+
78
+ cashOutTaxRate =
79
+ directCashOutAmount >= minimumSwapAmountOut ? context.cashOutTaxRate : JBConstants.MAX_CASH_OUT_TAX_RATE;
80
+ cashOutCount = context.cashOutCount;
81
+ totalSupply = context.totalSupply;
82
+ effectiveSurplusValue = 0;
83
+ }
84
+
85
+ function beforePayRecordedWith(JBBeforePayRecordedContext calldata context)
86
+ external
87
+ pure
88
+ returns (uint256 weight, JBPayHookSpecification[] memory hookSpecifications)
89
+ {
90
+ weight = context.weight;
91
+ hookSpecifications = new JBPayHookSpecification[](0);
92
+ }
93
+
94
+ function hasMintPermissionFor(uint256, JBRuleset calldata, address) external pure returns (bool) {
95
+ return false;
96
+ }
97
+
98
+ function setPoolFor(uint256, PoolKey calldata, uint256, address) external pure {}
99
+
100
+ function setPoolFor(uint256, uint24, int24, uint256, address) external pure {}
101
+
102
+ function initializePoolFor(uint256, uint24, int24, uint256, address, uint160) external pure {}
103
+
104
+ function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
105
+ return interfaceId == type(IJBRulesetDataHook).interfaceId || interfaceId == type(IERC165).interfaceId;
106
+ }
107
+ }
108
+
109
+ contract CodexCrossChainBuybackRouteMismatchTest is TestBaseWorkflow {
110
+ REVOwner internal ownerHook;
111
+ ConfigurableSuckerRegistry internal suckerRegistry;
112
+ ThresholdBuybackRegistry internal buybackRegistry;
113
+
114
+ function setUp() public override {
115
+ super.setUp();
116
+
117
+ suckerRegistry = new ConfigurableSuckerRegistry();
118
+ buybackRegistry = new ThresholdBuybackRegistry(50 ether);
119
+
120
+ ownerHook = new REVOwner(
121
+ IJBBuybackHookRegistry(address(buybackRegistry)),
122
+ jbDirectory(),
123
+ 999_999,
124
+ IJBSuckerRegistry(address(suckerRegistry)),
125
+ address(0),
126
+ address(0)
127
+ );
128
+ }
129
+
130
+ function test_buybackRouteUsesOmnichainContextForRouting() public {
131
+ suckerRegistry.setRemoteTotals(0, 900 ether);
132
+
133
+ JBBeforeCashOutRecordedContext memory context = JBBeforeCashOutRecordedContext({
134
+ terminal: address(jbMultiTerminal()),
135
+ holder: address(0xBEEF),
136
+ projectId: 1,
137
+ rulesetId: 0,
138
+ cashOutCount: 100,
139
+ totalSupply: 1000,
140
+ surplus: JBTokenAmount({
141
+ token: JBConstants.NATIVE_TOKEN,
142
+ value: 100 ether,
143
+ decimals: 18,
144
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
145
+ }),
146
+ useTotalSurplus: true,
147
+ cashOutTaxRate: 0,
148
+ beneficiaryIsFeeless: false,
149
+ metadata: ""
150
+ });
151
+
152
+ uint256 localDirectCashOut = JBCashOuts.cashOutFrom({
153
+ surplus: context.surplus.value,
154
+ cashOutCount: context.cashOutCount,
155
+ totalSupply: context.totalSupply,
156
+ cashOutTaxRate: context.cashOutTaxRate
157
+ });
158
+ uint256 omnichainDirectCashOut = JBCashOuts.cashOutFrom({
159
+ surplus: context.surplus.value + 900 ether,
160
+ cashOutCount: context.cashOutCount,
161
+ totalSupply: context.totalSupply,
162
+ cashOutTaxRate: context.cashOutTaxRate
163
+ });
164
+
165
+ assertLt(localDirectCashOut, 50 ether, "local route should prefer swap in the mock");
166
+ assertGe(omnichainDirectCashOut, 50 ether, "omnichain route should prefer direct reclaim");
167
+
168
+ (uint256 returnedTaxRate,, uint256 returnedSupply, uint256 returnedSurplus,) =
169
+ ownerHook.beforeCashOutRecordedWith(context);
170
+
171
+ // After the fix, REVOwner forwards the cross-chain-adjusted context to the buyback hook.
172
+ // The buyback hook now sees the full omnichain surplus (1000 ether) and correctly routes
173
+ // to direct reclaim (passthrough) instead of swap.
174
+ assertEq(returnedSupply, context.totalSupply, "owner returns cross-chain total supply");
175
+ assertEq(returnedSurplus, context.surplus.value + 900 ether, "owner returns cross-chain effective surplus");
176
+ // With omnichain context, the direct reclaim (100 ether) exceeds the threshold (50 ether),
177
+ // so the buyback hook chooses passthrough (returns the original cashOutTaxRate of 0).
178
+ assertEq(
179
+ returnedTaxRate,
180
+ context.cashOutTaxRate,
181
+ "routing correctly uses omnichain context - direct reclaim beats threshold"
182
+ );
183
+ }
184
+ }