@rev-net/core-v6 0.0.33 → 0.0.34

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,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
+ }
@@ -148,32 +148,82 @@ contract NemesisOperatorDelegationTest is TestBaseWorkflow {
148
148
  );
149
149
  }
150
150
 
151
- function test_revealTokensOperatorCanRedirectHiddenTokens() public {
151
+ function test_hiddenTokensOperatorCanAllowHolderToHideOwnTokens() public {
152
152
  uint256 userTokens = _payUserIntoRevnet(10e18);
153
153
  uint256 hiddenCount = userTokens / 2;
154
154
 
155
155
  _grantPermission(USER, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
156
- _grantPermission(USER, REVNET_ID, OPERATOR, JBPermissionIds.REVEAL_TOKENS);
156
+ _allowHolderToHide(USER);
157
157
 
158
158
  vm.prank(USER);
159
159
  HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hiddenCount, USER);
160
160
 
161
- vm.prank(OPERATOR);
162
- HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hiddenCount, OPERATOR, USER);
161
+ vm.prank(USER);
162
+ HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hiddenCount, USER);
163
163
 
164
164
  assertEq(HIDDEN_TOKENS.hiddenBalanceOf(USER, REVNET_ID), 0, "holder hidden balance was consumed");
165
165
  assertEq(
166
- jbController().TOKENS().totalBalanceOf(OPERATOR, REVNET_ID),
167
- hiddenCount,
168
- "operator receives the holder's revealed tokens"
166
+ jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
167
+ userTokens,
168
+ "holder gets their own revealed tokens back"
169
169
  );
170
+ }
171
+
172
+ function test_hiddenTokensPermissionedOperatorCanHideOwnTokens() public {
173
+ vm.deal(OPERATOR, 10e18);
174
+ vm.prank(OPERATOR);
175
+ uint256 operatorTokens = jbMultiTerminal().pay{value: 10e18}({
176
+ projectId: REVNET_ID,
177
+ token: JBConstants.NATIVE_TOKEN,
178
+ amount: 10e18,
179
+ beneficiary: OPERATOR,
180
+ minReturnedTokens: 0,
181
+ memo: "",
182
+ metadata: ""
183
+ });
184
+ uint256 hiddenCount = operatorTokens / 2;
185
+
186
+ _grantPermission(OPERATOR, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
187
+ _grantOperatorHidePermission(OPERATOR);
188
+
189
+ vm.prank(OPERATOR);
190
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, hiddenCount, OPERATOR);
191
+
192
+ assertEq(HIDDEN_TOKENS.hiddenBalanceOf(OPERATOR, REVNET_ID), hiddenCount, "operator hidden balance was updated");
170
193
  assertEq(
171
- jbController().TOKENS().totalBalanceOf(USER, REVNET_ID),
172
- userTokens - hiddenCount,
173
- "holder does not get the revealed tokens back"
194
+ jbController().TOKENS().totalBalanceOf(OPERATOR, REVNET_ID),
195
+ operatorTokens - hiddenCount,
196
+ "operator's visible balance was reduced"
174
197
  );
175
198
  }
176
199
 
200
+ function test_hiddenTokensDelegateCannotHideAnotherHoldersTokens() public {
201
+ uint256 userTokens = _payUserIntoRevnet(10e18);
202
+
203
+ _grantPermission(USER, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
204
+ _allowHolderToHide(USER);
205
+
206
+ vm.prank(OPERATOR);
207
+ vm.expectRevert(
208
+ abi.encodeWithSelector(REVHiddenTokens.REVHiddenTokens_Unauthorized.selector, REVNET_ID, OPERATOR)
209
+ );
210
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, userTokens / 2, USER);
211
+ }
212
+
213
+ function test_hiddenTokensOperatorCanDisallowHolder() public {
214
+ uint256 userTokens = _payUserIntoRevnet(10e18);
215
+
216
+ _grantPermission(USER, REVNET_ID, address(HIDDEN_TOKENS), JBPermissionIds.BURN_TOKENS);
217
+ _allowHolderToHide(USER);
218
+
219
+ vm.prank(address(REV_DEPLOYER));
220
+ HIDDEN_TOKENS.setTokenHidingAllowedFor(REVNET_ID, USER, false);
221
+
222
+ vm.prank(USER);
223
+ vm.expectRevert(abi.encodeWithSelector(REVHiddenTokens.REVHiddenTokens_Unauthorized.selector, REVNET_ID, USER));
224
+ HIDDEN_TOKENS.hideTokensOf(REVNET_ID, userTokens / 2, USER);
225
+ }
226
+
177
227
  function _grantPermission(address account, uint256 revnetId, address operator, uint8 permissionId) internal {
178
228
  uint8[] memory permissionIds = new uint8[](1);
179
229
  permissionIds[0] = permissionId;
@@ -200,6 +250,23 @@ contract NemesisOperatorDelegationTest is TestBaseWorkflow {
200
250
  assertGt(tokenCount, 0, "payment should mint revnet tokens");
201
251
  }
202
252
 
253
+ function _allowHolderToHide(address holder) internal {
254
+ vm.prank(address(REV_DEPLOYER));
255
+ HIDDEN_TOKENS.setTokenHidingAllowedFor(REVNET_ID, holder, true);
256
+ }
257
+
258
+ function _grantOperatorHidePermission(address operator) internal {
259
+ uint8[] memory permissionIds = new uint8[](1);
260
+ permissionIds[0] = JBPermissionIds.HIDE_TOKENS;
261
+
262
+ vm.prank(address(REV_DEPLOYER));
263
+ jbPermissions()
264
+ .setPermissionsFor(
265
+ address(REV_DEPLOYER),
266
+ JBPermissionsData({operator: operator, projectId: uint56(REVNET_ID), permissionIds: permissionIds})
267
+ );
268
+ }
269
+
203
270
  function _deployFeeProject() internal {
204
271
  JBAccountingContext[] memory acc = new JBAccountingContext[](1);
205
272
  acc[0] = JBAccountingContext({