@rev-net/core-v6 0.0.37 → 0.0.39

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