@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.
- package/ADMINISTRATION.md +38 -51
- package/ARCHITECTURE.md +24 -11
- package/AUDIT_INSTRUCTIONS.md +43 -80
- package/CHANGELOG.md +4 -3
- package/README.md +20 -32
- package/RISKS.md +49 -214
- package/SKILLS.md +10 -17
- package/USER_JOURNEYS.md +47 -66
- package/package.json +1 -1
- package/references/runtime.md +3 -2
- package/src/REVDeployer.sol +13 -2
- package/src/REVHiddenTokens.sol +48 -26
- package/src/REVLoans.sol +2 -2
- package/src/REVOwner.sol +10 -2
- package/src/interfaces/IREVHiddenTokens.sol +21 -6
- package/test/TestAuditFixVerification.t.sol +675 -0
- package/test/TestHiddenTokens.t.sol +51 -8
- package/test/TestTerminalEncodingInHash.t.sol +326 -0
- package/test/audit/CodexCrossChainBuybackRouteMismatch.t.sol +184 -0
- package/test/audit/NemesisOperatorDelegation.t.sol +77 -10
|
@@ -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
|
|
214
|
+
// Reveal tokens back to USER.
|
|
211
215
|
vm.prank(USER);
|
|
212
|
-
HIDDEN_TOKENS.revealTokensOf(REVNET_ID, hideCount,
|
|
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(
|
|
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
|
|
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,
|
|
335
|
-
HIDDEN_TOKENS.revealTokensOf(REVNET_ID, userTokens,
|
|
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
|
+
}
|