@zoralabs/coins 2.3.0 → 2.3.1

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 (53) hide show
  1. package/.turbo/turbo-build$colon$js.log +116 -97
  2. package/CHANGELOG.md +7 -1
  3. package/README.md +1 -0
  4. package/abis/AddressConstants.json +7 -0
  5. package/abis/BaseTest.json +62 -0
  6. package/abis/BuySupplyWithV4SwapHook.json +429 -0
  7. package/abis/IUniswapV4Router04.json +484 -0
  8. package/abis/MockAirlock.json +39 -0
  9. package/abis/SimpleERC20.json +326 -0
  10. package/addresses/8453.json +7 -9
  11. package/audits/report-cantinacode-zora-1021.pdf +0 -0
  12. package/dist/index.cjs +140 -19
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.js +139 -18
  15. package/dist/index.js.map +1 -1
  16. package/dist/wagmiGenerated.d.ts +205 -28
  17. package/dist/wagmiGenerated.d.ts.map +1 -1
  18. package/package/wagmiGenerated.ts +139 -18
  19. package/package.json +1 -1
  20. package/script/DeployPostDeploymentHooks.s.sol +1 -3
  21. package/src/deployment/CoinsDeployerBase.sol +9 -8
  22. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
  23. package/src/utils/AutoSwapper.sol +1 -1
  24. package/src/version/ContractVersionBase.sol +1 -1
  25. package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
  26. package/test/Coin.t.sol +21 -9
  27. package/test/CoinUniV4.t.sol +1 -2
  28. package/test/ContentCoinRewards.t.sol +1 -3
  29. package/test/CreatorCoin.t.sol +1 -4
  30. package/test/CreatorCoinRewards.t.sol +1 -3
  31. package/test/Factory.t.sol +3 -3
  32. package/test/MultiOwnable.t.sol +4 -4
  33. package/test/Upgrades.t.sol +26 -17
  34. package/test/ZoraHookRegistry.t.sol +19 -9
  35. package/test/mocks/MockAirlock.sol +22 -0
  36. package/test/mocks/SimpleERC20.sol +8 -0
  37. package/test/utils/BaseTest.sol +155 -2
  38. package/test/utils/hookmate/README.md +50 -0
  39. package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
  40. package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
  41. package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
  42. package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
  43. package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
  44. package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
  45. package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
  46. package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
  47. package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
  48. package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
  49. package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
  50. package/wagmi.config.ts +1 -1
  51. package/src/utils/uniswap/BytesLib.sol +0 -35
  52. package/src/utils/uniswap/Path.sol +0 -31
  53. /package/abis/{VmContractHelper226.json → VmContractHelper239.json} +0 -0
@@ -0,0 +1,509 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
+ import {BuySupplyWithV4SwapHook} from "../src/hooks/deployment/BuySupplyWithV4SwapHook.sol";
6
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
8
+ import {ICoin} from "../src/interfaces/ICoin.sol";
9
+ import {ISwapRouter} from "../src/interfaces/ISwapRouter.sol";
10
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
11
+ import {ContentCoin} from "../src/ContentCoin.sol";
12
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
13
+ import {console} from "forge-std/console.sol";
14
+
15
+ contract BuySupplyWithV4SwapHookTest is BaseTest {
16
+ address constant ZORA = 0x1111111111166b7FE7bd91427724B487980aFc69;
17
+ BuySupplyWithV4SwapHook postDeployHook;
18
+
19
+ // TODO: Add tests to verify swap path always goes from input currency to backing currency
20
+ // 1. Test V3-only swap paths (e.g., USDC -> WETH -> Creator Coin)
21
+ // 2. Test V4-only swap paths (e.g., WETH -> Creator Coin via V4)
22
+ // 3. Test mixed V3->V4 swap paths (e.g., USDC -> WETH via V3, then WETH -> Creator Coin via V4)
23
+ // 4. Test different input currencies (USDC, WETH, other ERC20s) all properly route to backing currency
24
+ // 5. Verify the final currency received always matches the Content Coin's backing currency
25
+ // 6. Clean up debug logging in BuySupplyWithV4SwapHook.sol
26
+
27
+ function setUp() public override {
28
+ super.setUpWithBlockNumber(33646532);
29
+
30
+ postDeployHook = new BuySupplyWithV4SwapHook(factory, address(swapRouter), address(V4_POOL_MANAGER));
31
+ }
32
+
33
+ function _encodeV4HookData(
34
+ address buyRecipient,
35
+ bytes memory v3Route,
36
+ PoolKey[] memory v4Route,
37
+ address inputCurrency,
38
+ uint256 inputAmount,
39
+ uint256 minAmountOut
40
+ ) internal pure returns (bytes memory) {
41
+ BuySupplyWithV4SwapHook.InitialSupplyParams memory params = BuySupplyWithV4SwapHook.InitialSupplyParams({
42
+ buyRecipient: buyRecipient,
43
+ v3Route: v3Route,
44
+ v4Route: v4Route,
45
+ inputCurrency: inputCurrency,
46
+ inputAmount: inputAmount,
47
+ minAmountOut: minAmountOut
48
+ });
49
+ return abi.encode(params);
50
+ }
51
+
52
+ function _encodeV3Path(address tokenA, uint24 feeA, address tokenB, uint24 feeB, address tokenC) internal pure returns (bytes memory) {
53
+ return abi.encodePacked(tokenA, feeA, tokenB, feeB, tokenC);
54
+ }
55
+
56
+ function _encodeV3PathSingle(address tokenA, uint24 fee, address tokenB) internal pure returns (bytes memory) {
57
+ return abi.encodePacked(tokenA, fee, tokenB);
58
+ }
59
+
60
+ function _deployCreatorCoin(address payoutRecipient) internal returns (address creatorCoinAddress) {
61
+ bytes memory creatorPoolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
62
+
63
+ vm.prank(payoutRecipient);
64
+ creatorCoinAddress = factory.deployCreatorCoin(
65
+ payoutRecipient, // payoutRecipient
66
+ _getDefaultOwners(), // owners
67
+ "https://creator.com", // uri
68
+ "Creator Coin", // name
69
+ "CREATOR", // symbol
70
+ creatorPoolConfig, // poolConfig (ZORA-backed)
71
+ users.platformReferrer, // platformReferrer
72
+ bytes32(0) // coinSalt
73
+ );
74
+ }
75
+
76
+ function _deployContentCoinWithHook(
77
+ address backingCurrency,
78
+ uint256 payableAmount,
79
+ address caller,
80
+ bytes memory hookData
81
+ ) internal returns (address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) {
82
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(backingCurrency);
83
+
84
+ vm.prank(caller);
85
+ bytes memory hookDataOut;
86
+ (coinAddress, hookDataOut) = factory.deployWithHook{value: payableAmount}(
87
+ caller, // payoutRecipient
88
+ _getDefaultOwners(), // owners
89
+ "https://test.com", // uri
90
+ "Content Coin", // name
91
+ "CONTENT", // symbol
92
+ poolConfig, // poolConfig
93
+ users.platformReferrer, // platformReferrer
94
+ address(postDeployHook), // postDeployHook
95
+ hookData // postDeployHookData
96
+ );
97
+
98
+ (amountCurrency, coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
99
+ }
100
+
101
+ /// @dev Test buying initial supply of a Content Coin backed by ZORA
102
+ /// This only requires V3 swap (ETH -> ZORA) since the coin is already backed by ZORA
103
+ function test_buyContentCoinSupply_V3SwapOnly() public {
104
+ uint256 initialOrderSize = 0.1 ether;
105
+ vm.deal(users.creator, initialOrderSize);
106
+
107
+ // Create V3 path: ETH -> USDC -> ZORA
108
+ bytes memory v3Route = _encodeV3Path(
109
+ address(weth),
110
+ 3000, // WETH/USDC 0.3%
111
+ USDC_ADDRESS,
112
+ 3000, // USDC/ZORA 0.3%
113
+ ZORA
114
+ );
115
+
116
+ console.logBytes(v3Route);
117
+
118
+ // No V4 route needed since coin is backed by ZORA
119
+ PoolKey[] memory v4Route = new PoolKey[](0);
120
+
121
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
122
+
123
+ // Deploy Content Coin backed by ZORA
124
+ (address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(ZORA, initialOrderSize, users.creator, hookData);
125
+
126
+ ContentCoin coin = ContentCoin(payable(coinAddress));
127
+
128
+ // Verify the coin is properly configured
129
+ assertEq(coin.currency(), ZORA, "Coin should be backed by ZORA");
130
+ assertGt(amountCurrency, 0, "Should have received ZORA from V3 swap");
131
+ assertGt(coinsPurchased, 0, "Should have purchased coins");
132
+
133
+ // Creator should have their launch supply + purchased coins
134
+ assertEq(
135
+ coin.balanceOf(users.creator),
136
+ CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
137
+ "Creator should have launch supply + purchased coins"
138
+ );
139
+
140
+ // Verify V3 swap worked correctly (mock implementation returns positive values)
141
+ // Note: In real implementation this would check actual pool liquidity
142
+ }
143
+
144
+ /// @dev Test that BuyInitialSupply event is emitted with accurate data using snapshot pattern
145
+ function test_BuyInitialSupplyEvent() public {
146
+ uint256 initialOrderSize = 0.1 ether;
147
+ vm.deal(users.creator, initialOrderSize * 2); // Double to account for both runs
148
+
149
+ // Create V3 path: ETH -> USDC -> ZORA
150
+ bytes memory v3Route = _encodeV3Path(
151
+ address(weth),
152
+ 3000, // WETH/USDC 0.3%
153
+ USDC_ADDRESS,
154
+ 3000, // USDC/ZORA 0.3%
155
+ ZORA
156
+ );
157
+
158
+ // No V4 route needed since coin is backed by ZORA
159
+ PoolKey[] memory v4Route = new PoolKey[](0);
160
+
161
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
162
+
163
+ PoolKey[] memory expectedV4Route = new PoolKey[](1);
164
+
165
+ // FIRST RUN: Use snapshot pattern to capture expected values
166
+ uint256 snapshot = vm.snapshot();
167
+
168
+ // Execute deployment to get actual values
169
+ (address coinAddress, uint256 expectedAmountCurrency, uint256 expectedCoinsPurchased) = _deployContentCoinWithHook(
170
+ ZORA,
171
+ initialOrderSize,
172
+ users.creator,
173
+ hookData
174
+ );
175
+
176
+ expectedV4Route[0] = ICoin(payable(coinAddress)).getPoolKey();
177
+
178
+ // Revert to snapshot to restore state
179
+ vm.revertToState(snapshot);
180
+
181
+ // SECOND RUN: Execute with event verification using captured values
182
+ // Note: We skip checking coin address (first indexed param) since it will be different after snapshot revert
183
+ vm.expectEmit(false, true, true, true); // Skip coin address, check recipient, coinsPurchased, and all data
184
+ emit BuySupplyWithV4SwapHook.BuyInitialSupply(
185
+ address(0), // coin (indexed) - skip checking since address will differ after revert
186
+ users.creator, // recipient (indexed)
187
+ expectedCoinsPurchased, // coinsPurchased (indexed)
188
+ v3Route, // v3Route (data)
189
+ expectedV4Route, // v4Route (data)
190
+ address(0), // inputCurrency (data) - ETH represented as address(0)
191
+ initialOrderSize, // inputAmount (data)
192
+ expectedAmountCurrency // v4SwapInput (data) - amount received from V3 swap
193
+ );
194
+
195
+ // Deploy Content Coin backed by ZORA - this should emit event with matching parameters
196
+ _deployContentCoinWithHook(ZORA, initialOrderSize, users.creator, hookData);
197
+ }
198
+
199
+ /// @dev Test buying initial supply of a Content Coin paired with ETH
200
+ /// This requires no V3 or V4 routing - just direct V4 swap with ETH
201
+ function test_buyContentCoinSupply_ETHPaired() public {
202
+ uint256 initialOrderSize = 0.05 ether;
203
+ vm.deal(users.creator, initialOrderSize);
204
+
205
+ // No V3 route needed - direct ETH to coin swap
206
+ bytes memory v3Route = "";
207
+
208
+ // No V4 route needed - direct swap
209
+ PoolKey[] memory v4Route = new PoolKey[](0);
210
+
211
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
212
+
213
+ // Deploy Content Coin paired with ETH (address(0))
214
+ (address coinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
215
+ address(0),
216
+ initialOrderSize,
217
+ users.creator,
218
+ hookData
219
+ );
220
+
221
+ ContentCoin coin = ContentCoin(payable(coinAddress));
222
+
223
+ // Verify the coin is properly configured as ETH-paired
224
+ assertEq(coin.currency(), address(0), "Coin should be paired with ETH");
225
+ assertEq(amountCurrency, initialOrderSize, "Should have used all ETH directly");
226
+ assertGt(coinsPurchased, 0, "Should have purchased coins");
227
+
228
+ // Creator should have their launch reward + purchased coins
229
+ assertEq(
230
+ coin.balanceOf(users.creator),
231
+ CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
232
+ "Creator should have launch reward + purchased coins"
233
+ );
234
+ }
235
+
236
+ /// @dev Test deploying Content Coin with owned Creator Coin tokens (no ETH, no V3 swap)
237
+ /// This demonstrates using existing ERC20 tokens to purchase initial supply during deployment
238
+ function test_buyContentCoinSupply_WithOwnedCreatorCoins() public {
239
+ // STEP 1: Deploy Creator Coin backed by ZORA
240
+ address creatorCoinAddress = _deployCreatorCoin(users.creator);
241
+
242
+ // STEP 2: Give another user ZORA tokens and have them swap for Creator Coins
243
+ address anotherCreator = makeAddr("anotherCreator");
244
+ uint256 zoraAmount = 10e18; // 10 ZORA tokens
245
+ deal(ZORA, anotherCreator, zoraAmount);
246
+ assertEq(IERC20(ZORA).balanceOf(anotherCreator), zoraAmount, "anotherCreator should have ZORA tokens");
247
+
248
+ // Swap ZORA tokens for Creator Coins using proper V4 swap mechanism
249
+ uint128 swapAmountIn = uint128(zoraAmount);
250
+ _swapSomeCurrencyForCoin(ICoin(payable(creatorCoinAddress)), ZORA, swapAmountIn, anotherCreator);
251
+
252
+ uint256 creatorCoinAmount = IERC20(creatorCoinAddress).balanceOf(anotherCreator);
253
+
254
+ // STEP 3: Have anotherCreator approve the hook to spend their Creator Coins
255
+ vm.prank(anotherCreator);
256
+ IERC20(creatorCoinAddress).approve(address(postDeployHook), creatorCoinAmount);
257
+
258
+ // STEP 4: Deploy Content Coin backed by Creator Coin using owned tokens
259
+
260
+ // No V3 route needed - anotherCreator already has Creator Coins
261
+ bytes memory v3Route = "";
262
+
263
+ // No V4 route needed - direct Creator Coin to Content Coin swap
264
+ PoolKey[] memory v4Route = new PoolKey[](0);
265
+
266
+ bytes memory hookData = _encodeV4HookData(anotherCreator, v3Route, v4Route, creatorCoinAddress, creatorCoinAmount, 0);
267
+
268
+ // Deploy with amount = 0 (no ETH needed since using owned tokens)
269
+ (address contentCoinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
270
+ creatorCoinAddress,
271
+ 0, // No ETH needed
272
+ anotherCreator,
273
+ hookData
274
+ );
275
+
276
+ ContentCoin contentCoin = ContentCoin(payable(contentCoinAddress));
277
+
278
+ // Verify the content coin is properly configured
279
+ assertEq(contentCoin.currency(), creatorCoinAddress, "Content coin should be backed by Creator coin");
280
+ assertGt(amountCurrency, 0, "Should have used some Creator Coins");
281
+ assertGt(coinsPurchased, 0, "Should have purchased content coins");
282
+
283
+ // anotherCreator should have their launch reward + purchased content coins
284
+ assertEq(
285
+ contentCoin.balanceOf(anotherCreator),
286
+ CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
287
+ "anotherCreator should have launch reward + purchased content coins"
288
+ );
289
+
290
+ // Verify Creator Coin balance decreased
291
+ uint256 remainingCreatorCoins = IERC20(creatorCoinAddress).balanceOf(anotherCreator);
292
+ assertLt(remainingCreatorCoins, creatorCoinAmount, "anotherCreator should have spent some Creator Coins");
293
+ assertEq(remainingCreatorCoins, creatorCoinAmount - amountCurrency, "Creator Coin balance should decrease by amount used");
294
+ }
295
+
296
+ /// @dev Test buying initial supply of a Content Coin backed by a Creator Coin
297
+ /// This requires V3 swap (ETH -> ZORA) then V4 swap (ZORA -> Creator Coin -> Content Coin)
298
+ function test_buyContentCoinSupply_CreatorCoinBacked() public {
299
+ uint256 initialOrderSize = 0.08 ether;
300
+ vm.deal(users.creator, initialOrderSize);
301
+
302
+ // STEP 1: Deploy Creator Coin backed by ZORA
303
+ address creatorCoinAddress = _deployCreatorCoin(users.creator);
304
+
305
+ // STEP 2: Deploy Content Coin backed by Creator Coin
306
+
307
+ // Create V3 path: ETH -> USDC -> ZORA (to get the creator coin's backing currency)
308
+ bytes memory v3Route = _encodeV3Path(
309
+ address(weth),
310
+ 3000, // WETH/USDC 0.3%
311
+ USDC_ADDRESS,
312
+ 3000, // USDC/ZORA 0.3%
313
+ ZORA
314
+ );
315
+
316
+ // V4 route: ZORA -> Creator Coin (then Creator Coin -> Content Coin will be added automatically)
317
+ PoolKey[] memory v4Route = new PoolKey[](1);
318
+ v4Route[0] = ICoin(payable(creatorCoinAddress)).getPoolKey();
319
+
320
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, 0);
321
+
322
+ (address contentCoinAddress, uint256 amountCurrency, uint256 coinsPurchased) = _deployContentCoinWithHook(
323
+ creatorCoinAddress,
324
+ initialOrderSize,
325
+ users.creator,
326
+ hookData
327
+ );
328
+
329
+ ContentCoin contentCoin = ContentCoin(payable(contentCoinAddress));
330
+
331
+ // Verify the content coin is properly configured
332
+ assertEq(contentCoin.currency(), creatorCoinAddress, "Content coin should be backed by Creator coin");
333
+ assertGt(amountCurrency, 0, "Should have received ZORA from V3 swap");
334
+ assertGt(coinsPurchased, 0, "Should have purchased content coins");
335
+
336
+ // Creator should have their launch reward + purchased content coins
337
+ assertEq(
338
+ contentCoin.balanceOf(users.creator),
339
+ CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased,
340
+ "Creator should have launch reward + purchased content coins"
341
+ );
342
+ }
343
+
344
+ // ============ ERROR HANDLING TESTS ============
345
+
346
+ function test_RevertWhen_InsufficientInputCurrencyETH() public {
347
+ uint256 inputAmount = 1 ether;
348
+ uint256 insufficientAmount = 0.5 ether;
349
+
350
+ // Create V3 path: ETH -> USDC -> ZORA
351
+ bytes memory v3Route = _encodeV3Path(
352
+ address(weth),
353
+ 3000, // WETH/USDC 0.3%
354
+ USDC_ADDRESS,
355
+ 3000, // USDC/ZORA 0.3%
356
+ ZORA
357
+ );
358
+
359
+ PoolKey[] memory v4Route = new PoolKey[](0);
360
+
361
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), inputAmount, 0);
362
+
363
+ // Should revert with InsufficientInputCurrency
364
+ vm.deal(users.creator, insufficientAmount);
365
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
366
+ vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, insufficientAmount));
367
+
368
+ vm.prank(users.creator);
369
+ factory.deployWithHook{value: insufficientAmount}(
370
+ users.creator, // payoutRecipient
371
+ _getDefaultOwners(), // owners
372
+ "https://test.com", // uri
373
+ "Content Coin", // name
374
+ "CONTENT", // symbol
375
+ poolConfig, // poolConfig
376
+ users.platformReferrer, // platformReferrer
377
+ address(postDeployHook), // postDeployHook
378
+ hookData // postDeployHookData
379
+ );
380
+ }
381
+
382
+ function test_RevertWhen_InsufficientInputCurrencyERC20() public {
383
+ // Deploy Creator Coin first
384
+ address creatorCoinAddress = _deployCreatorCoin(users.creator);
385
+
386
+ uint256 userBalance = 500e18;
387
+
388
+ // Give user some Creator Coins but less than required
389
+ deal(creatorCoinAddress, users.creator, userBalance);
390
+
391
+ // Approve a small amount to spend for Creator Coins
392
+ vm.prank(users.creator);
393
+ IERC20(creatorCoinAddress).approve(address(postDeployHook), 1);
394
+
395
+ // No V3 route needed - user already has Creator Coins (but insufficient amount)
396
+ bytes memory v3Route = "";
397
+ PoolKey[] memory v4Route = new PoolKey[](0);
398
+
399
+ uint256 zoraAmount = 10e18; // 10 ZORA tokens
400
+ deal(ZORA, users.creator, zoraAmount);
401
+
402
+ // Swap ZORA tokens for Creator Coins using proper V4 swap mechanism
403
+ uint128 swapAmountIn = uint128(zoraAmount);
404
+ _swapSomeCurrencyForCoin(ICoin(payable(creatorCoinAddress)), ZORA, swapAmountIn, users.creator);
405
+
406
+ uint256 inputAmount = IERC20(creatorCoinAddress).balanceOf(users.creator);
407
+ uint256 amountToApprove = inputAmount / 2;
408
+
409
+ // only approve half of the input amount - it should revert
410
+ vm.prank(users.creator);
411
+ IERC20(creatorCoinAddress).approve(address(postDeployHook), amountToApprove);
412
+
413
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, creatorCoinAddress, inputAmount, 0);
414
+
415
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
416
+ // Should revert with InsufficientInputCurrency
417
+ vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, amountToApprove));
418
+
419
+ vm.prank(users.creator);
420
+ factory.deployWithHook(
421
+ users.creator, // payoutRecipient
422
+ _getDefaultOwners(), // owners
423
+ "https://test.com", // uri
424
+ "Content Coin", // name
425
+ "CONTENT", // symbol
426
+ poolConfig, // poolConfig
427
+ users.platformReferrer, // platformReferrer
428
+ address(postDeployHook), // postDeployHook
429
+ hookData // postDeployHookData
430
+ );
431
+ }
432
+
433
+ function test_RevertWhen_V3RouteDoesNotConnectToV4RouteStart() public {
434
+ // Deploy Creator Coin backed by ZORA
435
+ address creatorCoinAddress = _deployCreatorCoin(users.creator);
436
+
437
+ vm.deal(users.creator, 1 ether);
438
+
439
+ // Create V3 path that ends with USDC
440
+ bytes memory v3Route = _encodeV3PathSingle(
441
+ address(weth),
442
+ 3000, // WETH/USDC 0.3%
443
+ USDC_ADDRESS
444
+ );
445
+
446
+ // Create V4 route that starts with ZORA (not USDC - mismatch!)
447
+ PoolKey[] memory v4Route = new PoolKey[](1);
448
+ v4Route[0] = ICoin(payable(creatorCoinAddress)).getPoolKey();
449
+
450
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), 1 ether, 0);
451
+
452
+ // Should revert with V3RouteDoesNotConnectToV4RouteStart
453
+
454
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
455
+
456
+ vm.prank(users.creator);
457
+ vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.V3RouteDoesNotConnectToV4RouteStart.selector));
458
+ factory.deployWithHook{value: 1 ether}(
459
+ users.creator, // payoutRecipient
460
+ _getDefaultOwners(), // owners
461
+ "https://test.com", // uri
462
+ "Content Coin", // name
463
+ "CONTENT", // symbol
464
+ poolConfig, // poolConfig
465
+ users.platformReferrer, // platformReferrer
466
+ address(postDeployHook), // postDeployHook
467
+ hookData // postDeployHookData
468
+ );
469
+ }
470
+
471
+ function test_RevertWhen_InsufficientOutputAmount() public {
472
+ uint256 initialOrderSize = 0.1 ether;
473
+ vm.deal(users.creator, initialOrderSize);
474
+
475
+ // Create V3 path: ETH -> USDC -> ZORA
476
+ bytes memory v3Route = _encodeV3Path(
477
+ address(weth),
478
+ 3000, // WETH/USDC 0.3%
479
+ USDC_ADDRESS,
480
+ 3000, // USDC/ZORA 0.3%
481
+ ZORA
482
+ );
483
+
484
+ // No V4 route needed since coin is backed by ZORA
485
+ PoolKey[] memory v4Route = new PoolKey[](0);
486
+
487
+ // Set impossibly high minimum amount out (1 million coins)
488
+ uint256 impossibleMinAmountOut = type(uint256).max;
489
+
490
+ bytes memory hookData = _encodeV4HookData(users.creator, v3Route, v4Route, address(0), initialOrderSize, impossibleMinAmountOut);
491
+
492
+ // Should revert with InsufficientOutputAmount
493
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
494
+ vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientOutputAmount.selector));
495
+
496
+ vm.prank(users.creator);
497
+ factory.deployWithHook{value: initialOrderSize}(
498
+ users.creator, // payoutRecipient
499
+ _getDefaultOwners(), // owners
500
+ "https://test.com", // uri
501
+ "Content Coin", // name
502
+ "CONTENT", // symbol
503
+ poolConfig, // poolConfig
504
+ users.platformReferrer, // platformReferrer
505
+ address(postDeployHook), // postDeployHook
506
+ hookData // postDeployHookData
507
+ );
508
+ }
509
+ }
package/test/Coin.t.sol CHANGED
@@ -16,7 +16,7 @@ contract CoinTest is BaseTest {
16
16
  using stdJson for string;
17
17
 
18
18
  function setUp() public override {
19
- super.setUp();
19
+ super.setUpNonForked();
20
20
  }
21
21
 
22
22
  function test_contract_ierc165_support() public {
@@ -107,23 +107,35 @@ contract CoinTest is BaseTest {
107
107
  }
108
108
 
109
109
  function test_burn() public {
110
- _deployV4Coin();
111
- vm.deal(users.buyer, 1 ether);
112
- vm.prank(users.buyer);
113
- _swapSomeCurrencyForCoin(coinV4, address(weth), 1 ether, users.coinRecipient);
110
+ // Deploy a mock ERC20 currency
111
+ MockERC20 mockCurrency = new MockERC20("Mock Currency", "MOCK");
112
+ mockCurrency.mint(users.buyer, 1000 ether);
113
+ // Pool manager needs currency for liquidity operations
114
+ mockCurrency.mint(address(poolManager), 1000000 ether);
115
+
116
+ // Deploy coin with mock currency
117
+ coinV4 = ContentCoin(payable(address(_deployV4Coin(address(mockCurrency), address(0), bytes32(0)))));
114
118
 
115
- uint256 beforeBalance = coinV4.balanceOf(users.coinRecipient);
119
+ // Approve with permit2 and swap
120
+ uint128 swapAmount = 1 ether;
121
+ vm.startPrank(users.buyer);
122
+ UniV4SwapHelper.approveTokenWithPermit2(permit2, address(router), address(mockCurrency), swapAmount, uint48(block.timestamp + 1 days));
123
+ vm.stopPrank();
124
+
125
+ _swapSomeCurrencyForCoin(coinV4, address(mockCurrency), swapAmount, users.buyer);
126
+
127
+ uint256 beforeBalance = coinV4.balanceOf(users.buyer);
116
128
  uint256 beforeTotalSupply = coinV4.totalSupply();
117
129
 
118
130
  uint256 burnAmount = beforeBalance / 2;
119
131
 
120
- vm.prank(users.coinRecipient);
132
+ vm.prank(users.buyer);
121
133
  coinV4.burn(burnAmount);
122
134
 
123
- uint256 afterBalance = coinV4.balanceOf(users.coinRecipient);
135
+ uint256 afterBalance = coinV4.balanceOf(users.buyer);
124
136
  uint256 afterTotalSupply = coinV4.totalSupply();
125
137
 
126
- assertEq(beforeBalance - afterBalance, burnAmount, "coinRecipient coin balance");
138
+ assertEq(beforeBalance - afterBalance, burnAmount, "buyer coin balance");
127
139
  assertEq(beforeTotalSupply - afterTotalSupply, burnAmount, "coin total supply");
128
140
  }
129
141
 
@@ -38,9 +38,8 @@ contract CoinUniV4Test is BaseTest {
38
38
  MockERC20 internal mockERC20B;
39
39
 
40
40
  function setUp() public override {
41
- super.setUpWithBlockNumber(30267794);
41
+ super.setUpNonForked();
42
42
 
43
- quoter = IV4Quoter(V4_QUOTER);
44
43
  mockERC20A = new MockERC20("MockERC20A", "MCKA");
45
44
  mockERC20B = new MockERC20("MockERC20B", "MCKB");
46
45
 
@@ -21,9 +21,7 @@ contract ContentCoinRewardsTest is BaseTest {
21
21
  address internal tradeReferrer;
22
22
 
23
23
  function setUp() public override {
24
- super.setUpWithBlockNumber(30267794);
25
-
26
- deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
24
+ super.setUpNonForked();
27
25
 
28
26
  backingCreatorCoin = CreatorCoin(_deployCreatorCoin());
29
27
 
@@ -13,10 +13,7 @@ contract CreatorCoinTest is BaseTest {
13
13
  CreatorCoin internal creatorCoin;
14
14
 
15
15
  function setUp() public override {
16
- super.setUpWithBlockNumber(30267794);
17
-
18
- deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
19
-
16
+ super.setUpNonForked();
20
17
  _deployCreatorCoin();
21
18
  }
22
19
 
@@ -22,9 +22,7 @@ contract CreatorCoinRewardsTest is BaseTest {
22
22
  address internal tradeReferrer;
23
23
 
24
24
  function setUp() public override {
25
- super.setUpWithBlockNumber(30267794);
26
-
27
- deal(address(zoraToken), address(poolManager), 1_000_000_000e18);
25
+ super.setUpNonForked();
28
26
 
29
27
  // Set up referrer addresses for all tests
30
28
  platformReferrer = makeAddr("platformReferrer");
@@ -8,7 +8,7 @@ import {IZoraFactory} from "../src/interfaces/IZoraFactory.sol";
8
8
 
9
9
  contract FactoryTest is BaseTest {
10
10
  function setUp() public override {
11
- super.setUp();
11
+ super.setUpNonForked();
12
12
  }
13
13
 
14
14
  function test_factory_constructor_and_proxy_setup() public {
@@ -115,8 +115,8 @@ contract FactoryTest is BaseTest {
115
115
 
116
116
  address platformReferrer = users.platformReferrer;
117
117
 
118
- bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(weth));
119
- bytes memory poolConfigForGettingAddress = poolConfigChanged ? CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(0)) : poolConfig;
118
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(address(0));
119
+ bytes memory poolConfigForGettingAddress = poolConfigChanged ? CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA_TOKEN_ADDRESS) : poolConfig;
120
120
 
121
121
  address expectedCoinAddress = factory.coinAddress(msgSender, name, symbol, poolConfigForGettingAddress, platformReferrer, salt);
122
122
 
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
5
5
 
6
6
  contract MultiOwnableTest is BaseTest {
7
7
  function setUp() public override {
8
- super.setUp();
8
+ super.setUpNonForked();
9
9
 
10
10
  _deployV4Coin();
11
11
  }
@@ -135,7 +135,7 @@ contract MultiOwnableTest is BaseTest {
135
135
 
136
136
  function test_revert_init_with_zero_owners() public {
137
137
  address[] memory emptyOwners = new address[](0);
138
- bytes memory poolConfig_ = _generatePoolConfig(address(weth));
138
+ bytes memory poolConfig_ = _generatePoolConfig(address(0));
139
139
  vm.expectRevert(MultiOwnable.OneOwnerRequired.selector);
140
140
  factory.deploy(users.creator, emptyOwners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
141
141
  }
@@ -143,7 +143,7 @@ contract MultiOwnableTest is BaseTest {
143
143
  function test_revert_init_with_zero_address() public {
144
144
  address[] memory owners = new address[](1);
145
145
  owners[0] = address(0);
146
- bytes memory poolConfig_ = _generatePoolConfig(address(weth));
146
+ bytes memory poolConfig_ = _generatePoolConfig(address(0));
147
147
  vm.expectRevert(MultiOwnable.OwnerCannotBeAddressZero.selector);
148
148
  factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
149
149
  }
@@ -152,7 +152,7 @@ contract MultiOwnableTest is BaseTest {
152
152
  address[] memory owners = new address[](2);
153
153
  owners[0] = users.creator;
154
154
  owners[1] = users.creator;
155
- bytes memory poolConfig_ = _generatePoolConfig(address(weth));
155
+ bytes memory poolConfig_ = _generatePoolConfig(address(0));
156
156
  vm.expectRevert(MultiOwnable.AlreadyOwner.selector);
157
157
  factory.deploy(users.creator, owners, "https://test.com", "Test Token", "TEST", poolConfig_, users.platformReferrer, 0);
158
158
  }