@rev-net/core-v6 0.0.8 → 0.0.9

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.
Files changed (58) hide show
  1. package/ADMINISTRATION.md +186 -0
  2. package/ARCHITECTURE.md +87 -0
  3. package/README.md +4 -2
  4. package/RISKS.md +49 -0
  5. package/SKILLS.md +22 -2
  6. package/STYLE_GUIDE.md +482 -0
  7. package/foundry.toml +6 -6
  8. package/package.json +8 -8
  9. package/script/Deploy.s.sol +3 -2
  10. package/src/REVDeployer.sol +75 -52
  11. package/src/REVLoans.sol +13 -4
  12. package/src/interfaces/IREVDeployer.sol +2 -1
  13. package/src/structs/REV721TiersHookFlags.sol +14 -0
  14. package/src/structs/REVBaseline721HookConfig.sol +27 -0
  15. package/src/structs/REVDeploy721TiersHookConfig.sol +2 -2
  16. package/test/REV.integrations.t.sol +4 -3
  17. package/test/REVAutoIssuanceFuzz.t.sol +12 -8
  18. package/test/REVDeployerAuditRegressions.t.sol +4 -3
  19. package/test/REVInvincibility.t.sol +8 -6
  20. package/test/REVInvincibilityHandler.sol +1 -0
  21. package/test/REVLifecycle.t.sol +4 -3
  22. package/test/REVLoans.invariants.t.sol +5 -3
  23. package/test/REVLoansAttacks.t.sol +4 -3
  24. package/test/REVLoansAuditRegressions.t.sol +13 -24
  25. package/test/REVLoansFeeRecovery.t.sol +4 -3
  26. package/test/REVLoansSourced.t.sol +4 -3
  27. package/test/REVLoansUnSourced.t.sol +4 -3
  28. package/test/REVLoans_AuditFindings.t.sol +644 -0
  29. package/test/TestEmptyBuybackSpecs.t.sol +4 -3
  30. package/test/TestPR09_ConversionDocumentation.t.sol +4 -3
  31. package/test/TestPR10_LiquidationBehavior.t.sol +4 -3
  32. package/test/TestPR11_LowFindings.t.sol +4 -3
  33. package/test/TestPR12_FlashLoanSurplus.t.sol +4 -3
  34. package/test/TestPR13_CrossSourceReallocation.t.sol +4 -3
  35. package/test/TestPR15_CashOutCallerValidation.t.sol +4 -3
  36. package/test/TestPR16_ZeroRepayment.t.sol +4 -3
  37. package/test/TestPR21_Uint112Overflow.t.sol +4 -3
  38. package/test/TestPR22_HookArrayOOB.t.sol +4 -3
  39. package/test/TestPR26_BurnHeldTokens.t.sol +4 -3
  40. package/test/TestPR27_CEIPattern.t.sol +4 -3
  41. package/test/TestPR29_SwapTerminalPermission.t.sol +4 -3
  42. package/test/TestPR32_MixedFixes.t.sol +4 -3
  43. package/test/TestSplitWeightAdjustment.t.sol +445 -0
  44. package/test/TestSplitWeightE2E.t.sol +528 -0
  45. package/test/TestSplitWeightFork.t.sol +821 -0
  46. package/test/TestStageTransitionBorrowable.t.sol +4 -3
  47. package/test/fork/ForkTestBase.sol +617 -0
  48. package/test/fork/TestCashOutFork.t.sol +245 -0
  49. package/test/fork/TestLoanBorrowFork.t.sol +163 -0
  50. package/test/fork/TestLoanLiquidationFork.t.sol +129 -0
  51. package/test/fork/TestLoanReallocateFork.t.sol +103 -0
  52. package/test/fork/TestLoanRepayFork.t.sol +184 -0
  53. package/test/fork/TestSplitWeightFork.t.sol +186 -0
  54. package/test/mock/MockBuybackDataHook.sol +7 -4
  55. package/test/mock/MockBuybackDataHookMintPath.sol +7 -3
  56. package/test/regression/TestI20_CumulativeLoanCounter.t.sol +6 -5
  57. package/test/regression/TestL27_LiquidateGapHandling.t.sol +7 -6
  58. package/SECURITY.md +0 -68
@@ -0,0 +1,821 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity 0.8.26;
3
+
4
+ import "forge-std/Test.sol";
5
+ import /* {*} from */ "@bananapus/core-v6/test/helpers/TestBaseWorkflow.sol";
6
+ import /* {*} from */ "./../src/REVDeployer.sol";
7
+ import "@croptop/core-v6/src/CTPublisher.sol";
8
+ import "@bananapus/core-v6/script/helpers/CoreDeploymentLib.sol";
9
+ import "@bananapus/721-hook-v6/script/helpers/Hook721DeploymentLib.sol";
10
+ import "@bananapus/suckers-v6/script/helpers/SuckerDeploymentLib.sol";
11
+ import "@croptop/core-v6/script/helpers/CroptopDeploymentLib.sol";
12
+ import "@bananapus/router-terminal-v6/script/helpers/RouterTerminalDeploymentLib.sol";
13
+
14
+ import {JBConstants} from "@bananapus/core-v6/src/libraries/JBConstants.sol";
15
+ import {JBMetadataResolver} from "@bananapus/core-v6/src/libraries/JBMetadataResolver.sol";
16
+ import {JBAccountingContext} from "@bananapus/core-v6/src/structs/JBAccountingContext.sol";
17
+ import {REVLoans} from "../src/REVLoans.sol";
18
+ import {REVStageConfig, REVAutoIssuance} from "../src/structs/REVStageConfig.sol";
19
+ import {REVLoanSource} from "../src/structs/REVLoanSource.sol";
20
+ import {REVDescription} from "../src/structs/REVDescription.sol";
21
+ import {IREVLoans} from "./../src/interfaces/IREVLoans.sol";
22
+ import {JBSuckerDeployerConfig} from "@bananapus/suckers-v6/src/structs/JBSuckerDeployerConfig.sol";
23
+ import {JBSuckerRegistry} from "@bananapus/suckers-v6/src/JBSuckerRegistry.sol";
24
+ import {JB721TiersHookDeployer} from "@bananapus/721-hook-v6/src/JB721TiersHookDeployer.sol";
25
+ import {JB721TiersHook} from "@bananapus/721-hook-v6/src/JB721TiersHook.sol";
26
+ import {JB721TiersHookStore} from "@bananapus/721-hook-v6/src/JB721TiersHookStore.sol";
27
+ import {JBAddressRegistry} from "@bananapus/address-registry-v6/src/JBAddressRegistry.sol";
28
+ import {IJBAddressRegistry} from "@bananapus/address-registry-v6/src/interfaces/IJBAddressRegistry.sol";
29
+ import {IJBRulesetDataHook} from "@bananapus/core-v6/src/interfaces/IJBRulesetDataHook.sol";
30
+ import {IJBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHookRegistry.sol";
31
+ import {IJBPayHook} from "@bananapus/core-v6/src/interfaces/IJBPayHook.sol";
32
+ import {REVBaseline721HookConfig} from "../src/structs/REVBaseline721HookConfig.sol";
33
+ import {REV721TiersHookFlags} from "../src/structs/REV721TiersHookFlags.sol";
34
+ import {JB721TierConfig} from "@bananapus/721-hook-v6/src/structs/JB721TierConfig.sol";
35
+ import {JB721InitTiersConfig} from "@bananapus/721-hook-v6/src/structs/JB721InitTiersConfig.sol";
36
+ import {IJB721TokenUriResolver} from "@bananapus/721-hook-v6/src/interfaces/IJB721TokenUriResolver.sol";
37
+ import {REVDeploy721TiersHookConfig} from "../src/structs/REVDeploy721TiersHookConfig.sol";
38
+ import {REVCroptopAllowedPost} from "../src/structs/REVCroptopAllowedPost.sol";
39
+
40
+ // Buyback hook
41
+ import {JBBuybackHook} from "@bananapus/buyback-hook-v6/src/JBBuybackHook.sol";
42
+ import {JBBuybackHookRegistry} from "@bananapus/buyback-hook-v6/src/JBBuybackHookRegistry.sol";
43
+ import {IJBBuybackHook} from "@bananapus/buyback-hook-v6/src/interfaces/IJBBuybackHook.sol";
44
+ import {IWETH9} from "@bananapus/buyback-hook-v6/src/interfaces/external/IWETH9.sol";
45
+ import {IGeomeanOracle} from "@bananapus/buyback-hook-v6/src/interfaces/IGeomeanOracle.sol";
46
+
47
+ // Uniswap V4
48
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
49
+ import {IUnlockCallback} from "@uniswap/v4-core/src/interfaces/callback/IUnlockCallback.sol";
50
+ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
51
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
52
+ import {PoolId, PoolIdLibrary} from "@uniswap/v4-core/src/types/PoolId.sol";
53
+ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
54
+ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
55
+ import {ModifyLiquidityParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
56
+ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
57
+ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
58
+
59
+ import {IERC20} from "@openzeppelin/contracts/interfaces/IERC20.sol";
60
+
61
+ /// @notice Helper that adds liquidity to a V4 pool via the unlock/callback pattern.
62
+ contract LiquidityHelper is IUnlockCallback {
63
+ IPoolManager public immutable poolManager;
64
+
65
+ struct AddLiqParams {
66
+ PoolKey key;
67
+ int24 tickLower;
68
+ int24 tickUpper;
69
+ int256 liquidityDelta;
70
+ }
71
+
72
+ constructor(IPoolManager _poolManager) {
73
+ poolManager = _poolManager;
74
+ }
75
+
76
+ function addLiquidity(
77
+ PoolKey calldata key,
78
+ int24 tickLower,
79
+ int24 tickUpper,
80
+ int256 liquidityDelta
81
+ )
82
+ external
83
+ payable
84
+ {
85
+ bytes memory data = abi.encode(AddLiqParams(key, tickLower, tickUpper, liquidityDelta));
86
+ poolManager.unlock(data);
87
+ }
88
+
89
+ function unlockCallback(bytes calldata data) external override returns (bytes memory) {
90
+ require(msg.sender == address(poolManager), "only PM");
91
+
92
+ AddLiqParams memory params = abi.decode(data, (AddLiqParams));
93
+
94
+ (BalanceDelta callerDelta,) = poolManager.modifyLiquidity(
95
+ params.key,
96
+ ModifyLiquidityParams({
97
+ tickLower: params.tickLower,
98
+ tickUpper: params.tickUpper,
99
+ liquidityDelta: params.liquidityDelta,
100
+ salt: bytes32(0)
101
+ }),
102
+ ""
103
+ );
104
+
105
+ _settleIfNegative(params.key.currency0, callerDelta.amount0());
106
+ _settleIfNegative(params.key.currency1, callerDelta.amount1());
107
+ _takeIfPositive(params.key.currency0, callerDelta.amount0());
108
+ _takeIfPositive(params.key.currency1, callerDelta.amount1());
109
+
110
+ return abi.encode(callerDelta);
111
+ }
112
+
113
+ function _settleIfNegative(Currency currency, int128 delta) internal {
114
+ if (delta >= 0) return;
115
+ uint256 amount = uint256(uint128(-delta));
116
+
117
+ if (currency.isAddressZero()) {
118
+ poolManager.settle{value: amount}();
119
+ } else {
120
+ poolManager.sync(currency);
121
+ IERC20(Currency.unwrap(currency)).transfer(address(poolManager), amount);
122
+ poolManager.settle();
123
+ }
124
+ }
125
+
126
+ function _takeIfPositive(Currency currency, int128 delta) internal {
127
+ if (delta <= 0) return;
128
+ uint256 amount = uint256(uint128(delta));
129
+ poolManager.take(currency, address(this), amount);
130
+ }
131
+
132
+ receive() external payable {}
133
+ }
134
+
135
+ /// @notice Fork tests verifying that revnet 721 tier splits + real Uniswap V4 buyback hook produce correct token
136
+ /// issuance in both the swap path (AMM buyback) and the mint path (direct minting).
137
+ ///
138
+ /// Requires: RPC_ETHEREUM_MAINNET env var for mainnet fork (real PoolManager).
139
+ ///
140
+ /// Run with: FOUNDRY_PROFILE=fork forge test --match-contract TestSplitWeightFork -vvv --skip "script/*"
141
+ contract TestSplitWeightFork is TestBaseWorkflow {
142
+ using JBMetadataResolver for bytes;
143
+ using PoolIdLibrary for PoolKey;
144
+ using CurrencyLibrary for Currency;
145
+ using StateLibrary for IPoolManager;
146
+
147
+ // ───────────────────────── Mainnet constants
148
+ // ─────────────────────────
149
+
150
+ address constant POOL_MANAGER_ADDR = 0x000000000004444c5dc75cB358380D2e3dE08A90;
151
+ address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
152
+
153
+ /// @notice Full-range tick bounds for tickSpacing = 60.
154
+ int24 constant TICK_LOWER = -887_220;
155
+ int24 constant TICK_UPPER = 887_220;
156
+
157
+ // ───────────────────────── State
158
+ // ─────────────────────────
159
+
160
+ REVDeployer REV_DEPLOYER;
161
+ JBBuybackHook BUYBACK_HOOK;
162
+ JBBuybackHookRegistry BUYBACK_REGISTRY;
163
+ JB721TiersHook EXAMPLE_HOOK;
164
+ IJB721TiersHookDeployer HOOK_DEPLOYER;
165
+ IJB721TiersHookStore HOOK_STORE;
166
+ IJBAddressRegistry ADDRESS_REGISTRY;
167
+ IREVLoans LOANS_CONTRACT;
168
+ IJBSuckerRegistry SUCKER_REGISTRY;
169
+ CTPublisher PUBLISHER;
170
+ IPoolManager poolManager;
171
+ IWETH9 weth;
172
+ LiquidityHelper liqHelper;
173
+
174
+ uint256 FEE_PROJECT_ID;
175
+
176
+ address private constant TRUSTED_FORWARDER = 0xB2b5841DBeF766d4b521221732F9B618fCf34A87;
177
+ address PAYER = makeAddr("payer");
178
+ address SPLIT_BENEFICIARY = makeAddr("splitBeneficiary");
179
+
180
+ // Tier configuration: 1 ETH tier with 30% split.
181
+ uint104 constant TIER_PRICE = 1 ether;
182
+ uint32 constant SPLIT_PERCENT = 300_000_000; // 30% of SPLITS_TOTAL_PERCENT (1_000_000_000)
183
+ uint112 constant INITIAL_ISSUANCE = 1000e18; // 1000 tokens per ETH
184
+
185
+ // ───────────────────────── Setup
186
+ // ─────────────────────────
187
+
188
+ function setUp() public override {
189
+ // Fork mainnet first — we need the real V4 PoolManager.
190
+ string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
191
+ if (bytes(rpcUrl).length == 0) {
192
+ vm.skip(true);
193
+ return;
194
+ }
195
+ vm.createSelectFork(rpcUrl);
196
+
197
+ // Verify V4 PoolManager is deployed.
198
+ require(POOL_MANAGER_ADDR.code.length > 0, "PoolManager not deployed at expected address");
199
+
200
+ // Deploy fresh JB core on the forked mainnet.
201
+ super.setUp();
202
+
203
+ poolManager = IPoolManager(POOL_MANAGER_ADDR);
204
+ weth = IWETH9(WETH_ADDR);
205
+ liqHelper = new LiquidityHelper(poolManager);
206
+
207
+ FEE_PROJECT_ID = jbProjects().createFor(multisig());
208
+
209
+ SUCKER_REGISTRY = new JBSuckerRegistry(jbDirectory(), jbPermissions(), multisig(), address(0));
210
+ HOOK_STORE = new JB721TiersHookStore();
211
+ EXAMPLE_HOOK =
212
+ new JB721TiersHook(jbDirectory(), jbPermissions(), jbRulesets(), HOOK_STORE, jbSplits(), multisig());
213
+ ADDRESS_REGISTRY = new JBAddressRegistry();
214
+ HOOK_DEPLOYER = new JB721TiersHookDeployer(EXAMPLE_HOOK, HOOK_STORE, ADDRESS_REGISTRY, multisig());
215
+ PUBLISHER = new CTPublisher(jbDirectory(), jbPermissions(), FEE_PROJECT_ID, multisig());
216
+
217
+ // Deploy REAL buyback hook with real PoolManager.
218
+ BUYBACK_HOOK = new JBBuybackHook(
219
+ jbDirectory(),
220
+ jbPermissions(),
221
+ jbPrices(),
222
+ jbProjects(),
223
+ jbTokens(),
224
+ weth,
225
+ poolManager,
226
+ address(0) // trustedForwarder
227
+ );
228
+
229
+ // Deploy the registry and set the buyback hook as the default.
230
+ BUYBACK_REGISTRY = new JBBuybackHookRegistry(
231
+ jbPermissions(),
232
+ jbProjects(),
233
+ address(this), // owner
234
+ address(0) // trustedForwarder
235
+ );
236
+ BUYBACK_REGISTRY.setDefaultHook(IJBRulesetDataHook(address(BUYBACK_HOOK)));
237
+
238
+ LOANS_CONTRACT = new REVLoans({
239
+ controller: jbController(),
240
+ projects: jbProjects(),
241
+ revId: FEE_PROJECT_ID,
242
+ owner: address(this),
243
+ permit2: permit2(),
244
+ trustedForwarder: TRUSTED_FORWARDER
245
+ });
246
+
247
+ REV_DEPLOYER = new REVDeployer{salt: "REVDeployer_Fork"}(
248
+ jbController(),
249
+ SUCKER_REGISTRY,
250
+ FEE_PROJECT_ID,
251
+ HOOK_DEPLOYER,
252
+ PUBLISHER,
253
+ IJBBuybackHookRegistry(address(BUYBACK_REGISTRY)),
254
+ address(LOANS_CONTRACT),
255
+ TRUSTED_FORWARDER
256
+ );
257
+
258
+ vm.prank(multisig());
259
+ jbProjects().approve(address(REV_DEPLOYER), FEE_PROJECT_ID);
260
+
261
+ // Fund the payer.
262
+ vm.deal(PAYER, 100 ether);
263
+ }
264
+
265
+ modifier onlyFork() {
266
+ string memory rpcUrl = vm.envOr("RPC_ETHEREUM_MAINNET", string(""));
267
+ if (bytes(rpcUrl).length == 0) return;
268
+ _;
269
+ }
270
+
271
+ // ───────────────────────── Helpers
272
+ // ─────────────────────────
273
+
274
+ function _buildMinimalConfig()
275
+ internal
276
+ view
277
+ returns (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc)
278
+ {
279
+ JBAccountingContext[] memory acc = new JBAccountingContext[](1);
280
+ acc[0] = JBAccountingContext({
281
+ token: JBConstants.NATIVE_TOKEN, decimals: 18, currency: uint32(uint160(JBConstants.NATIVE_TOKEN))
282
+ });
283
+ tc = new JBTerminalConfig[](1);
284
+ tc[0] = JBTerminalConfig({terminal: jbMultiTerminal(), accountingContextsToAccept: acc});
285
+
286
+ REVStageConfig[] memory stages = new REVStageConfig[](1);
287
+ JBSplit[] memory splits = new JBSplit[](1);
288
+ splits[0].beneficiary = payable(multisig());
289
+ splits[0].percent = 10_000;
290
+ stages[0] = REVStageConfig({
291
+ startsAtOrAfter: uint40(block.timestamp),
292
+ autoIssuances: new REVAutoIssuance[](0),
293
+ splitPercent: 0,
294
+ splits: splits,
295
+ initialIssuance: INITIAL_ISSUANCE,
296
+ issuanceCutFrequency: 0,
297
+ issuanceCutPercent: 0,
298
+ cashOutTaxRate: 5000,
299
+ extraMetadata: 0
300
+ });
301
+
302
+ cfg = REVConfig({
303
+ description: REVDescription("Fork Test", "FORK", "ipfs://fork", "FORK_SALT"),
304
+ baseCurrency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
305
+ splitOperator: multisig(),
306
+ stageConfigurations: stages
307
+ });
308
+
309
+ sdc = REVSuckerDeploymentConfig({
310
+ deployerConfigurations: new JBSuckerDeployerConfig[](0), salt: keccak256(abi.encodePacked("FORK_TEST"))
311
+ });
312
+ }
313
+
314
+ function _build721Config() internal view returns (REVDeploy721TiersHookConfig memory) {
315
+ JB721TierConfig[] memory tiers = new JB721TierConfig[](1);
316
+ JBSplit[] memory tierSplits = new JBSplit[](1);
317
+ tierSplits[0] = JBSplit({
318
+ preferAddToBalance: false,
319
+ percent: uint32(JBConstants.SPLITS_TOTAL_PERCENT),
320
+ projectId: 0,
321
+ beneficiary: payable(SPLIT_BENEFICIARY),
322
+ lockedUntil: 0,
323
+ hook: IJBSplitHook(address(0))
324
+ });
325
+
326
+ tiers[0] = JB721TierConfig({
327
+ price: TIER_PRICE,
328
+ initialSupply: 100,
329
+ votingUnits: 0,
330
+ reserveFrequency: 0,
331
+ reserveBeneficiary: address(0),
332
+ encodedIPFSUri: bytes32("tier1"),
333
+ category: 1,
334
+ discountPercent: 0,
335
+ allowOwnerMint: false,
336
+ useReserveBeneficiaryAsDefault: false,
337
+ transfersPausable: false,
338
+ useVotingUnits: false,
339
+ cannotBeRemoved: false,
340
+ cannotIncreaseDiscountPercent: false,
341
+ splitPercent: SPLIT_PERCENT,
342
+ splits: tierSplits
343
+ });
344
+
345
+ return REVDeploy721TiersHookConfig({
346
+ baseline721HookConfiguration: REVBaseline721HookConfig({
347
+ name: "Fork NFT",
348
+ symbol: "FNFT",
349
+ baseUri: "ipfs://",
350
+ tokenUriResolver: IJB721TokenUriResolver(address(0)),
351
+ contractUri: "ipfs://contract",
352
+ tiersConfig: JB721InitTiersConfig({
353
+ tiers: tiers,
354
+ currency: uint32(uint160(JBConstants.NATIVE_TOKEN)),
355
+ decimals: 18,
356
+ prices: IJBPrices(address(0))
357
+ }),
358
+ reserveBeneficiary: address(0),
359
+ flags: REV721TiersHookFlags({
360
+ noNewTiersWithReserves: false,
361
+ noNewTiersWithVotes: false,
362
+ noNewTiersWithOwnerMinting: false,
363
+ preventOverspending: false
364
+ })
365
+ }),
366
+ salt: bytes32("FORK_721"),
367
+ splitOperatorCanAdjustTiers: false,
368
+ splitOperatorCanUpdateMetadata: false,
369
+ splitOperatorCanMint: false,
370
+ splitOperatorCanIncreaseDiscountPercent: false
371
+ });
372
+ }
373
+
374
+ /// @notice Deploy the fee project, then deploy a revnet with 721 tiers.
375
+ function _deployRevnetWith721() internal returns (uint256 revnetId, IJB721TiersHook hook) {
376
+ // Deploy fee project first.
377
+ (REVConfig memory feeCfg, JBTerminalConfig[] memory feeTc, REVSuckerDeploymentConfig memory feeSdc) =
378
+ _buildMinimalConfig();
379
+ feeCfg.description = REVDescription("Fee", "FEE", "ipfs://fee", "FEE_SALT");
380
+
381
+ vm.prank(multisig());
382
+ REV_DEPLOYER.deployFor({
383
+ revnetId: FEE_PROJECT_ID,
384
+ configuration: feeCfg,
385
+ terminalConfigurations: feeTc,
386
+ suckerDeploymentConfiguration: feeSdc
387
+ });
388
+
389
+ // Deploy the revnet with 721 hook.
390
+ (REVConfig memory cfg, JBTerminalConfig[] memory tc, REVSuckerDeploymentConfig memory sdc) =
391
+ _buildMinimalConfig();
392
+ REVDeploy721TiersHookConfig memory hookConfig = _build721Config();
393
+
394
+ (revnetId, hook) = REV_DEPLOYER.deployWith721sFor({
395
+ revnetId: 0,
396
+ configuration: cfg,
397
+ terminalConfigurations: tc,
398
+ suckerDeploymentConfiguration: sdc,
399
+ tiered721HookConfiguration: hookConfig,
400
+ allowedPosts: new REVCroptopAllowedPost[](0)
401
+ });
402
+ }
403
+
404
+ /// @notice Set up a V4 pool for the revnet's project token / WETH pair and register it with the buyback hook.
405
+ function _setupPool(uint256 revnetId, uint256 liquidityTokenAmount) internal returns (PoolKey memory key) {
406
+ // Get the project token.
407
+ address projectToken = address(jbTokens().tokenOf(revnetId));
408
+ require(projectToken != address(0), "project token not deployed");
409
+
410
+ // Build sorted pool key.
411
+ address token0;
412
+ address token1;
413
+ if (projectToken < WETH_ADDR) {
414
+ token0 = projectToken;
415
+ token1 = WETH_ADDR;
416
+ } else {
417
+ token0 = WETH_ADDR;
418
+ token1 = projectToken;
419
+ }
420
+
421
+ key = PoolKey({
422
+ currency0: Currency.wrap(token0),
423
+ currency1: Currency.wrap(token1),
424
+ fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
425
+ tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
426
+ hooks: IHooks(address(0))
427
+ });
428
+
429
+ // Initialize pool at price = 1.0 (tick 0).
430
+ uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(0);
431
+ poolManager.initialize(key, sqrtPrice);
432
+
433
+ // Fund LiquidityHelper with project tokens via JBTokens.mintFor (not deal).
434
+ // deal() skips ERC20Votes checkpoints, causing underflow when tokens are burned.
435
+ vm.prank(address(jbController()));
436
+ jbTokens().mintFor(address(liqHelper), revnetId, liquidityTokenAmount);
437
+ vm.deal(address(liqHelper), liquidityTokenAmount);
438
+ vm.prank(address(liqHelper));
439
+ IWETH9(WETH_ADDR).deposit{value: liquidityTokenAmount}();
440
+
441
+ // Approve PoolManager to spend tokens from LiquidityHelper.
442
+ vm.startPrank(address(liqHelper));
443
+ IERC20(projectToken).approve(address(poolManager), type(uint256).max);
444
+ IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
445
+ vm.stopPrank();
446
+
447
+ // Add full-range liquidity.
448
+ int256 liquidityDelta = int256(liquidityTokenAmount / 2);
449
+ vm.prank(address(liqHelper));
450
+ liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
451
+
452
+ // Mock the oracle at address(0) for hookless pools.
453
+ // The buyback hook calls IGeomeanOracle(address(key.hooks)).observe() for TWAP.
454
+ // Since hooks = address(0), we need code there + a mock response.
455
+ // tick=0 means 1:1 price → TWAP says pool rate is ~1 token/WETH → minting wins.
456
+ _mockOracle(liquidityDelta, 0, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
457
+
458
+ // Cache immutables before prank (vm.prank only applies to the next call).
459
+ uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
460
+
461
+ // Register pool with buyback hook via split operator (multisig has SET_BUYBACK_POOL permission).
462
+ vm.prank(multisig());
463
+ BUYBACK_HOOK.setPoolFor({
464
+ projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
465
+ });
466
+ }
467
+
468
+ /// @notice Mock the IGeomeanOracle at address(0) for hookless pools.
469
+ /// @param liquidity The liquidity to use for secondsPerLiquidity computation.
470
+ /// @param tick The TWAP tick to report (e.g. 0 for 1:1 price).
471
+ /// @param twapWindow The TWAP window in seconds (must match the buyback hook's configured window).
472
+ function _mockOracle(int256 liquidity, int24 tick, uint32 twapWindow) internal {
473
+ // Etch minimal bytecode at address(0) so it's treated as a contract.
474
+ vm.etch(address(0), hex"00");
475
+
476
+ int56[] memory tickCumulatives = new int56[](2);
477
+ tickCumulatives[0] = 0;
478
+ // arithmeticMeanTick = (tickCumulatives[1] - tickCumulatives[0]) / twapWindow = tick
479
+ tickCumulatives[1] = int56(tick) * int56(int32(twapWindow));
480
+
481
+ uint160[] memory secondsPerLiquidityCumulativeX128s = new uint160[](2);
482
+ secondsPerLiquidityCumulativeX128s[0] = 0;
483
+ uint256 liq = uint256(liquidity > 0 ? liquidity : -liquidity);
484
+ if (liq == 0) liq = 1;
485
+ secondsPerLiquidityCumulativeX128s[1] = uint160((uint256(twapWindow) << 128) / liq);
486
+
487
+ vm.mockCall(
488
+ address(0),
489
+ abi.encodeWithSelector(IGeomeanOracle.observe.selector),
490
+ abi.encode(tickCumulatives, secondsPerLiquidityCumulativeX128s)
491
+ );
492
+ }
493
+
494
+ /// @notice Build payment metadata with both 721 tier selection AND buyback quote.
495
+ function _buildPayMetadataWithQuote(
496
+ address hookMetadataTarget,
497
+ uint256 amountToSwapWith,
498
+ uint256 minimumSwapAmountOut
499
+ )
500
+ internal
501
+ view
502
+ returns (bytes memory)
503
+ {
504
+ // 721 tier metadata: mint tier 1.
505
+ uint16[] memory tierIds = new uint16[](1);
506
+ tierIds[0] = 1;
507
+ bytes memory tierData = abi.encode(true, tierIds); // (allowOverspending, tierIdsToMint)
508
+ bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
509
+
510
+ // Buyback quote metadata.
511
+ bytes memory quoteData = abi.encode(amountToSwapWith, minimumSwapAmountOut);
512
+ bytes4 quoteMetadataId = JBMetadataResolver.getId("quote");
513
+
514
+ // Combine both metadata entries.
515
+ bytes4[] memory ids = new bytes4[](2);
516
+ ids[0] = tierMetadataId;
517
+ ids[1] = quoteMetadataId;
518
+ bytes[] memory datas = new bytes[](2);
519
+ datas[0] = tierData;
520
+ datas[1] = quoteData;
521
+
522
+ return JBMetadataResolver.createMetadata(ids, datas);
523
+ }
524
+
525
+ /// @notice Build payment metadata with only 721 tier selection (no quote → TWAP/spot fallback).
526
+ function _buildPayMetadataNoQuote(address hookMetadataTarget) internal view returns (bytes memory) {
527
+ uint16[] memory tierIds = new uint16[](1);
528
+ tierIds[0] = 1;
529
+ bytes memory tierData = abi.encode(true, tierIds);
530
+ bytes4 tierMetadataId = JBMetadataResolver.getId("pay", hookMetadataTarget);
531
+
532
+ bytes4[] memory ids = new bytes4[](1);
533
+ ids[0] = tierMetadataId;
534
+ bytes[] memory datas = new bytes[](1);
535
+ datas[0] = tierData;
536
+
537
+ return JBMetadataResolver.createMetadata(ids, datas);
538
+ }
539
+
540
+ // ───────────────────────── Tests
541
+ // ─────────────────────────
542
+
543
+ /// @notice SWAP PATH: Pool offers good rate → buyback hook swaps on AMM instead of minting.
544
+ /// With 30% tier split, the buyback should swap with 0.7 ETH worth.
545
+ /// Terminal mints 0 tokens (weight=0), buyback hook mints via controller after swap.
546
+ function test_fork_swapPath_splitWithBuyback() public onlyFork {
547
+ (uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
548
+
549
+ // Set up pool with deep liquidity at 1:1 price (pool offers ~1 token per WETH).
550
+ // The issuance rate is 1000 tokens/ETH, so the pool rate (~1 token/WETH) is much worse.
551
+ // Wait — at 1:1 pool price, 1 WETH gets ~1 token. Minting gets 1000 tokens.
552
+ // So minting is better → buyback will NOT swap.
553
+ // To make swap win, we need the pool to offer MORE tokens per WETH than minting.
554
+ // Minting rate = 1000 tokens/ETH (before split reduction, the buyback sees reduced weight).
555
+ //
556
+ // After REVDeployer scales weight: weight = 1000e18 * 0.7e18 / 1e18 = 700e18
557
+ // The buyback hook receives weight=700e18 and amount=0.7 ETH.
558
+ // tokenCountWithoutHook = mulDiv(0.7e18, 700e18, 1e18) = 490 tokens.
559
+ //
560
+ // Wait, that's wrong. Let me re-trace:
561
+ // REVDeployer.beforePayRecordedWith:
562
+ // 1. 721 hook returns splitAmount=0.3 ETH → projectAmount = 0.7 ETH
563
+ // 2. buybackHookContext.amount.value = 0.7 ETH, weight = context.weight = 1000e18
564
+ // 3. Buyback hook sees: amountToSwapWith = 0.7 ETH, weight = 1000e18
565
+ // tokenCountWithoutHook = mulDiv(0.7e18, 1000e18, 1e18) = 700 tokens
566
+ // 4. If pool offers > 700 tokens for 0.7 WETH → swap wins
567
+ // 5. If pool offers < 700 tokens → mint wins
568
+ //
569
+ // At 1:1 pool price, 0.7 WETH gets ~0.7 tokens (after fees). That's way less than 700.
570
+ // We need a pool priced so projectToken is CHEAP — e.g., 1 WETH = 2000 tokens.
571
+ //
572
+ // Let's create a pool at a tick where projectToken is very cheap.
573
+ // tick = -69_000 gives approximately 1 WETH = 1000 tokens. We want more than 700 for 0.7 WETH.
574
+ // Actually, let's just seed the pool with lots of project tokens and little WETH.
575
+ // This naturally makes project tokens cheaper.
576
+
577
+ // Instead of tick manipulation, let's just use a pool at tick 0 (1:1) but seed asymmetrically:
578
+ // Lots of project tokens, little WETH → effective price favors the buyer.
579
+ // Actually V4 pool price is set at initialization (sqrtPriceX96), seeding doesn't change the tick.
580
+ //
581
+ // Let's initialize at a tick where 1 WETH = many project tokens.
582
+ // For swap to win: pool must give > 700 tokens for 0.7 WETH.
583
+ // Rate needed: > 1000 tokens/WETH.
584
+ // Use tick = -69082 which gives ~1:1000 ratio (1 WETH ≈ 1000 tokens).
585
+ // With 0.3% fee and slippage, it might give ~997, which is still > 700. Swap wins.
586
+
587
+ address projectToken = address(jbTokens().tokenOf(revnetId));
588
+ require(projectToken != address(0), "project token not deployed");
589
+
590
+ bool projectTokenIs0 = projectToken < WETH_ADDR;
591
+
592
+ // Build sorted pool key.
593
+ address token0 = projectTokenIs0 ? projectToken : WETH_ADDR;
594
+ address token1 = projectTokenIs0 ? WETH_ADDR : projectToken;
595
+
596
+ PoolKey memory key = PoolKey({
597
+ currency0: Currency.wrap(token0),
598
+ currency1: Currency.wrap(token1),
599
+ fee: REV_DEPLOYER.DEFAULT_BUYBACK_POOL_FEE(),
600
+ tickSpacing: REV_DEPLOYER.DEFAULT_BUYBACK_TICK_SPACING(),
601
+ hooks: IHooks(address(0))
602
+ });
603
+
604
+ // Set initial tick so that 1 WETH = ~2000 project tokens.
605
+ // If projectToken is token0: price = token1/token0 = WETH/projectToken.
606
+ // We want projectToken cheap → WETH/projectToken high → tick positive.
607
+ // tick ~= 76_000 → price ~= 2000.
608
+ // If WETH is token0: price = token1/token0 = projectToken/WETH.
609
+ // We want projectToken/WETH high → tick positive.
610
+ // tick ~= 76_000 → price ~= 2000.
611
+ // Either way: positive tick ≈ 2000 of token1 per token0.
612
+ //
613
+ // But we want "1 WETH = 2000 projectTokens".
614
+ // If projectToken is token0: price = WETH per projectToken = 1/2000 → negative tick.
615
+ // tick ≈ -76_000.
616
+ // If WETH is token0: price = projectToken per WETH = 2000 → positive tick.
617
+ // tick ≈ 76_000.
618
+ int24 initTick;
619
+ if (projectTokenIs0) {
620
+ // price = WETH/projectToken = 0.0005 → tick ≈ -76_000
621
+ initTick = -76_020; // Rounded to tickSpacing=60
622
+ } else {
623
+ // price = projectToken/WETH = 2000 → tick ≈ 76_000
624
+ initTick = 76_020;
625
+ }
626
+
627
+ uint160 sqrtPrice = TickMath.getSqrtPriceAtTick(initTick);
628
+ poolManager.initialize(key, sqrtPrice);
629
+
630
+ // Seed liquidity. We need both tokens.
631
+ // IMPORTANT: Use JBTokens.mintFor (not deal) so ERC20Votes checkpoints are updated.
632
+ // deal() only sets balanceOf/totalSupply but skips Votes checkpoints, causing burn underflow.
633
+ uint256 projectLiq = 10_000_000e18; // lots of project tokens
634
+ uint256 wethLiq = 5000e18; // some WETH
635
+
636
+ vm.prank(address(jbController()));
637
+ jbTokens().mintFor(address(liqHelper), revnetId, projectLiq);
638
+ vm.deal(address(liqHelper), wethLiq);
639
+ vm.prank(address(liqHelper));
640
+ IWETH9(WETH_ADDR).deposit{value: wethLiq}();
641
+
642
+ vm.startPrank(address(liqHelper));
643
+ IERC20(projectToken).approve(address(poolManager), type(uint256).max);
644
+ IERC20(WETH_ADDR).approve(address(poolManager), type(uint256).max);
645
+ vm.stopPrank();
646
+
647
+ // Add full-range liquidity.
648
+ int256 liquidityDelta = int256(wethLiq / 4); // Use fraction for liquidity units
649
+ vm.prank(address(liqHelper));
650
+ liqHelper.addLiquidity(key, TICK_LOWER, TICK_UPPER, liquidityDelta);
651
+
652
+ // Mock the oracle at address(0) to report the actual pool price (initTick).
653
+ // This makes the TWAP quote reflect ~2000 tokens/WETH, so the swap path wins.
654
+ _mockOracle(liquidityDelta, initTick, uint32(REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW()));
655
+
656
+ // Register pool with buyback hook.
657
+ uint256 twapWindow = REV_DEPLOYER.DEFAULT_BUYBACK_TWAP_WINDOW();
658
+ vm.prank(multisig());
659
+ BUYBACK_HOOK.setPoolFor({
660
+ projectId: revnetId, poolKey: key, twapWindow: twapWindow, terminalToken: JBConstants.NATIVE_TOKEN
661
+ });
662
+
663
+ // Build metadata: mint tier 1 + quote for swap.
664
+ // The quote tells buyback to swap with the full amount, expecting at least 1 token out.
665
+ address metadataTarget = hook.METADATA_ID_TARGET();
666
+ bytes memory metadata = _buildPayMetadataWithQuote({
667
+ hookMetadataTarget: metadataTarget,
668
+ amountToSwapWith: 0.7 ether, // projectAmount after 30% split
669
+ minimumSwapAmountOut: 1 // Accept any amount from swap
670
+ });
671
+
672
+ // Record payer balance before.
673
+ uint256 payerBalBefore = jbTokens().totalBalanceOf(PAYER, revnetId);
674
+
675
+ // Pay 1 ETH through the terminal.
676
+ vm.prank(PAYER);
677
+ uint256 terminalTokensReturned = jbMultiTerminal().pay{value: 1 ether}({
678
+ projectId: revnetId,
679
+ token: JBConstants.NATIVE_TOKEN,
680
+ amount: 1 ether,
681
+ beneficiary: PAYER,
682
+ minReturnedTokens: 0,
683
+ memo: "Fork: swap path with splits",
684
+ metadata: metadata
685
+ });
686
+
687
+ // pay() returns beneficiaryBalanceAfter - beneficiaryBalanceBefore, capturing ALL token sources.
688
+ // In the SWAP path:
689
+ // - Terminal mints 0 tokens (weight=0 from buyback hook)
690
+ // - Buyback hook's afterPay swaps 0.7 ETH on AMM and mints via controller
691
+ // - Pool at ~2000:1 price → 0.7 ETH yields ~1400 tokens (minus pool fee)
692
+ // - pay() returns the total (0 from terminal + ~1400 from buyback swap)
693
+ // - More than the 700 tokens minting would produce → swap was the right call
694
+ assertGt(terminalTokensReturned, 700e18, "swap path: should get more tokens than minting (pool rate better)");
695
+
696
+ console.log(
697
+ " Swap path: buyback swapped for %s tokens (minting would give 700)", terminalTokensReturned / 1e18
698
+ );
699
+ }
700
+
701
+ /// @notice MINT PATH: Pool offers bad rate → buyback decides minting is better.
702
+ /// With 30% tier split, REVDeployer scales weight from 1000e18 to 700e18.
703
+ /// Terminal mints 700 tokens.
704
+ function test_fork_mintPath_splitWithBuyback() public onlyFork {
705
+ (uint256 revnetId, IJB721TiersHook hook) = _deployRevnetWith721();
706
+
707
+ // Set up pool with 1:1 price. At this price:
708
+ // 0.7 WETH → ~0.7 tokens from pool (after fees).
709
+ // Direct minting: 700 tokens.
710
+ // Minting wins by a huge margin → buyback returns context.weight unchanged.
711
+ _setupPool(revnetId, 10_000 ether);
712
+
713
+ // Build metadata: mint tier 1 + quote for "swap" with 0.7 ETH, but expect many tokens (forces mint path).
714
+ // When minimumSwapAmountOut > actual pool output, the buyback hook falls back to minting.
715
+ // Actually the buyback hook uses max(payerQuote, twapQuote). If we set minimumSwapAmountOut=0,
716
+ // it'll use the TWAP/spot quote. At 1:1 pool price, spot says ~0.7 tokens for 0.7 WETH.
717
+ // tokenCountWithoutHook = 700 tokens. 700 > ~0.7 → mint wins.
718
+ // We don't even need quote metadata — the spot fallback handles it.
719
+ address metadataTarget = hook.METADATA_ID_TARGET();
720
+ bytes memory metadata = _buildPayMetadataNoQuote(metadataTarget);
721
+
722
+ // Pay 1 ETH through the terminal.
723
+ vm.prank(PAYER);
724
+ uint256 tokensReceived = jbMultiTerminal().pay{value: 1 ether}({
725
+ projectId: revnetId,
726
+ token: JBConstants.NATIVE_TOKEN,
727
+ amount: 1 ether,
728
+ beneficiary: PAYER,
729
+ minReturnedTokens: 0,
730
+ memo: "Fork: mint path with splits",
731
+ metadata: metadata
732
+ });
733
+
734
+ // Mint path: buyback returns context.weight unchanged.
735
+ // REVDeployer scales: weight = 1000e18 * 0.7e18 / 1e18 = 700e18.
736
+ // Terminal: tokenCount = mulDiv(1e18, 700e18, 1e18) = 700e18.
737
+ uint256 expectedTokens = 700e18;
738
+
739
+ assertEq(tokensReceived, expectedTokens, "mint path: should receive 700 tokens (weight scaled for 30% split)");
740
+
741
+ console.log(" Mint path: terminal minted %s tokens (expected 700)", tokensReceived / 1e18);
742
+ }
743
+
744
+ /// @notice MINT PATH without splits: baseline confirming 1000 tokens for 1 ETH.
745
+ function test_fork_mintPath_noSplits_fullTokens() public onlyFork {
746
+ (uint256 revnetId,) = _deployRevnetWith721();
747
+ _setupPool(revnetId, 10_000 ether);
748
+
749
+ // Pay 1 ETH with NO tier metadata (no NFT purchase, no splits).
750
+ vm.prank(PAYER);
751
+ uint256 tokensReceived = jbMultiTerminal().pay{value: 1 ether}({
752
+ projectId: revnetId,
753
+ token: JBConstants.NATIVE_TOKEN,
754
+ amount: 1 ether,
755
+ beneficiary: PAYER,
756
+ minReturnedTokens: 0,
757
+ memo: "Fork: no split baseline",
758
+ metadata: ""
759
+ });
760
+
761
+ // No splits → no weight reduction. Full 1000 tokens.
762
+ uint256 expectedTokens = 1000e18;
763
+ assertEq(tokensReceived, expectedTokens, "no splits: should receive 1000 tokens");
764
+ }
765
+
766
+ /// @notice Invariant: tokens / projectAmount rate is identical with and without splits.
767
+ function test_fork_invariant_tokenPerEthConsistent() public onlyFork {
768
+ // --- Revnet 1: with 721 splits (30%) ---
769
+ (uint256 revnetId1, IJB721TiersHook hook1) = _deployRevnetWith721();
770
+ _setupPool(revnetId1, 10_000 ether);
771
+
772
+ address metadataTarget1 = hook1.METADATA_ID_TARGET();
773
+ bytes memory metadata1 = _buildPayMetadataNoQuote(metadataTarget1);
774
+
775
+ vm.prank(PAYER);
776
+ uint256 tokens1 = jbMultiTerminal().pay{value: 1 ether}({
777
+ projectId: revnetId1,
778
+ token: JBConstants.NATIVE_TOKEN,
779
+ amount: 1 ether,
780
+ beneficiary: PAYER,
781
+ minReturnedTokens: 0,
782
+ memo: "invariant: with splits",
783
+ metadata: metadata1
784
+ });
785
+
786
+ // --- Revnet 2: no splits (plain payment, no tier metadata) ---
787
+ // Deploy a second revnet without 721 hook.
788
+ (REVConfig memory cfg2, JBTerminalConfig[] memory tc2, REVSuckerDeploymentConfig memory sdc2) =
789
+ _buildMinimalConfig();
790
+ cfg2.description = REVDescription("NoSplit Fork", "NSF", "ipfs://nosplit", "NSF_SALT");
791
+
792
+ uint256 revnetId2 = REV_DEPLOYER.deployFor({
793
+ revnetId: 0, configuration: cfg2, terminalConfigurations: tc2, suckerDeploymentConfiguration: sdc2
794
+ });
795
+
796
+ // Set up pool for revnet2 too (so buyback hook has a pool, but will choose mint at 1:1).
797
+ _setupPool(revnetId2, 10_000 ether);
798
+
799
+ vm.prank(PAYER);
800
+ uint256 tokens2 = jbMultiTerminal().pay{value: 1 ether}({
801
+ projectId: revnetId2,
802
+ token: JBConstants.NATIVE_TOKEN,
803
+ amount: 1 ether,
804
+ beneficiary: PAYER,
805
+ minReturnedTokens: 0,
806
+ memo: "invariant: no splits",
807
+ metadata: ""
808
+ });
809
+
810
+ // Rate check: tokens / projectAmount should be the same.
811
+ // Revnet 1: 700 tokens / 0.7 ETH = 1000 tokens/ETH
812
+ // Revnet 2: 1000 tokens / 1.0 ETH = 1000 tokens/ETH
813
+ uint256 projectAmount1 = 0.7 ether;
814
+ uint256 projectAmount2 = 1 ether;
815
+
816
+ uint256 rate1 = (tokens1 * 1e18) / projectAmount1;
817
+ uint256 rate2 = (tokens2 * 1e18) / projectAmount2;
818
+
819
+ assertEq(rate1, rate2, "token-per-ETH rate should be identical with and without splits");
820
+ }
821
+ }