@zoralabs/coins 0.7.1 → 0.9.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 (104) hide show
  1. package/.env +1 -0
  2. package/.turbo/turbo-build.log +114 -109
  3. package/.turbo/turbo-update-contract-version.log +22 -0
  4. package/CHANGELOG.md +28 -0
  5. package/abis/BadImpl.json +15 -0
  6. package/abis/BalanceDeltaLibrary.json +15 -0
  7. package/abis/BaseCoin.json +1442 -0
  8. package/abis/BaseCoinDeployHook.json +78 -0
  9. package/abis/BaseHook.json +897 -0
  10. package/abis/BaseTest.json +13 -91
  11. package/abis/BeforeSwapDeltaLibrary.json +15 -0
  12. package/abis/BuySupplyWithSwapRouterHook.json +126 -0
  13. package/abis/Coin.json +48 -92
  14. package/abis/CoinConstants.json +65 -0
  15. package/abis/CoinTest.json +13 -91
  16. package/abis/CoinV4.json +1664 -0
  17. package/abis/CurrencyLibrary.json +25 -0
  18. package/abis/DeployHooks.json +9 -0
  19. package/abis/DopplerUniswapV3Test.json +13 -91
  20. package/abis/FactoryTest.json +13 -91
  21. package/abis/FakeHookNoInterface.json +21 -0
  22. package/abis/HookDeployer.json +68 -0
  23. package/abis/Hooks.json +28 -0
  24. package/abis/HooksTest.json +651 -0
  25. package/abis/IAllowanceTransfer.json +486 -0
  26. package/abis/ICoin.json +25 -1
  27. package/abis/ICoinDeployHook.json +31 -0
  28. package/abis/IContractMetadata.json +28 -0
  29. package/abis/IEIP712.json +15 -0
  30. package/abis/IEIP712_v4.json +15 -0
  31. package/abis/IERC20Minimal.json +172 -0
  32. package/abis/IERC6909Claims.json +288 -0
  33. package/abis/IERC721Permit_v4.json +88 -0
  34. package/abis/IExtsload.json +64 -0
  35. package/abis/IExttload.json +40 -0
  36. package/abis/IHasAfterCoinDeploy.json +31 -0
  37. package/abis/IHasContractName.json +15 -0
  38. package/abis/IHookDeployer.json +42 -0
  39. package/abis/IHooks.json +789 -0
  40. package/abis/IImmutableState.json +15 -0
  41. package/abis/IMulticall_v4.json +21 -0
  42. package/abis/INotifier.json +187 -0
  43. package/abis/IPermit2.json +865 -0
  44. package/abis/IPermit2Forwarder.json +138 -0
  45. package/abis/IPoolInitializer_v4.json +53 -0
  46. package/abis/IPoolManager.json +1286 -0
  47. package/abis/IPositionManager.json +712 -0
  48. package/abis/IProtocolFees.json +174 -0
  49. package/abis/ISignatureTransfer.json +394 -0
  50. package/abis/ISubscriber.json +89 -0
  51. package/abis/ISwapRouter.json +82 -0
  52. package/abis/IUnorderedNonce.json +44 -0
  53. package/abis/IV4Router.json +47 -0
  54. package/abis/IZoraFactory.json +144 -0
  55. package/abis/IZoraV4CoinHook.json +83 -0
  56. package/abis/ImmutableState.json +36 -0
  57. package/abis/LPFeeLibrary.json +65 -0
  58. package/abis/MultiOwnableTest.json +13 -91
  59. package/abis/Simulate.json +0 -91
  60. package/abis/UniV3BuySell.json +12 -0
  61. package/abis/UniV3Errors.json +32 -0
  62. package/abis/UpgradeFactoryImpl.json +9 -0
  63. package/abis/UpgradesTest.json +604 -0
  64. package/abis/ZoraFactoryImpl.json +111 -0
  65. package/abis/ZoraV4CoinHook.json +989 -0
  66. package/addresses/8453.json +2 -1
  67. package/addresses/84532.json +4 -3
  68. package/dist/index.cjs +125 -62
  69. package/dist/index.cjs.map +1 -1
  70. package/dist/index.js +120 -56
  71. package/dist/index.js.map +1 -1
  72. package/dist/wagmiGenerated.d.ts +212 -464
  73. package/dist/wagmiGenerated.d.ts.map +1 -1
  74. package/package/wagmiGenerated.ts +122 -66
  75. package/package.json +4 -4
  76. package/script/CoinsDeployerBase.sol +32 -0
  77. package/script/DeployHooks.s.sol +22 -0
  78. package/script/Simulate.s.sol +3 -2
  79. package/script/UpgradeCoinImpl.sol +2 -2
  80. package/script/UpgradeFactoryImpl.s.sol +23 -0
  81. package/src/Coin.sol +35 -342
  82. package/src/ZoraFactoryImpl.sol +73 -45
  83. package/src/hooks/BaseCoinDeployHook.sol +62 -0
  84. package/src/hooks/BuySupplyWithSwapRouterHook.sol +78 -0
  85. package/src/interfaces/ICoin.sol +5 -1
  86. package/src/interfaces/ICoinDeployHook.sol +8 -0
  87. package/src/interfaces/ISwapRouter.sol +1 -35
  88. package/src/interfaces/IZoraFactory.sol +52 -0
  89. package/src/{utils → libs}/CoinConstants.sol +6 -6
  90. package/src/libs/CoinLegacy.sol +4 -4
  91. package/src/libs/CoinLegacyMarket.sol +182 -0
  92. package/src/libs/CoinSetupV3.sol +111 -0
  93. package/src/libs/UniV3BuySell.sol +449 -0
  94. package/src/libs/UniV3Errors.sol +11 -0
  95. package/src/version/ContractVersionBase.sol +1 -1
  96. package/test/Coin.t.sol +20 -17
  97. package/test/CoinDopplerUniV3.t.sol +7 -17
  98. package/test/Factory.t.sol +4 -3
  99. package/test/Hooks.t.sol +274 -0
  100. package/test/Upgrades.t.sol +67 -0
  101. package/test/utils/BaseTest.sol +18 -3
  102. package/wagmi.config.ts +6 -9
  103. package/src/libs/CoinSetup.sol +0 -37
  104. /package/abis/{CoinSetup.json → CoinSetupV3.json} +0 -0
package/src/Coin.sol CHANGED
@@ -18,15 +18,16 @@ import {ERC20PermitUpgradeable} from "@openzeppelin/contracts-upgradeable/token/
18
18
  import {ReentrancyGuardUpgradeable} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol";
19
19
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
20
20
  import {ContractVersionBase} from "./version/ContractVersionBase.sol";
21
- import {CoinConstants} from "./utils/CoinConstants.sol";
22
21
  import {MultiOwnable} from "./utils/MultiOwnable.sol";
23
22
  import {FullMath} from "./utils/uniswap/FullMath.sol";
24
23
  import {TickMath} from "./utils/uniswap/TickMath.sol";
25
24
  import {LiquidityAmounts} from "./utils/uniswap/LiquidityAmounts.sol";
26
- import {CoinSetup} from "./libs/CoinSetup.sol";
25
+ import {CoinConstants} from "./libs/CoinConstants.sol";
27
26
  import {MarketConstants} from "./libs/MarketConstants.sol";
28
27
  import {LpPosition} from "./types/LpPosition.sol";
29
28
  import {PoolState} from "./types/PoolState.sol";
29
+ import {CoinSetupV3, UniV3Config, CoinV3Config} from "./libs/CoinSetupV3.sol";
30
+ import {UniV3BuySell, CoinConfig} from "./libs/UniV3BuySell.sol";
30
31
 
31
32
  /*
32
33
  $$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\
@@ -38,7 +39,7 @@ import {PoolState} from "./types/PoolState.sol";
38
39
  \$$$$$$ | $$$$$$ |$$$$$$\ $$ | \$$ |
39
40
  \______/ \______/ \______|\__| \__|
40
41
  */
41
- contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ReentrancyGuardUpgradeable {
42
+ contract Coin is ICoin, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ReentrancyGuardUpgradeable {
42
43
  using SafeERC20 for IERC20;
43
44
 
44
45
  /// @notice The address of the WETH contract
@@ -65,6 +66,11 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
65
66
  /// @notice The address of the currency
66
67
  address public currency;
67
68
 
69
+ /// @notice The state of the market
70
+ bytes public market;
71
+ uint8 public marketVersion;
72
+
73
+ /// @notice deprecated
68
74
  PoolConfiguration public poolConfiguration;
69
75
 
70
76
  /// @notice Returns the state of the pool
@@ -101,7 +107,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
101
107
  isInitialized = true;
102
108
  isExited = false;
103
109
  maxShareToBeSold = poolConfiguration.maxDiscoverySupplyShare;
104
- totalTokensOnBondingCurve = POOL_LAUNCH_SUPPLY;
110
+ totalTokensOnBondingCurve = CoinConstants.POOL_LAUNCH_SUPPLY;
105
111
  }
106
112
 
107
113
  /**
@@ -183,13 +189,31 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
183
189
  platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
184
190
 
185
191
  // Mint the total supply
186
- _mint(address(this), MAX_TOTAL_SUPPLY);
192
+ _mint(address(this), CoinConstants.MAX_TOTAL_SUPPLY);
187
193
 
188
194
  // Distribute the creator launch reward
189
- _transfer(address(this), payoutRecipient, CREATOR_LAUNCH_REWARD);
195
+ _transfer(address(this), payoutRecipient, CoinConstants.CREATOR_LAUNCH_REWARD);
196
+
197
+ UniV3Config memory uniswapV3Config = UniV3Config({weth: WETH, v3Factory: v3Factory, swapRouter: swapRouter, airlock: airlock});
190
198
 
191
199
  // Deploy the pool
192
- _deployLiquidity(poolConfig_);
200
+ (currency, poolAddress, poolConfiguration) = CoinSetupV3.setupPool(poolConfig_, uniswapV3Config, address(this));
201
+
202
+ // Split out the deployment of liquidity to avoid stack too deep
203
+ CoinSetupV3.deployLiquidity(address(this), currency, poolConfiguration, poolAddress);
204
+ }
205
+
206
+ function buildCoinConfig() internal view returns (CoinConfig memory coinConfig) {
207
+ coinConfig = CoinConfig({
208
+ protocolRewardRecipient: protocolRewardRecipient,
209
+ platformReferrer: platformReferrer,
210
+ currency: currency,
211
+ payoutRecipient: payoutRecipient,
212
+ protocolRewards: protocolRewards,
213
+ poolConfiguration: poolConfiguration,
214
+ poolAddress: poolAddress,
215
+ uniswapV3Config: UniV3Config({weth: WETH, v3Factory: v3Factory, swapRouter: swapRouter, airlock: airlock})
216
+ });
193
217
  }
194
218
 
195
219
  /// @notice Executes a buy order
@@ -204,41 +228,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
204
228
  uint160 sqrtPriceLimitX96,
205
229
  address tradeReferrer
206
230
  ) public payable nonReentrant returns (uint256, uint256) {
207
- // Ensure the recipient is not the zero address
208
- if (recipient == address(0)) {
209
- revert AddressZero();
210
- }
211
-
212
- // Calculate the trade reward
213
- uint256 tradeReward = _calculateReward(orderSize, TOTAL_FEE_BPS);
214
-
215
- // Calculate the remaining size
216
- uint256 trueOrderSize = orderSize - tradeReward;
217
-
218
- // Handle incoming currency
219
- _handleIncomingCurrency(orderSize, trueOrderSize);
220
-
221
- // Set up the swap parameters
222
- ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
223
- tokenIn: currency,
224
- tokenOut: address(this),
225
- fee: MarketConstants.LP_FEE,
226
- recipient: recipient,
227
- amountIn: trueOrderSize,
228
- amountOutMinimum: minAmountOut,
229
- sqrtPriceLimitX96: sqrtPriceLimitX96
230
- });
231
-
232
- // Execute the swap
233
- uint256 amountOut = ISwapRouter(swapRouter).exactInputSingle(params);
234
-
235
- _handleTradeRewards(tradeReward, tradeReferrer);
236
-
237
- _handleMarketRewards();
238
-
239
- emit CoinBuy(msg.sender, recipient, tradeReferrer, amountOut, currency, tradeReward, trueOrderSize);
240
-
241
- return (orderSize, amountOut);
231
+ return UniV3BuySell.buy(recipient, orderSize, minAmountOut, sqrtPriceLimitX96, tradeReferrer, address(this), buildCoinConfig());
242
232
  }
243
233
 
244
234
  /// @notice Executes a sell order
@@ -254,11 +244,6 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
254
244
  uint160 sqrtPriceLimitX96,
255
245
  address tradeReferrer
256
246
  ) public nonReentrant returns (uint256, uint256) {
257
- // Ensure the recipient is not the zero address
258
- if (recipient == address(0)) {
259
- revert AddressZero();
260
- }
261
-
262
247
  // Record the coin balance of this contract before the swap
263
248
  uint256 beforeCoinBalance = balanceOf(address(this));
264
249
 
@@ -268,55 +253,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
268
253
  // Approve the Uniswap V3 swap router
269
254
  this.approve(swapRouter, orderSize);
270
255
 
271
- // Set the swap parameters
272
- ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
273
- tokenIn: address(this),
274
- tokenOut: currency,
275
- fee: MarketConstants.LP_FEE,
276
- recipient: address(this),
277
- amountIn: orderSize,
278
- amountOutMinimum: minAmountOut,
279
- sqrtPriceLimitX96: sqrtPriceLimitX96
280
- });
281
-
282
- // Execute the swap
283
- uint256 amountOut = ISwapRouter(swapRouter).exactInputSingle(params);
284
-
285
- // Record the coin balance of this contract after the swap
286
- uint256 afterCoinBalance = balanceOf(address(this));
287
-
288
- // If the swap was partially executed:
289
- if (afterCoinBalance > beforeCoinBalance) {
290
- // Calculate the refund
291
- uint256 coinRefund = afterCoinBalance - beforeCoinBalance;
292
-
293
- // Update the order size
294
- orderSize -= coinRefund;
295
-
296
- // Transfer the refund back to the seller
297
- _transfer(address(this), recipient, coinRefund);
298
- }
299
-
300
- // If currency is WETH, convert to ETH
301
- if (currency == WETH) {
302
- IWETH(WETH).withdraw(amountOut);
303
- }
304
-
305
- // Calculate the trade reward
306
- uint256 tradeReward = _calculateReward(amountOut, TOTAL_FEE_BPS);
307
-
308
- // Calculate the payout after the fee
309
- uint256 payoutSize = amountOut - tradeReward;
310
-
311
- _handlePayout(payoutSize, recipient);
312
-
313
- _handleTradeRewards(tradeReward, tradeReferrer);
314
-
315
- _handleMarketRewards();
316
-
317
- emit CoinSell(msg.sender, recipient, tradeReferrer, orderSize, currency, tradeReward, payoutSize);
318
-
319
- return (orderSize, payoutSize);
256
+ return UniV3BuySell.sell(recipient, beforeCoinBalance, orderSize, minAmountOut, sqrtPriceLimitX96, tradeReferrer, buildCoinConfig());
320
257
  }
321
258
 
322
259
  /// @notice Enables a user to burn their tokens
@@ -330,7 +267,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
330
267
  /// @dev This function is a fallback, secondary rewards will be claimed automatically on each buy and sell.
331
268
  /// @param pushEthRewards Whether to push the ETH directly to the recipients.
332
269
  function claimSecondaryRewards(bool pushEthRewards) external nonReentrant {
333
- MarketRewards memory rewards = _handleMarketRewards();
270
+ MarketRewards memory rewards = UniV3BuySell.handleMarketRewards(address(this), buildCoinConfig());
334
271
 
335
272
  if (pushEthRewards && rewards.totalAmountCurrency > 0 && currency == WETH) {
336
273
  IProtocolRewards(protocolRewards).withdrawFor(payoutRecipient, rewards.creatorPayoutAmountCurrency);
@@ -373,7 +310,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
373
310
 
374
311
  /// @dev Called by the pool after minting liquidity to transfer the associated coins
375
312
  function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
376
- if (msg.sender != poolAddress) revert OnlyPool();
313
+ if (msg.sender != poolAddress) revert OnlyPool(msg.sender, poolAddress);
377
314
 
378
315
  IERC20(address(this)).safeTransfer(poolAddress, amount0Owed == 0 ? amount1Owed : amount0Owed);
379
316
  }
@@ -405,248 +342,4 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
405
342
 
406
343
  tokenURI = newURI;
407
344
  }
408
-
409
- /// @dev Deploys the Uniswap V3 pool and mints initial liquidity based on the pool configuration
410
- function _deployLiquidity(bytes memory poolConfig_) internal {
411
- (uint8 version, address currency_) = abi.decode(poolConfig_, (uint8, address));
412
-
413
- // Store the currency, defaulting to WETH if address(0)
414
- currency = currency_ == address(0) ? WETH : currency_;
415
-
416
- // Sort the token addresses
417
- address token0 = address(this) < currency ? address(this) : currency;
418
- address token1 = address(this) < currency ? currency : address(this);
419
- bool isCoinToken0 = token0 == address(this);
420
-
421
- (uint160 sqrtPriceX96, PoolConfiguration memory _poolConfig) = CoinSetup.setupPoolWithVersion(version, poolConfig_, isCoinToken0, WETH);
422
-
423
- poolConfiguration = _poolConfig;
424
-
425
- poolAddress = _createPool(token0, token1, sqrtPriceX96);
426
-
427
- LpPosition[] memory positions = CoinSetup.calculatePositions(isCoinToken0, poolConfiguration);
428
-
429
- _mintPositions(positions);
430
- }
431
-
432
- /// @dev Creates the Uniswap V3 pool for the coin/currency pair
433
- function _createPool(address token0, address token1, uint160 sqrtPriceX96) internal returns (address pool) {
434
- pool = IUniswapV3Factory(v3Factory).createPool(token0, token1, MarketConstants.LP_FEE);
435
-
436
- // This pool should be new, if it has already been initialized
437
- // then we will fail the creation step prompting the user to try again.
438
- IUniswapV3Pool(pool).initialize(sqrtPriceX96);
439
- }
440
-
441
- /// @dev Mints the calculated liquidity positions into the Uniswap V3 pool
442
- function _mintPositions(LpPosition[] memory lbpPositions) internal {
443
- for (uint256 i; i < lbpPositions.length; i++) {
444
- IUniswapV3Pool(poolAddress).mint(address(this), lbpPositions[i].tickLower, lbpPositions[i].tickUpper, lbpPositions[i].liquidity, "");
445
- }
446
- }
447
-
448
- /// @dev Handles incoming currency transfers for buy orders; if WETH is the currency the caller has the option to send native-ETH
449
- /// @param orderSize The total size of the order in the currency
450
- /// @param trueOrderSize The actual amount being used for the swap after fees
451
- function _handleIncomingCurrency(uint256 orderSize, uint256 trueOrderSize) internal {
452
- if (currency == WETH && msg.value > 0) {
453
- if (msg.value != orderSize) {
454
- revert EthAmountMismatch();
455
- }
456
-
457
- if (msg.value < MIN_ORDER_SIZE) {
458
- revert EthAmountTooSmall();
459
- }
460
-
461
- IWETH(WETH).deposit{value: trueOrderSize}();
462
- IWETH(WETH).approve(swapRouter, trueOrderSize);
463
- } else {
464
- // Ensure ETH is not sent with a non-ETH pair
465
- if (msg.value != 0) {
466
- revert EthTransferInvalid();
467
- }
468
-
469
- uint256 beforeBalance = IERC20(currency).balanceOf(address(this));
470
- IERC20(currency).safeTransferFrom(msg.sender, address(this), orderSize);
471
- uint256 afterBalance = IERC20(currency).balanceOf(address(this));
472
-
473
- if ((afterBalance - beforeBalance) != orderSize) {
474
- revert ERC20TransferAmountMismatch();
475
- }
476
-
477
- IERC20(currency).approve(swapRouter, trueOrderSize);
478
- }
479
- }
480
-
481
- /// @dev Handles sending ETH and ERC20 payouts and refunds to recipients
482
- /// @param orderPayout The amount of currency to pay out
483
- /// @param recipient The address to receive the payout
484
- function _handlePayout(uint256 orderPayout, address recipient) internal {
485
- if (currency == WETH) {
486
- Address.sendValue(payable(recipient), orderPayout);
487
- } else {
488
- IERC20(currency).safeTransfer(recipient, orderPayout);
489
- }
490
- }
491
-
492
- /// @dev Handles calculating and depositing fees to an escrow protocol rewards contract
493
- function _handleTradeRewards(uint256 totalValue, address _tradeReferrer) internal {
494
- if (_tradeReferrer == address(0)) {
495
- _tradeReferrer = protocolRewardRecipient;
496
- }
497
-
498
- uint256 tokenCreatorFee = _calculateReward(totalValue, TOKEN_CREATOR_FEE_BPS);
499
- uint256 platformReferrerFee = _calculateReward(totalValue, PLATFORM_REFERRER_FEE_BPS);
500
- uint256 tradeReferrerFee = _calculateReward(totalValue, TRADE_REFERRER_FEE_BPS);
501
- uint256 protocolFee = totalValue - tokenCreatorFee - platformReferrerFee - tradeReferrerFee;
502
-
503
- if (currency == WETH) {
504
- address[] memory recipients = new address[](4);
505
- uint256[] memory amounts = new uint256[](4);
506
- bytes4[] memory reasons = new bytes4[](4);
507
-
508
- recipients[0] = payoutRecipient;
509
- amounts[0] = tokenCreatorFee;
510
- reasons[0] = bytes4(keccak256("COIN_CREATOR_REWARD"));
511
-
512
- recipients[1] = platformReferrer;
513
- amounts[1] = platformReferrerFee;
514
- reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_REWARD"));
515
-
516
- recipients[2] = _tradeReferrer;
517
- amounts[2] = tradeReferrerFee;
518
- reasons[2] = bytes4(keccak256("COIN_TRADE_REFERRER_REWARD"));
519
-
520
- recipients[3] = protocolRewardRecipient;
521
- amounts[3] = protocolFee;
522
- reasons[3] = bytes4(keccak256("COIN_PROTOCOL_REWARD"));
523
-
524
- IProtocolRewards(protocolRewards).depositBatch{value: totalValue}(recipients, amounts, reasons, "");
525
- }
526
-
527
- if (currency != WETH) {
528
- IERC20(currency).safeTransfer(payoutRecipient, tokenCreatorFee);
529
- IERC20(currency).safeTransfer(platformReferrer, platformReferrerFee);
530
- IERC20(currency).safeTransfer(_tradeReferrer, tradeReferrerFee);
531
- IERC20(currency).safeTransfer(protocolRewardRecipient, protocolFee);
532
- }
533
-
534
- emit CoinTradeRewards(
535
- payoutRecipient,
536
- platformReferrer,
537
- _tradeReferrer,
538
- protocolRewardRecipient,
539
- tokenCreatorFee,
540
- platformReferrerFee,
541
- tradeReferrerFee,
542
- protocolFee,
543
- currency
544
- );
545
- }
546
-
547
- /// @dev Collects and distributes accrued fees from all LP positions
548
- function _handleMarketRewards() internal returns (MarketRewards memory) {
549
- uint256 totalAmountToken0;
550
- uint256 totalAmountToken1;
551
- uint256 amount0;
552
- uint256 amount1;
553
-
554
- bool isCoinToken0 = address(this) < currency;
555
- LpPosition[] memory positions = CoinSetup.calculatePositions(isCoinToken0, poolConfiguration);
556
-
557
- for (uint256 i; i < positions.length; i++) {
558
- // Must burn to update the collect mapping on the pool
559
- IUniswapV3Pool(poolAddress).burn(positions[i].tickLower, positions[i].tickUpper, 0);
560
-
561
- (amount0, amount1) = IUniswapV3Pool(poolAddress).collect(
562
- address(this),
563
- positions[i].tickLower,
564
- positions[i].tickUpper,
565
- type(uint128).max,
566
- type(uint128).max
567
- );
568
-
569
- totalAmountToken0 += amount0;
570
- totalAmountToken1 += amount1;
571
- }
572
-
573
- address token0 = currency < address(this) ? currency : address(this);
574
- address token1 = currency < address(this) ? address(this) : currency;
575
-
576
- MarketRewards memory rewards;
577
-
578
- rewards = _transferMarketRewards(token0, totalAmountToken0, rewards);
579
- rewards = _transferMarketRewards(token1, totalAmountToken1, rewards);
580
-
581
- emit CoinMarketRewards(payoutRecipient, platformReferrer, protocolRewardRecipient, currency, rewards);
582
-
583
- return rewards;
584
- }
585
-
586
- function _transferMarketRewards(address token, uint256 totalAmount, MarketRewards memory rewards) internal returns (MarketRewards memory) {
587
- if (totalAmount > 0) {
588
- address dopplerRecipient = IAirlock(airlock).owner();
589
- uint256 dopplerPayout = _calculateReward(totalAmount, DOPPLER_MARKET_REWARD_BPS);
590
- uint256 creatorPayout = _calculateReward(totalAmount, CREATOR_MARKET_REWARD_BPS);
591
- uint256 platformReferrerPayout = _calculateReward(totalAmount, PLATFORM_REFERRER_MARKET_REWARD_BPS);
592
- uint256 protocolPayout = totalAmount - creatorPayout - platformReferrerPayout - dopplerPayout;
593
-
594
- if (token == WETH) {
595
- IWETH(WETH).withdraw(totalAmount);
596
-
597
- rewards.totalAmountCurrency = totalAmount;
598
- rewards.creatorPayoutAmountCurrency = creatorPayout;
599
- rewards.platformReferrerAmountCurrency = platformReferrerPayout;
600
- rewards.protocolAmountCurrency = protocolPayout;
601
-
602
- address[] memory recipients = new address[](4);
603
- recipients[0] = payoutRecipient;
604
- recipients[1] = platformReferrer;
605
- recipients[2] = protocolRewardRecipient;
606
- recipients[3] = dopplerRecipient;
607
-
608
- uint256[] memory amounts = new uint256[](4);
609
- amounts[0] = rewards.creatorPayoutAmountCurrency;
610
- amounts[1] = rewards.platformReferrerAmountCurrency;
611
- amounts[2] = rewards.protocolAmountCurrency;
612
- amounts[3] = dopplerPayout;
613
-
614
- bytes4[] memory reasons = new bytes4[](4);
615
- reasons[0] = bytes4(keccak256("COIN_CREATOR_MARKET_REWARD"));
616
- reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_MARKET_REWARD"));
617
- reasons[2] = bytes4(keccak256("COIN_PROTOCOL_MARKET_REWARD"));
618
- reasons[3] = bytes4(keccak256("COIN_DOPPLER_MARKET_REWARD"));
619
-
620
- IProtocolRewards(protocolRewards).depositBatch{value: totalAmount}(recipients, amounts, reasons, "");
621
- IProtocolRewards(protocolRewards).withdrawFor(dopplerRecipient, dopplerPayout);
622
- } else if (token == address(this)) {
623
- rewards.totalAmountCoin = totalAmount;
624
- rewards.creatorPayoutAmountCoin = creatorPayout;
625
- rewards.platformReferrerAmountCoin = platformReferrerPayout;
626
- rewards.protocolAmountCoin = protocolPayout;
627
-
628
- _transfer(address(this), payoutRecipient, rewards.creatorPayoutAmountCoin);
629
- _transfer(address(this), platformReferrer, rewards.platformReferrerAmountCoin);
630
- _transfer(address(this), protocolRewardRecipient, rewards.protocolAmountCoin);
631
- _transfer(address(this), dopplerRecipient, dopplerPayout);
632
- } else {
633
- rewards.totalAmountCurrency = totalAmount;
634
- rewards.creatorPayoutAmountCurrency = creatorPayout;
635
- rewards.platformReferrerAmountCurrency = platformReferrerPayout;
636
- rewards.protocolAmountCurrency = protocolPayout;
637
-
638
- IERC20(currency).safeTransfer(payoutRecipient, creatorPayout);
639
- IERC20(currency).safeTransfer(platformReferrer, platformReferrerPayout);
640
- IERC20(currency).safeTransfer(protocolRewardRecipient, protocolPayout);
641
- IERC20(currency).safeTransfer(dopplerRecipient, dopplerPayout);
642
- }
643
- }
644
-
645
- return rewards;
646
- }
647
-
648
- /// @dev Utility for computing amounts in basis points.
649
- function _calculateReward(uint256 amount, uint256 bps) internal pure returns (uint256) {
650
- return (amount * bps) / 10_000;
651
- }
652
345
  }
@@ -9,11 +9,17 @@ import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.s
9
9
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10
10
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
11
11
  import {CoinConfigurationVersions} from "./libs/CoinConfigurationVersions.sol";
12
-
12
+ import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
13
+ import {IWETH} from "./interfaces/IWETH.sol";
13
14
  import {IZoraFactory} from "./interfaces/IZoraFactory.sol";
15
+ import {IHasAfterCoinDeploy} from "./hooks/BaseCoinDeployHook.sol";
16
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
14
17
  import {Coin} from "./Coin.sol";
18
+ import {ICoin} from "./interfaces/ICoin.sol";
19
+ import {IHasContractName} from "@zoralabs/shared-contracts/interfaces/IContractMetadata.sol";
20
+ import {ContractVersionBase} from "./version/ContractVersionBase.sol";
15
21
 
16
- contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable {
22
+ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgradeable, OwnableUpgradeable, IHasContractName, ContractVersionBase {
17
23
  using SafeERC20 for IERC20;
18
24
 
19
25
  /// @notice The coin contract implementation address
@@ -23,15 +29,7 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
23
29
  coinImpl = _coinImpl;
24
30
  }
25
31
 
26
- /// @notice Creates a new coin contract
27
- /// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
28
- /// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
29
- /// @param uri The coin metadata uri
30
- /// @param name The name of the coin
31
- /// @param symbol The symbol of the coin
32
- /// @param poolConfig The config parameters for the Uniswap v3 pool; `abi.encode(address currency, int24 tickLower, int24 tickUpper, uint16 numDiscoveryPositions, uint256 maxDiscoverySupplyShare)`
33
- /// @param platformReferrer The address of the platform referrer
34
- /// @param orderSize The order size for the first buy; must match msg.value for ETH/WETH pairs
32
+ /// @inheritdoc IZoraFactory
35
33
  function deploy(
36
34
  address payoutRecipient,
37
35
  address[] memory owners,
@@ -42,40 +40,39 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
42
40
  address platformReferrer,
43
41
  uint256 orderSize
44
42
  ) public payable nonReentrant returns (address, uint256) {
45
- bytes32 salt = _generateSalt(payoutRecipient, uri);
46
-
47
- Coin coin = Coin(payable(Clones.cloneDeterministic(coinImpl, salt)));
48
-
49
- coin.initialize(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer);
43
+ Coin coin = _createAndInitializeCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer);
50
44
 
51
45
  uint256 coinsPurchased = _handleFirstOrder(coin, orderSize);
52
46
 
53
- emit CoinCreated(
54
- msg.sender,
55
- payoutRecipient,
56
- coin.platformReferrer(),
57
- coin.currency(),
58
- uri,
59
- name,
60
- symbol,
61
- address(coin),
62
- coin.poolAddress(),
63
- coin.contractVersion()
64
- );
65
-
66
47
  return (address(coin), coinsPurchased);
67
48
  }
68
49
 
69
- /// @notice Creates a new coin contract
70
- /// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
71
- /// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
72
- /// @param uri The coin metadata uri
73
- /// @param name The name of the coin
74
- /// @param symbol The symbol of the coin
75
- /// @param platformReferrer The address to receive platform referral rewards
76
- /// @param currency The address of the trading currency; address(0) for ETH/WETH
77
- /// @param tickLower The lower tick for the Uniswap V3 LP position; ignored for ETH/WETH pairs
78
- /// @param orderSize The order size for the first buy; must match msg.value for ETH/WETH pairs
50
+ /// @inheritdoc IZoraFactory
51
+ function deployWithHook(
52
+ address payoutRecipient,
53
+ address[] memory owners,
54
+ string memory uri,
55
+ string memory name,
56
+ string memory symbol,
57
+ bytes memory poolConfig,
58
+ address platformReferrer,
59
+ address hook,
60
+ bytes calldata hookData
61
+ ) public payable nonReentrant returns (address coin, bytes memory hookDataOut) {
62
+ coin = address(_createAndInitializeCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer));
63
+
64
+ if (hook != address(0)) {
65
+ if (!IERC165(hook).supportsInterface(type(IHasAfterCoinDeploy).interfaceId)) {
66
+ revert InvalidHook();
67
+ }
68
+ hookDataOut = IHasAfterCoinDeploy(hook).afterCoinDeploy{value: msg.value}(msg.sender, ICoin(coin), hookData);
69
+ } else if (msg.value > 0) {
70
+ // cannot send eth without a hook
71
+ revert EthTransferInvalid();
72
+ }
73
+ }
74
+
75
+ /// @inheritdoc IZoraFactory
79
76
  function deploy(
80
77
  address payoutRecipient,
81
78
  address[] memory owners,
@@ -87,16 +84,30 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
87
84
  int24 tickLower,
88
85
  uint256 orderSize
89
86
  ) public payable nonReentrant returns (address, uint256) {
87
+ bytes memory poolConfig = abi.encode(CoinConfigurationVersions.LEGACY_POOL_VERSION, currency, tickLower);
88
+
89
+ Coin coin = _createAndInitializeCoin(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer);
90
+
91
+ uint256 coinsPurchased = _handleFirstOrder(coin, orderSize);
92
+
93
+ return (address(coin), coinsPurchased);
94
+ }
95
+
96
+ function _createAndInitializeCoin(
97
+ address payoutRecipient,
98
+ address[] memory owners,
99
+ string memory uri,
100
+ string memory name,
101
+ string memory symbol,
102
+ bytes memory poolConfig,
103
+ address platformReferrer
104
+ ) internal returns (Coin) {
90
105
  bytes32 salt = _generateSalt(payoutRecipient, uri);
91
106
 
92
107
  Coin coin = Coin(payable(Clones.cloneDeterministic(coinImpl, salt)));
93
108
 
94
- bytes memory poolConfig = abi.encode(CoinConfigurationVersions.LEGACY_POOL_VERSION, currency, tickLower);
95
-
96
109
  coin.initialize(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer);
97
110
 
98
- uint256 coinsPurchased = _handleFirstOrder(coin, orderSize);
99
-
100
111
  emit CoinCreated(
101
112
  msg.sender,
102
113
  payoutRecipient,
@@ -110,7 +121,7 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
110
121
  coin.contractVersion()
111
122
  );
112
123
 
113
- return (address(coin), coinsPurchased);
124
+ return coin;
114
125
  }
115
126
 
116
127
  /// @dev Generates a unique salt for deterministic deployment
@@ -182,7 +193,24 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
182
193
  return ERC1967Utils.getImplementation();
183
194
  }
184
195
 
196
+ /// @inheritdoc IHasContractName
197
+ function contractName() public pure override returns (string memory) {
198
+ return "ZoraCoinFactory";
199
+ }
200
+
185
201
  /// @dev Authorizes an upgrade to a new implementation
186
202
  /// @param newImpl The new implementation address
187
- function _authorizeUpgrade(address newImpl) internal override onlyOwner {}
203
+ function _authorizeUpgrade(address newImpl) internal override onlyOwner {
204
+ // try to get the existing contract name - if it reverts, the existing contract was an older version that didn't have the contract name
205
+ // unfortunately we cannot use supportsInterface here because the existing implementation did not have that function
206
+ try IHasContractName(newImpl).contractName() returns (string memory name) {
207
+ if (!_equals(name, contractName())) {
208
+ revert UpgradeToMismatchedContractName(contractName(), name);
209
+ }
210
+ } catch {}
211
+ }
212
+
213
+ function _equals(string memory a, string memory b) internal pure returns (bool) {
214
+ return (keccak256(bytes(a)) == keccak256(bytes(b)));
215
+ }
188
216
  }