@zoralabs/coins 2.1.2 → 2.3.0

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/.turbo/turbo-build$colon$js.log +152 -0
  2. package/CHANGELOG.md +93 -0
  3. package/README.md +4 -0
  4. package/abis/BaseCoin.json +26 -5
  5. package/abis/BaseTest.json +2 -7
  6. package/abis/ContentCoin.json +26 -5
  7. package/abis/CreatorCoin.json +30 -9
  8. package/abis/FeeEstimatorHook.json +94 -6
  9. package/abis/ICoin.json +26 -0
  10. package/abis/ICoinV3.json +26 -0
  11. package/abis/ICreatorCoin.json +39 -0
  12. package/abis/IERC721.json +36 -36
  13. package/abis/IHasCoinType.json +15 -0
  14. package/abis/IHasTotalSupplyForPositions.json +15 -0
  15. package/abis/{LiquidityMigrationReceiver.json → IUpgradeableDestinationV4HookWithUpdateableFee.json} +10 -18
  16. package/abis/IZoraFactory.json +121 -0
  17. package/abis/IZoraHookRegistry.json +188 -0
  18. package/abis/VmContractHelper226.json +233 -0
  19. package/abis/ZoraFactoryImpl.json +101 -6
  20. package/abis/ZoraHookRegistry.json +375 -0
  21. package/abis/{CreatorCoinHook.json → ZoraV4CoinHook.json} +95 -2
  22. package/addresses/8453.json +6 -5
  23. package/audits/report-cantinacode-zora-0827.pdf +3498 -4
  24. package/dist/index.cjs +93 -13
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +93 -13
  27. package/dist/index.js.map +1 -1
  28. package/dist/wagmiGenerated.d.ts +144 -22
  29. package/dist/wagmiGenerated.d.ts.map +1 -1
  30. package/foundry.toml +4 -1
  31. package/package/wagmiGenerated.ts +93 -13
  32. package/package.json +6 -4
  33. package/script/PrintRegisterUpgradePath.s.sol +0 -7
  34. package/script/TestBackingCoinSwap.s.sol +0 -3
  35. package/script/TestV4Swap.s.sol +0 -3
  36. package/script/UpgradeFactoryImpl.s.sol +1 -1
  37. package/src/BaseCoin.sol +19 -24
  38. package/src/ContentCoin.sol +11 -2
  39. package/src/CreatorCoin.sol +34 -15
  40. package/src/ZoraFactoryImpl.sol +163 -92
  41. package/src/deployment/CoinsDeployerBase.sol +24 -58
  42. package/src/hook-registry/ZoraHookRegistry.sol +97 -0
  43. package/src/hooks/{BaseZoraV4CoinHook.sol → ZoraV4CoinHook.sol} +77 -15
  44. package/src/interfaces/ICoin.sol +19 -1
  45. package/src/interfaces/ICreatorCoin.sol +4 -0
  46. package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
  47. package/src/interfaces/IZoraFactory.sol +51 -10
  48. package/src/interfaces/IZoraHookRegistry.sol +47 -0
  49. package/src/libs/CoinConstants.sol +43 -32
  50. package/src/libs/CoinDopplerMultiCurve.sol +11 -11
  51. package/src/libs/CoinRewardsV4.sol +68 -37
  52. package/src/libs/CoinSetup.sol +2 -9
  53. package/src/libs/DopplerMath.sol +2 -2
  54. package/src/libs/HooksDeployment.sol +13 -65
  55. package/src/libs/V4Liquidity.sol +109 -15
  56. package/src/version/ContractVersionBase.sol +1 -1
  57. package/test/Coin.t.sol +5 -5
  58. package/test/CoinRewardsV4.t.sol +33 -0
  59. package/test/CoinUniV4.t.sol +32 -30
  60. package/test/ContentCoinRewards.t.sol +363 -0
  61. package/test/CreatorCoin.t.sol +53 -29
  62. package/test/CreatorCoinRewards.t.sol +375 -0
  63. package/test/DeploymentHooks.t.sol +64 -12
  64. package/test/Factory.t.sol +24 -7
  65. package/test/HooksDeployment.t.sol +4 -4
  66. package/test/LiquidityMigration.t.sol +149 -16
  67. package/test/Upgrades.t.sol +44 -48
  68. package/test/V4Liquidity.t.sol +178 -0
  69. package/test/ZoraHookRegistry.t.sol +266 -0
  70. package/test/utils/BaseTest.sol +25 -43
  71. package/test/utils/FeeEstimatorHook.sol +4 -6
  72. package/test/utils/RewardTestHelpers.sol +106 -0
  73. package/.turbo/turbo-build.log +0 -199
  74. package/abis/AutoSwapperTest.json +0 -618
  75. package/abis/BadImpl.json +0 -15
  76. package/abis/BaseZoraV4CoinHook.json +0 -1664
  77. package/abis/CoinConstants.json +0 -158
  78. package/abis/CoinRewardsV4.json +0 -67
  79. package/abis/CoinTest.json +0 -819
  80. package/abis/CoinUniV4Test.json +0 -1128
  81. package/abis/ContentCoinHook.json +0 -1733
  82. package/abis/CreatorCoinTest.json +0 -887
  83. package/abis/Deploy.json +0 -9
  84. package/abis/DeployHooks.json +0 -9
  85. package/abis/DeployScript.json +0 -35
  86. package/abis/DeployedCoinVersionLookupTest.json +0 -740
  87. package/abis/DifferentNamespaceVersionLookup.json +0 -39
  88. package/abis/FactoryTest.json +0 -748
  89. package/abis/FakeHookNoInterface.json +0 -21
  90. package/abis/GenerateDeterministicParams.json +0 -9
  91. package/abis/HooksDeploymentTest.json +0 -645
  92. package/abis/HooksTest.json +0 -709
  93. package/abis/InvalidLiquidityMigrationReceiver.json +0 -21
  94. package/abis/LiquidityMigrationTest.json +0 -889
  95. package/abis/MockBadFactory.json +0 -15
  96. package/abis/MultiOwnableTest.json +0 -766
  97. package/abis/PrintUpgradeCommand.json +0 -9
  98. package/abis/TestDeployedCoinVersionLookupImplementation.json +0 -39
  99. package/abis/TestV4Swap.json +0 -9
  100. package/abis/UpgradeFactoryImpl.json +0 -9
  101. package/abis/UpgradeHooks.json +0 -35
  102. package/abis/UpgradesTest.json +0 -723
  103. package/src/hooks/ContentCoinHook.sol +0 -27
  104. package/src/hooks/CreatorCoinHook.sol +0 -27
  105. package/src/libs/CreatorCoinConstants.sol +0 -16
  106. package/src/libs/CreatorCoinRewards.sol +0 -34
  107. package/src/libs/MarketConstants.sol +0 -15
@@ -17,7 +17,13 @@ import {ICoin} from "../src/interfaces/ICoin.sol";
17
17
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
18
18
  import {CoinCommon} from "../src/libs/CoinCommon.sol";
19
19
  import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
20
+ import {MultiOwnable} from "../src/utils/MultiOwnable.sol";
20
21
  import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol";
22
+ import {BaseCoin} from "../src/BaseCoin.sol";
23
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
24
+ import {SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
25
+ import {BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
26
+ import {BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
21
27
 
22
28
  contract LiquidityMigrationReceiver is IUpgradeableDestinationV4Hook, IERC165 {
23
29
  function initializeFromMigration(
@@ -45,6 +51,8 @@ contract InvalidLiquidityMigrationReceiver is IERC165 {
45
51
  contract LiquidityMigrationTest is BaseTest {
46
52
  MockERC20 internal mockERC20A;
47
53
 
54
+ address constant coinVersionLookup = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
55
+
48
56
  function setUp() public override {
49
57
  super.setUpWithBlockNumber(30267794);
50
58
 
@@ -77,7 +85,7 @@ contract LiquidityMigrationTest is BaseTest {
77
85
  address[] memory trustedMessageSenders = new address[](1);
78
86
  trustedMessageSenders[0] = UNIVERSAL_ROUTER;
79
87
 
80
- address originalHook = address(contentCoinHook);
88
+ address originalHook = address(hook);
81
89
 
82
90
  address newHook = address(new LiquidityMigrationReceiver());
83
91
 
@@ -109,7 +117,7 @@ contract LiquidityMigrationTest is BaseTest {
109
117
  assertEq(coinV4.balanceOf(address(originalHook)), originalHookCoinBalanceBefore, "original coin balance");
110
118
 
111
119
  // validate that the existing hook has no liquidity for its positions
112
- LpPosition[] memory positions = contentCoinHook.getPoolCoin(poolKey).positions;
120
+ LpPosition[] memory positions = hook.getPoolCoin(poolKey).positions;
113
121
 
114
122
  for (uint256 i = 0; i < positions.length; i++) {
115
123
  uint128 liquidity = V4Liquidity.getLiquidity(poolManager, address(originalHook), poolKey, positions[i].tickLower, positions[i].tickUpper);
@@ -125,31 +133,59 @@ contract LiquidityMigrationTest is BaseTest {
125
133
  assertEq(newPoolKey.tickSpacing, poolKey.tickSpacing, "poolkey tickSpacing");
126
134
  }
127
135
 
128
- function test_migrateLiquidity_enablesSwapsOnOldPoolKey() public {
136
+ function test_migrateLiquidity_revertsSwapsOnOldPoolKey() public {
129
137
  address currency = address(mockERC20A);
130
138
  mockERC20A.mint(address(poolManager), 1_000_000_000 ether);
131
139
  _deployV4Coin(currency);
132
140
 
133
141
  address trader = makeAddr("trader");
134
-
135
142
  mockERC20A.mint(trader, 10 ether);
136
143
 
137
- // do some swaps
144
+ // do some swaps before migration
138
145
  _swapSomeCurrencyForCoin(coinV4, currency, 1 ether, trader);
139
146
  _swapSomeCoinForCurrency(coinV4, currency, uint128(coinV4.balanceOf(trader)), trader);
140
147
 
141
- address newHook = address(new LiquidityMigrationReceiver());
142
-
143
148
  PoolKey memory poolKey = coinV4.getPoolKey();
149
+ bytes32 poolKeyHash = CoinCommon.hashPoolKey(poolKey);
150
+
151
+ // Verify the mapping exists before migration
152
+ IZoraV4CoinHook.PoolCoin memory poolCoinBefore = hook.getPoolCoin(poolKey);
153
+ assertEq(poolCoinBefore.coin, address(coinV4), "pool coin should exist before migration");
154
+
155
+ IZoraV4CoinHook.PoolCoin memory poolCoinByHashBefore = hook.getPoolCoinByHash(poolKeyHash);
156
+ assertEq(poolCoinByHashBefore.coin, address(coinV4), "pool coin by hash should exist before migration");
144
157
 
158
+ address newHook = address(new LiquidityMigrationReceiver());
145
159
  registerUpgradePath(address(poolKey.hooks), address(newHook));
146
160
 
147
161
  // migrate the liquidity
148
162
  vm.prank(users.creator);
149
163
  coinV4.migrateLiquidity(address(newHook), "");
150
164
 
151
- // now swap using the existing pool key, it should succeed
152
- _swapSomeCurrencyForCoin(poolKey, coinV4, currency, uint128(mockERC20A.balanceOf(trader)), trader);
165
+ // Verify that the old pool key mapping has been deleted
166
+ IZoraV4CoinHook.PoolCoin memory poolCoinAfter = hook.getPoolCoin(poolKey);
167
+ assertEq(poolCoinAfter.coin, address(0), "old pool key should have no associated coin after migration");
168
+ assertEq(poolCoinAfter.positions.length, 0, "old pool coin positions should be empty after migration");
169
+
170
+ IZoraV4CoinHook.PoolCoin memory poolCoinByHashAfter = hook.getPoolCoinByHash(poolKeyHash);
171
+ assertEq(poolCoinByHashAfter.coin, address(0), "old pool coin by hash should have no associated coin after migration");
172
+ assertEq(poolCoinByHashAfter.positions.length, 0, "old pool coin by hash positions should be empty after migration");
173
+
174
+ // Verify that hook operations revert with NoCoinForHook on the old poolkey
175
+ vm.expectRevert(abi.encodeWithSelector(IZoraV4CoinHook.NoCoinForHook.selector, poolKey));
176
+
177
+ SwapParams memory mockSwapParams = SwapParams({zeroForOne: true, amountSpecified: 1 ether, sqrtPriceLimitX96: 0});
178
+
179
+ BalanceDelta mockDelta = BalanceDeltaLibrary.ZERO_DELTA;
180
+
181
+ // Call afterSwap directly - this should revert with NoCoinForHook
182
+ vm.prank(address(poolManager));
183
+ IHooks(address(hook)).afterSwap(trader, poolKey, mockSwapParams, mockDelta, "");
184
+
185
+ // Verify the new pool key still works
186
+ PoolKey memory newPoolKey = coinV4.getPoolKey();
187
+ assertEq(address(newPoolKey.hooks), address(newHook), "coin should have updated to new hook");
188
+ assertTrue(address(poolKey.hooks) != address(newPoolKey.hooks), "old and new pool keys should be different");
153
189
  }
154
190
 
155
191
  function test_migrateLiquidity_emitsLiquidityMigrated() public {
@@ -185,8 +221,6 @@ contract LiquidityMigrationTest is BaseTest {
185
221
  address currency = address(mockERC20A);
186
222
  _deployV4Coin(currency);
187
223
 
188
- address originalHook = address(contentCoinHook);
189
-
190
224
  address invalidNewHook = address(new InvalidLiquidityMigrationReceiver());
191
225
 
192
226
  PoolKey memory poolKey = coinV4.getPoolKey();
@@ -203,14 +237,11 @@ contract LiquidityMigrationTest is BaseTest {
203
237
  address currency = address(mockERC20A);
204
238
  _deployV4Coin(currency);
205
239
 
206
- address originalHook = address(contentCoinHook);
240
+ address originalHook = address(hook);
207
241
 
208
242
  address newHook = address(new LiquidityMigrationReceiver());
209
243
 
210
- PoolKey memory poolKey = coinV4.getPoolKey();
211
-
212
244
  // Note: NOT registering the upgrade path
213
-
214
245
  // expect the migration to revert with UpgradePathNotRegistered error
215
246
  vm.prank(users.creator);
216
247
  vm.expectRevert(abi.encodeWithSelector(IUpgradeableV4Hook.UpgradePathNotRegistered.selector, originalHook, newHook));
@@ -221,7 +252,7 @@ contract LiquidityMigrationTest is BaseTest {
221
252
  address currency = address(mockERC20A);
222
253
  _deployV4Coin(currency);
223
254
 
224
- address originalHook = address(contentCoinHook);
255
+ address originalHook = address(hook);
225
256
  address newHook = address(new LiquidityMigrationReceiver());
226
257
  PoolKey memory poolKey = coinV4.getPoolKey();
227
258
 
@@ -388,4 +419,106 @@ contract LiquidityMigrationTest is BaseTest {
388
419
  // Should match isRegisteredUpgradePath
389
420
  assertEq(hookUpgradeGate.isAllowedHookUpgrade(baseImpl, upgradeImpl), hookUpgradeGate.isRegisteredUpgradePath(baseImpl, upgradeImpl));
390
421
  }
422
+
423
+ function test_migrateLiquidity_failsWithEmptyPositionBug() public {
424
+ // Reproduce the bug discovered in hook version 1.1.2 where migration
425
+ // tries to modify liquidity positions that have zero liquidity
426
+ vm.createSelectFork("base", 35671635);
427
+
428
+ address contentCoin = 0x81f5F30217dA777a5d6441606AFa57E093833d7C;
429
+ address oldHook = 0x9ea932730A7787000042e34390B8E435dD839040; // v1.1.2 hook
430
+ address newHook = 0xff74Be9D3596eA7a33BB4983DD7906fB34135040; // current hook
431
+ address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // deployed upgrade gate
432
+
433
+ BaseCoin coin = BaseCoin(contentCoin);
434
+
435
+ uint24 oldFee = coin.getPoolKey().fee;
436
+
437
+ // Register upgrade path
438
+ address[] memory baseImpls = new address[](1);
439
+ baseImpls[0] = oldHook;
440
+
441
+ vm.prank(Ownable(upgradeGate).owner());
442
+ IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, newHook);
443
+
444
+ // Get coin owner
445
+ address coinOwner = MultiOwnable(contentCoin).owners()[0];
446
+
447
+ // First, demonstrate the bug exists - this should fail
448
+ vm.prank(coinOwner);
449
+ vm.expectRevert();
450
+ coin.migrateLiquidity(newHook, "");
451
+
452
+ // Now fix the bug by etching fixed hook code onto the old hook address
453
+ bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
454
+
455
+ (IHooks fixedHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
456
+
457
+ // Etch the fixed hook code onto the old hook address
458
+ vm.etch(oldHook, address(fixedHook).code);
459
+
460
+ // Now migration should work
461
+ vm.prank(coinOwner);
462
+ coin.migrateLiquidity(newHook, "");
463
+ }
464
+
465
+ function test_migrateLiquidity_canUseNewFee() public {
466
+ // Reproduce the bug discovered in hook version 1.1.2 where migration
467
+ // tries to modify liquidity positions that have zero liquidity
468
+ vm.createSelectFork("base", 35754730);
469
+
470
+ // jacob creator coin
471
+ BaseCoin coin = BaseCoin(0x9B13358E3a023507E7046c18f508A958cDA75f54);
472
+
473
+ address upgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2; // live upgrade gate
474
+
475
+ uint24 oldFee = coin.getPoolKey().fee;
476
+
477
+ assertEq(oldFee, 30000);
478
+
479
+ // Now fix the bug by etching fixed hook code onto the old hook address
480
+ bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
481
+
482
+ (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
483
+
484
+ // Register upgrade path
485
+ address[] memory baseImpls = new address[](1);
486
+ baseImpls[0] = address(coin.hooks());
487
+
488
+ vm.prank(Ownable(upgradeGate).owner());
489
+ IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, address(newHook));
490
+
491
+ // Get coin owner
492
+ address coinOwner = MultiOwnable(address(coin)).owners()[0];
493
+
494
+ vm.prank(coinOwner);
495
+ coin.migrateLiquidity(address(newHook), "");
496
+
497
+ // fee should still be the same as before, because we didnt have the logic to update the fee in the old coin's hook.
498
+ assertEq(coin.getPoolKey().fee, oldFee);
499
+
500
+ address currencyAddress = address(coin.currency());
501
+
502
+ // now test swapping the migrated liquidity
503
+ address trader = makeAddr("trader");
504
+ deal(currencyAddress, trader, 10 ether);
505
+ _swapSomeCurrencyForCoin(coin, coin.currency(), 1 ether, trader);
506
+
507
+ // now migrate liquidity again, but this time to the same new hook as before
508
+ // since the bug has been fixed in the new hook, we should now be able to get the new fee
509
+ // register the upgrade path for the new hook to itself
510
+ baseImpls[0] = address(newHook);
511
+ vm.prank(Ownable(upgradeGate).owner());
512
+ IHooksUpgradeGate(upgradeGate).registerUpgradePath(baseImpls, address(newHook));
513
+
514
+ // migrate liquidity again to the same new hook as before
515
+ vm.prank(coinOwner);
516
+ coin.migrateLiquidity(address(newHook), "");
517
+
518
+ // the new fee should be the correct current fee
519
+ assertEq(coin.getPoolKey().fee, CoinConstants.LP_FEE_V4);
520
+
521
+ // now test swapping the migrated liquidity - it should work
522
+ _swapSomeCurrencyForCoin(coin, coin.currency(), 1 ether, trader);
523
+ }
391
524
  }
@@ -13,7 +13,7 @@ import {IWETH} from "../src/interfaces/IWETH.sol";
13
13
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
14
14
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
15
15
  import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
16
- import {ContentCoinHook} from "../src/hooks/ContentCoinHook.sol";
16
+ import {ZoraV4CoinHook} from "../src/hooks/ZoraV4CoinHook.sol";
17
17
  import {console} from "forge-std/console.sol";
18
18
  import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
19
19
  import {IHooksUpgradeGate} from "../src/interfaces/IHooksUpgradeGate.sol";
@@ -43,7 +43,7 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
43
43
 
44
44
  factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
45
45
 
46
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(contentCoinHook), address(creatorCoinHook));
46
+ ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
47
47
 
48
48
  vm.prank(factoryProxy.owner());
49
49
  factoryProxy.upgradeToAndCall(address(newImpl), "");
@@ -58,7 +58,7 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
58
58
 
59
59
  factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
60
60
 
61
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(contentCoinHook), address(creatorCoinHook));
61
+ ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
62
62
 
63
63
  vm.prank(factoryProxy.owner());
64
64
  factoryProxy.upgradeToAndCall(address(newImpl), "");
@@ -70,29 +70,25 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
70
70
  factoryProxy.upgradeToAndCall(address(badImpl), "");
71
71
  }
72
72
 
73
- function test_canUpgradeToSameContractName() public {
74
- // this test that we can upgrade to the same contract name, when we have already upgraded to a version that has a contract name
75
- vm.createSelectFork("base", 29675508);
73
+ // This fork test needs to be updated after hook registry + new factory is deployed
74
+ // function test_canUpgradeToSameContractName() public {
75
+ // // this test that we can upgrade to the same contract name, when we have already upgraded to a version that has a contract name
76
+ // vm.createSelectFork("base", 29675508);
76
77
 
77
- factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
78
+ // factoryProxy = ZoraFactoryImpl(0x777777751622c0d3258f214F9DF38E35BF45baF3);
78
79
 
79
- ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(contentCoinHook), address(creatorCoinHook));
80
+ // ZoraFactoryImpl newImpl = new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(hook), address(zoraHookRegistry));
80
81
 
81
- vm.prank(factoryProxy.owner());
82
- factoryProxy.upgradeToAndCall(address(newImpl), "");
82
+ // vm.prank(factoryProxy.owner());
83
+ // factoryProxy.upgradeToAndCall(address(newImpl), "");
83
84
 
84
- ZoraFactoryImpl newImpl2 = new ZoraFactoryImpl(
85
- factoryProxy.coinV4Impl(),
86
- factoryProxy.creatorCoinImpl(),
87
- address(contentCoinHook),
88
- address(creatorCoinHook)
89
- );
85
+ // ZoraFactoryImpl newImpl2 = new ZoraFactoryImpl(factoryProxy.coinV4Impl(), factoryProxy.creatorCoinImpl(), address(hook), address(zoraHookRegistry));
90
86
 
91
- vm.prank(factoryProxy.owner());
92
- factoryProxy.upgradeToAndCall(address(newImpl2), "");
87
+ // vm.prank(factoryProxy.owner());
88
+ // factoryProxy.upgradeToAndCall(address(newImpl2), "");
93
89
 
94
- assertEq(factoryProxy.implementation(), address(newImpl2));
95
- }
90
+ // assertEq(factoryProxy.implementation(), address(newImpl2));
91
+ // }
96
92
 
97
93
  function test_canUpgradeAndSwap() public {
98
94
  vm.createSelectFork("base");
@@ -179,32 +175,6 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
179
175
  vm.stopPrank();
180
176
  }
181
177
 
182
- function test_canCanFixBrokenContentCoinAndSwap() public {
183
- vm.createSelectFork("base", 31835069);
184
-
185
- address trader = 0xf69fEc6d858c77e969509843852178bd24CAd2B6;
186
-
187
- address contentCoin = 0x4E93A01c90f812284F71291a8d1415a904957156;
188
-
189
- address creatorCoin = ICoin(contentCoin).currency();
190
-
191
- uint256 amountIn = IERC20(creatorCoin).balanceOf(trader);
192
-
193
- require(amountIn > 0, "no balance");
194
-
195
- // this swap should revert because the content coin is broken
196
- _swapSomeCurrencyForCoinAndExpectRevert(ICoin(contentCoin), creatorCoin, uint128(amountIn), trader);
197
-
198
- bytes memory creationCode = HooksDeployment.contentCoinCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
199
-
200
- (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
201
-
202
- // etch new hook into the content coin, it shouldn't revert anymore when swapping
203
- vm.etch(address(ICoin(contentCoin).hooks()), address(newHook).code);
204
-
205
- _swapSomeCurrencyForCoin(ICoin(contentCoin), creatorCoin, uint128(amountIn), trader);
206
- }
207
-
208
178
  function test_canUpgradeBrokenContentCoinAndSwap() public {
209
179
  vm.createSelectFork("base", 32613149);
210
180
 
@@ -216,7 +186,7 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
216
186
 
217
187
  uint256 amountIn = 0.000111 ether;
218
188
 
219
- bytes memory creationCode = HooksDeployment.contentCoinCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
189
+ bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
220
190
 
221
191
  (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
222
192
 
@@ -268,7 +238,7 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
268
238
 
269
239
  address existingHook = address(creatorCoin.hooks());
270
240
 
271
- bytes memory creationCode = HooksDeployment.creatorCoinCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
241
+ bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
272
242
 
273
243
  (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
274
244
 
@@ -319,4 +289,30 @@ contract UpgradesTest is BaseTest, CoinsDeployerBase {
319
289
  // now try to swap some currency for the creator coin - it should succeed
320
290
  _swapSomeCurrencyForCoin(creatorCoin, zora, uint128(IERC20(zora).balanceOf(trader) / 2), trader);
321
291
  }
292
+
293
+ function test_canFixBrokenContentCoinAndSwap() public {
294
+ vm.createSelectFork("base", 31835069);
295
+
296
+ address trader = 0xf69fEc6d858c77e969509843852178bd24CAd2B6;
297
+
298
+ address contentCoin = 0x4E93A01c90f812284F71291a8d1415a904957156;
299
+
300
+ address creatorCoin = ICoin(contentCoin).currency();
301
+
302
+ uint256 amountIn = IERC20(creatorCoin).balanceOf(trader);
303
+
304
+ require(amountIn > 0, "no balance");
305
+
306
+ // this swap should revert because the content coin is broken
307
+ _swapSomeCurrencyForCoinAndExpectRevert(ICoin(contentCoin), creatorCoin, uint128(amountIn), trader);
308
+
309
+ bytes memory creationCode = HooksDeployment.makeHookCreationCode(address(poolManager), coinVersionLookup, new address[](0), upgradeGate);
310
+
311
+ (IHooks newHook, ) = HooksDeployment.deployHookWithExistingOrNewSalt(address(this), creationCode, bytes32(0));
312
+
313
+ // etch new hook into the content coin, it shouldn't revert anymore when swapping
314
+ vm.etch(address(ICoin(contentCoin).hooks()), address(newHook).code);
315
+
316
+ _swapSomeCurrencyForCoin(ICoin(contentCoin), creatorCoin, uint128(amountIn), trader);
317
+ }
322
318
  }
@@ -0,0 +1,178 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "./utils/BaseTest.sol";
5
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
6
+ import {CoinDopplerMultiCurve} from "../src/libs/CoinDopplerMultiCurve.sol";
7
+ import {V4Liquidity} from "../src/libs/V4Liquidity.sol";
8
+ import {LpPosition} from "../src/types/LpPosition.sol";
9
+ import {PoolConfiguration} from "../src/interfaces/ICoin.sol";
10
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
11
+ import {MockERC20} from "./mocks/MockERC20.sol";
12
+ import {ContentCoin} from "../src/ContentCoin.sol";
13
+ import {ZoraV4CoinHook} from "../src/hooks/ZoraV4CoinHook.sol";
14
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
15
+
16
+ contract V4LiquidityTest is BaseTest {
17
+ MockERC20 internal mockERC20A;
18
+
19
+ function setUp() public override {
20
+ super.setUp();
21
+ mockERC20A = new MockERC20("MockERC20A", "MCKA");
22
+ }
23
+
24
+ function _poolConfigWithDuplicatePositions(address currency) private pure returns (bytes memory poolConfig) {
25
+ // Create configuration that will produce duplicate positions
26
+ int24[] memory tickLower_ = new int24[](2);
27
+ tickLower_[0] = -54000;
28
+ tickLower_[1] = -54000; // Same as first curve
29
+
30
+ int24[] memory tickUpper_ = new int24[](2);
31
+ tickUpper_[0] = 7000;
32
+ tickUpper_[1] = 7000; // Same as first curve
33
+
34
+ uint16[] memory numDiscoveryPositions_ = new uint16[](2);
35
+ numDiscoveryPositions_[0] = 5;
36
+ numDiscoveryPositions_[1] = 5;
37
+
38
+ uint256[] memory maxDiscoverySupplyShare_ = new uint256[](2);
39
+ maxDiscoverySupplyShare_[0] = 100000000000000000; // 0.1e18
40
+ maxDiscoverySupplyShare_[1] = 100000000000000000; // 0.1e18
41
+
42
+ poolConfig = CoinConfigurationVersions.encodeDopplerMultiCurveUniV4(currency, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_);
43
+ }
44
+
45
+ function _countDuplicatePositions(LpPosition[] memory positions) private pure returns (uint256 duplicateCount) {
46
+ for (uint256 i = 0; i < positions.length; i++) {
47
+ for (uint256 j = i + 1; j < positions.length; j++) {
48
+ if (positions[i].tickLower == positions[j].tickLower && positions[i].tickUpper == positions[j].tickUpper) {
49
+ duplicateCount++;
50
+ break; // Only count each unique duplicate once
51
+ }
52
+ }
53
+ }
54
+ }
55
+
56
+ function test_calculatePositionsWithDuplicateConfigCreatesDuplicates() public view {
57
+ address currency = address(mockERC20A);
58
+ bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
59
+
60
+ (, PoolConfiguration memory poolConfiguration) = CoinDopplerMultiCurve.setupPool(true, poolConfig);
61
+
62
+ LpPosition[] memory positions = CoinDopplerMultiCurve.calculatePositions(
63
+ true, // isCoinToken0
64
+ poolConfiguration,
65
+ CoinConstants.CONTENT_COIN_MARKET_SUPPLY
66
+ );
67
+
68
+ uint256 duplicateCount = _countDuplicatePositions(positions);
69
+ assertGt(duplicateCount, 0, "Should have duplicate positions");
70
+ }
71
+
72
+ function test_dedupePositionsMergesDuplicates() public view {
73
+ address currency = address(mockERC20A);
74
+ bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
75
+
76
+ (, PoolConfiguration memory poolConfiguration) = CoinDopplerMultiCurve.setupPool(true, poolConfig);
77
+
78
+ LpPosition[] memory originalPositions = CoinDopplerMultiCurve.calculatePositions(
79
+ true, // isCoinToken0
80
+ poolConfiguration,
81
+ CoinConstants.CONTENT_COIN_MARKET_SUPPLY
82
+ );
83
+
84
+ uint256 originalDuplicateCount = _countDuplicatePositions(originalPositions);
85
+ assertGt(originalDuplicateCount, 0, "Should have duplicate positions to test deduplication");
86
+
87
+ // Deduplicate the positions
88
+ LpPosition[] memory dedupedPositions = V4Liquidity.dedupePositions(originalPositions);
89
+
90
+ // Verify no duplicates exist in deduped array
91
+ uint256 dedupedDuplicateCount = _countDuplicatePositions(dedupedPositions);
92
+ assertEq(dedupedDuplicateCount, 0, "Should have no duplicates after deduplication");
93
+
94
+ // Verify that array is smaller after deduplication
95
+ assertLt(dedupedPositions.length, originalPositions.length, "Deduped array should be smaller");
96
+
97
+ // Calculate total liquidity before and after deduplication
98
+ uint256 totalOriginalLiquidity = 0;
99
+ uint256 totalDedupedLiquidity = 0;
100
+
101
+ for (uint256 i = 0; i < originalPositions.length; i++) {
102
+ totalOriginalLiquidity += originalPositions[i].liquidity;
103
+ }
104
+
105
+ for (uint256 i = 0; i < dedupedPositions.length; i++) {
106
+ totalDedupedLiquidity += dedupedPositions[i].liquidity;
107
+ }
108
+
109
+ assertEq(totalOriginalLiquidity, totalDedupedLiquidity, "Total liquidity should be preserved");
110
+ }
111
+
112
+ function test_memoryStructModification() public pure {
113
+ // this test shows that we can modify a struct in memory and it will be reflected in the array
114
+ LpPosition[] memory positions = new LpPosition[](2);
115
+ positions[0] = LpPosition({tickLower: -100, tickUpper: 100, liquidity: 1000});
116
+ positions[1] = LpPosition({tickLower: -200, tickUpper: 200, liquidity: 2000});
117
+
118
+ LpPosition memory pos = positions[0];
119
+ pos.liquidity += 500;
120
+ pos = positions[1];
121
+ pos.liquidity = 3000;
122
+
123
+ // The array element should be modified
124
+ assertEq(positions[0].liquidity, 1500, "Array element should change when modifying copy");
125
+ assertEq(positions[1].liquidity, 3000, "Array element should change when modifying copy");
126
+ }
127
+
128
+ function test_mstoreArrayLength() public pure {
129
+ LpPosition[] memory positions = new LpPosition[](5);
130
+ positions[0] = LpPosition({tickLower: -100, tickUpper: 100, liquidity: 1000});
131
+ positions[1] = LpPosition({tickLower: -200, tickUpper: 200, liquidity: 2000});
132
+ positions[2] = LpPosition({tickLower: -300, tickUpper: 300, liquidity: 3000});
133
+ positions[3] = LpPosition({tickLower: -400, tickUpper: 400, liquidity: 4000});
134
+ positions[4] = LpPosition({tickLower: -500, tickUpper: 500, liquidity: 5000});
135
+
136
+ assertEq(positions.length, 5, "Initial length should be 5");
137
+
138
+ assembly {
139
+ mstore(positions, 2)
140
+ }
141
+
142
+ assertEq(positions.length, 2, "Length should be 2 after mstore");
143
+ assertEq(positions[0].liquidity, 1000, "First element should be preserved");
144
+ assertEq(positions[1].liquidity, 2000, "Second element should be preserved");
145
+ }
146
+
147
+ function test_deployedCoinWithDuplicateConfigHasNoDuplicatePositions() public {
148
+ address currency = address(mockERC20A);
149
+
150
+ address[] memory owners = new address[](1);
151
+ owners[0] = users.creator;
152
+
153
+ bytes memory poolConfig = _poolConfigWithDuplicatePositions(currency);
154
+
155
+ (address coinAddress, ) = factory.deploy(
156
+ users.creator,
157
+ owners,
158
+ "https://test.com",
159
+ DEFAULT_NAME,
160
+ DEFAULT_SYMBOL,
161
+ poolConfig,
162
+ address(0),
163
+ address(0),
164
+ bytes(""),
165
+ bytes32(0)
166
+ );
167
+
168
+ ContentCoin coinV4 = ContentCoin(payable(coinAddress));
169
+
170
+ // get hooks
171
+ PoolKey memory poolKey = coinV4.getPoolKey();
172
+ LpPosition[] memory positions = ZoraV4CoinHook(payable(address(coinV4.hooks()))).getPoolCoin(poolKey).positions;
173
+
174
+ // Verify no duplicate positions exist in the deployed coin (deduplication worked during deployment)
175
+ uint256 duplicateCount = _countDuplicatePositions(positions);
176
+ assertEq(duplicateCount, 0, "Should have no duplicates after deployment");
177
+ }
178
+ }