@zoralabs/coins 0.6.1 → 0.7.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 (66) hide show
  1. package/.turbo/turbo-build.log +69 -55
  2. package/CHANGELOG.md +12 -0
  3. package/abis/BaseTest.json +0 -23
  4. package/abis/Coin.json +186 -77
  5. package/abis/CoinConfigurationVersions.json +7 -0
  6. package/abis/CoinSetup.json +7 -0
  7. package/abis/CoinTest.json +5 -49
  8. package/abis/CustomRevert.json +28 -0
  9. package/abis/DopplerUniswapV3Test.json +891 -0
  10. package/abis/FactoryTest.json +7 -23
  11. package/abis/IAirlock.json +15 -0
  12. package/abis/ICoin.json +52 -34
  13. package/abis/IDopplerErrors.json +44 -0
  14. package/abis/INonfungiblePositionManager.json +13 -0
  15. package/abis/IUniswapV3Factory.json +198 -0
  16. package/abis/IUniswapV3Pool.json +135 -0
  17. package/abis/MultiOwnableTest.json +0 -23
  18. package/abis/SafeCast.json +7 -0
  19. package/abis/Simulate.json +120 -0
  20. package/abis/SqrtPriceMath.json +22 -0
  21. package/abis/TickMath.json +24 -0
  22. package/abis/ZoraFactoryImpl.json +59 -0
  23. package/addresses/8453.json +3 -3
  24. package/dist/index.cjs +160 -39
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +160 -39
  27. package/dist/index.js.map +1 -1
  28. package/dist/wagmiGenerated.d.ts +349 -67
  29. package/dist/wagmiGenerated.d.ts.map +1 -1
  30. package/package/wagmiGenerated.ts +161 -40
  31. package/package.json +3 -3
  32. package/script/CoinsDeployerBase.sol +1 -1
  33. package/script/Simulate.s.sol +67 -0
  34. package/src/Coin.sol +159 -90
  35. package/src/ZoraFactoryImpl.sol +47 -1
  36. package/src/interfaces/IAirlock.sol +6 -0
  37. package/src/interfaces/ICoin.sol +18 -2
  38. package/src/interfaces/IDopplerErrors.sol +14 -0
  39. package/src/interfaces/INonfungiblePositionManager.sol +2 -0
  40. package/src/interfaces/IUniswapV3Factory.sol +64 -0
  41. package/src/interfaces/IUniswapV3Pool.sol +48 -0
  42. package/src/libs/CoinConfigurationVersions.sol +9 -0
  43. package/src/libs/CoinDopplerUniV3.sol +202 -0
  44. package/src/libs/CoinLegacy.sol +48 -0
  45. package/src/libs/CoinSetup.sol +37 -0
  46. package/src/libs/MarketConstants.sol +25 -0
  47. package/src/types/LpPosition.sol +8 -0
  48. package/src/types/PoolState.sol +24 -0
  49. package/src/utils/CoinConstants.sol +5 -12
  50. package/src/utils/uniswap/BitMath.sol +55 -0
  51. package/src/utils/uniswap/CustomRevert.sol +111 -0
  52. package/src/utils/uniswap/FixedPoint96.sol +11 -0
  53. package/src/utils/uniswap/FullMath.sol +118 -0
  54. package/src/utils/uniswap/LiquidityAmounts.sol +117 -0
  55. package/src/utils/uniswap/SafeCast.sol +61 -0
  56. package/src/utils/uniswap/SqrtPriceMath.sol +249 -0
  57. package/src/utils/uniswap/TickMath.sol +244 -0
  58. package/src/utils/uniswap/UnsafeMath.sol +30 -0
  59. package/src/version/ContractVersionBase.sol +1 -1
  60. package/test/Coin.t.sol +65 -65
  61. package/test/CoinDopplerUniV3.t.sol +452 -0
  62. package/test/Factory.t.sol +49 -7
  63. package/test/utils/BaseTest.sol +26 -7
  64. package/wagmi.config.ts +1 -1
  65. package/abis/IERC721Receiver.json +0 -36
  66. package/src/utils/TickMath.sol +0 -210
package/src/Coin.sol CHANGED
@@ -3,12 +3,13 @@ pragma solidity ^0.8.23;
3
3
 
4
4
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
5
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
6
- import {ICoin} from "./interfaces/ICoin.sol";
6
+ import {ICoin, PoolConfiguration} from "./interfaces/ICoin.sol";
7
7
  import {ICoinComments} from "./interfaces/ICoinComments.sol";
8
8
  import {IERC7572} from "./interfaces/IERC7572.sol";
9
- import {INonfungiblePositionManager} from "./interfaces/INonfungiblePositionManager.sol";
9
+ import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
10
10
  import {IUniswapV3Pool} from "./interfaces/IUniswapV3Pool.sol";
11
11
  import {ISwapRouter} from "./interfaces/ISwapRouter.sol";
12
+ import {IAirlock} from "./interfaces/IAirlock.sol";
12
13
  import {IProtocolRewards} from "./interfaces/IProtocolRewards.sol";
13
14
  import {IWETH} from "./interfaces/IWETH.sol";
14
15
 
@@ -19,7 +20,13 @@ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol
19
20
  import {ContractVersionBase} from "./version/ContractVersionBase.sol";
20
21
  import {CoinConstants} from "./utils/CoinConstants.sol";
21
22
  import {MultiOwnable} from "./utils/MultiOwnable.sol";
22
- import {TickMath} from "./utils/TickMath.sol";
23
+ import {FullMath} from "./utils/uniswap/FullMath.sol";
24
+ import {TickMath} from "./utils/uniswap/TickMath.sol";
25
+ import {LiquidityAmounts} from "./utils/uniswap/LiquidityAmounts.sol";
26
+ import {CoinSetup} from "./libs/CoinSetup.sol";
27
+ import {MarketConstants} from "./libs/MarketConstants.sol";
28
+ import {LpPosition} from "./types/LpPosition.sol";
29
+ import {PoolState} from "./types/PoolState.sol";
23
30
 
24
31
  /*
25
32
  $$$$$$\ $$$$$$\ $$$$$$\ $$\ $$\
@@ -34,25 +41,85 @@ import {TickMath} from "./utils/TickMath.sol";
34
41
  contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ReentrancyGuardUpgradeable {
35
42
  using SafeERC20 for IERC20;
36
43
 
44
+ /// @notice The address of the WETH contract
37
45
  address public immutable WETH;
38
- address public immutable nonfungiblePositionManager;
46
+ /// @notice The address of the Uniswap V3 factory
47
+ address public immutable v3Factory;
48
+ /// @notice The address of the Uniswap V3 swap router
39
49
  address public immutable swapRouter;
50
+ /// @notice The address of the Airlock contract, ownership is used for a protocol fee split.
51
+ address public immutable airlock;
52
+ /// @notice The address of the protocol rewards contract
40
53
  address public immutable protocolRewards;
54
+ /// @notice The address of the protocol reward recipient
41
55
  address public immutable protocolRewardRecipient;
42
56
 
57
+ /// @notice The metadata URI
58
+ string public tokenURI;
59
+ /// @notice The address of the coin creator
43
60
  address public payoutRecipient;
61
+ /// @notice The address of the platform referrer
44
62
  address public platformReferrer;
63
+ /// @notice The address of the Uniswap V3 pool
45
64
  address public poolAddress;
65
+ /// @notice The address of the currency
46
66
  address public currency;
47
- uint256 public lpTokenId;
48
- string public tokenURI;
49
67
 
68
+ PoolConfiguration public poolConfiguration;
69
+
70
+ /// @notice Returns the state of the pool
71
+ /// @dev This is a legacy function for compatibility with doppler default state
72
+ /// @return asset The address of the asset
73
+ /// @return numeraire The address of the numeraire
74
+ /// @return tickLower The lower tick
75
+ /// @return tickUpper The upper tick
76
+ /// @return numPositions The number of discovery positions
77
+ /// @return isInitialized Whether the pool is initialized
78
+ /// @return isExited Whether the pool is exited
79
+ /// @return maxShareToBeSold The maximum share to be sold
80
+ /// @return totalTokensOnBondingCurve The total tokens on the bonding curve
81
+ function poolState()
82
+ external
83
+ view
84
+ returns (
85
+ address asset,
86
+ address numeraire,
87
+ int24 tickLower,
88
+ int24 tickUpper,
89
+ uint16 numPositions,
90
+ bool isInitialized,
91
+ bool isExited,
92
+ uint256 maxShareToBeSold,
93
+ uint256 totalTokensOnBondingCurve
94
+ )
95
+ {
96
+ asset = address(this);
97
+ numeraire = currency;
98
+ tickLower = poolConfiguration.tickLower;
99
+ tickUpper = poolConfiguration.tickUpper;
100
+ numPositions = poolConfiguration.numPositions;
101
+ isInitialized = true;
102
+ isExited = false;
103
+ maxShareToBeSold = poolConfiguration.maxDiscoverySupplyShare;
104
+ totalTokensOnBondingCurve = POOL_LAUNCH_SUPPLY;
105
+ }
106
+
107
+ /**
108
+ * @notice The constructor for the static Coin contract deployment shared across all Coins.
109
+ * @param _protocolRewardRecipient The address of the protocol reward recipient
110
+ * @param _protocolRewards The address of the protocol rewards contract
111
+ * @param _weth The address of the WETH contract
112
+ * @param _v3Factory The address of the Uniswap V3 factory
113
+ * @param _swapRouter The address of the Uniswap V3 swap router
114
+ * @param _airlock The address of the Airlock contract, ownership is used for a protocol fee split.
115
+ */
50
116
  constructor(
51
117
  address _protocolRewardRecipient,
52
118
  address _protocolRewards,
53
119
  address _weth,
54
- address _nonfungiblePositionManager,
55
- address _swapRouter
120
+ address _v3Factory,
121
+ address _swapRouter,
122
+ address _airlock
56
123
  ) initializer {
57
124
  if (_protocolRewardRecipient == address(0)) {
58
125
  revert AddressZero();
@@ -63,18 +130,22 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
63
130
  if (_weth == address(0)) {
64
131
  revert AddressZero();
65
132
  }
66
- if (_nonfungiblePositionManager == address(0)) {
133
+ if (_v3Factory == address(0)) {
67
134
  revert AddressZero();
68
135
  }
69
136
  if (_swapRouter == address(0)) {
70
137
  revert AddressZero();
71
138
  }
139
+ if (_airlock == address(0)) {
140
+ revert AddressZero();
141
+ }
72
142
 
73
143
  protocolRewardRecipient = _protocolRewardRecipient;
74
144
  protocolRewards = _protocolRewards;
75
145
  WETH = _weth;
76
- nonfungiblePositionManager = _nonfungiblePositionManager;
77
146
  swapRouter = _swapRouter;
147
+ v3Factory = _v3Factory;
148
+ airlock = _airlock;
78
149
  }
79
150
 
80
151
  /// @notice Initializes a new coin
@@ -82,18 +153,16 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
82
153
  /// @param tokenURI_ The metadata URI
83
154
  /// @param name_ The coin name
84
155
  /// @param symbol_ The coin symbol
156
+ /// @param poolConfig_ The parameters for the v3 pool and liquidity
85
157
  /// @param platformReferrer_ The address of the platform referrer
86
- /// @param currency_ The address of the currency
87
- /// @param tickLower_ The tick lower for the Uniswap V3 pool; ignored for ETH/WETH
88
158
  function initialize(
89
159
  address payoutRecipient_,
90
160
  address[] memory owners_,
91
161
  string memory tokenURI_,
92
162
  string memory name_,
93
163
  string memory symbol_,
94
- address platformReferrer_,
95
- address currency_,
96
- int24 tickLower_
164
+ bytes memory poolConfig_,
165
+ address platformReferrer_
97
166
  ) public initializer {
98
167
  // Validate the creation parameters
99
168
  if (payoutRecipient_ == address(0)) {
@@ -110,9 +179,8 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
110
179
  _setPayoutRecipient(payoutRecipient_);
111
180
  _setContractURI(tokenURI_);
112
181
 
113
- // Set immutable state
182
+ // Store the referrer if set
114
183
  platformReferrer = platformReferrer_ == address(0) ? protocolRewardRecipient : platformReferrer_;
115
- currency = currency_ == address(0) ? WETH : currency_;
116
184
 
117
185
  // Mint the total supply
118
186
  _mint(address(this), MAX_TOTAL_SUPPLY);
@@ -120,11 +188,8 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
120
188
  // Distribute the creator launch reward
121
189
  _transfer(address(this), payoutRecipient, CREATOR_LAUNCH_REWARD);
122
190
 
123
- // Approve the transfer of the remaining supply to the pool
124
- IERC20(address(this)).safeIncreaseAllowance(address(nonfungiblePositionManager), POOL_LAUNCH_SUPPLY);
125
-
126
191
  // Deploy the pool
127
- _deployPool(tickLower_);
192
+ _deployLiquidity(poolConfig_);
128
193
  }
129
194
 
130
195
  /// @notice Executes a buy order
@@ -157,7 +222,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
157
222
  ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
158
223
  tokenIn: currency,
159
224
  tokenOut: address(this),
160
- fee: LP_FEE,
225
+ fee: MarketConstants.LP_FEE,
161
226
  recipient: recipient,
162
227
  amountIn: trueOrderSize,
163
228
  amountOutMinimum: minAmountOut,
@@ -207,7 +272,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
207
272
  ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
208
273
  tokenIn: address(this),
209
274
  tokenOut: currency,
210
- fee: LP_FEE,
275
+ fee: MarketConstants.LP_FEE,
211
276
  recipient: address(this),
212
277
  amountIn: orderSize,
213
278
  amountOutMinimum: minAmountOut,
@@ -257,6 +322,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
257
322
  /// @notice Enables a user to burn their tokens
258
323
  /// @param amount The amount of tokens to burn
259
324
  function burn(uint256 amount) external {
325
+ // This burn function sets the from as msg.sender, so having an unauthed call is safe.
260
326
  _burn(msg.sender, amount);
261
327
  }
262
328
 
@@ -302,24 +368,17 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
302
368
 
303
369
  /// @notice Receives ETH converted from WETH
304
370
  receive() external payable {
305
- if (msg.sender != WETH) {
306
- revert OnlyWeth();
307
- }
371
+ require(msg.sender == WETH, OnlyWeth());
308
372
  }
309
373
 
310
- /// @dev For receiving the Uniswap V3 LP NFT on market graduation.
311
- function onERC721Received(address, address, uint256, bytes calldata) external view returns (bytes4) {
374
+ /// @dev Called by the pool after minting liquidity to transfer the associated coins
375
+ function uniswapV3MintCallback(uint256 amount0Owed, uint256 amount1Owed, bytes calldata) external {
312
376
  if (msg.sender != poolAddress) revert OnlyPool();
313
377
 
314
- return this.onERC721Received.selector;
378
+ IERC20(address(this)).safeTransfer(poolAddress, amount0Owed == 0 ? amount1Owed : amount0Owed);
315
379
  }
316
380
 
317
- /// @dev No-op to allow a swap on the pool to set the correct initial price, if needed
318
- function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata) external {}
319
-
320
- /// @dev Overrides ERC20's _update function to
321
- /// - Prevent transfers to the pool if the market has not graduated.
322
- /// - Emit the superset `WowTokenTransfer` event with each ERC20 transfer.
381
+ /// @dev Overrides ERC20's _update function to emit a superset `CoinTransfer` event
323
382
  function _update(address from, address to, uint256 value) internal virtual override {
324
383
  super._update(from, to, value);
325
384
 
@@ -347,57 +406,43 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
347
406
  tokenURI = newURI;
348
407
  }
349
408
 
350
- /// @dev Deploy the pool
351
- function _deployPool(int24 tickLower_) internal {
352
- // If WETH is the pool's currency, validate the lower tick
353
- if (currency == WETH && tickLower_ < LP_TICK_LOWER_WETH) {
354
- revert InvalidWethLowerTick();
355
- }
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));
356
412
 
357
- // Note: This validation happens on the Uniswap pool already; reverting early here for clarity
358
- // If currency is not WETH: ensure lower tick is less than upper tick and satisfies the 200 tick spacing requirement for 1% Uniswap V3 pools
359
- if (currency != WETH && (tickLower_ >= LP_TICK_UPPER || tickLower_ % 200 != 0)) {
360
- revert InvalidCurrencyLowerTick();
361
- }
413
+ // Store the currency, defaulting to WETH if address(0)
414
+ currency = currency_ == address(0) ? WETH : currency_;
362
415
 
363
416
  // Sort the token addresses
364
417
  address token0 = address(this) < currency ? address(this) : currency;
365
418
  address token1 = address(this) < currency ? currency : address(this);
366
-
367
- // If the coin is token0
368
419
  bool isCoinToken0 = token0 == address(this);
369
420
 
370
- // Determine the tick values
371
- int24 tickLower = isCoinToken0 ? tickLower_ : -LP_TICK_UPPER;
372
- int24 tickUpper = isCoinToken0 ? LP_TICK_UPPER : -tickLower_;
373
-
374
- // Calculate the starting price for the pool
375
- uint160 sqrtPriceX96 = TickMath.getSqrtRatioAtTick(isCoinToken0 ? tickLower : tickUpper);
376
-
377
- // Determine the initial liquidity amounts
378
- uint256 amount0 = isCoinToken0 ? POOL_LAUNCH_SUPPLY : 0;
379
- uint256 amount1 = isCoinToken0 ? 0 : POOL_LAUNCH_SUPPLY;
380
-
381
- // Create and initialize the pool
382
- poolAddress = INonfungiblePositionManager(nonfungiblePositionManager).createAndInitializePoolIfNecessary(token0, token1, LP_FEE, sqrtPriceX96);
383
-
384
- // Construct the LP data
385
- INonfungiblePositionManager.MintParams memory params = INonfungiblePositionManager.MintParams({
386
- token0: token0,
387
- token1: token1,
388
- fee: LP_FEE,
389
- tickLower: tickLower,
390
- tickUpper: tickUpper,
391
- amount0Desired: amount0,
392
- amount1Desired: amount1,
393
- amount0Min: 0,
394
- amount1Min: 0,
395
- recipient: address(this),
396
- deadline: block.timestamp
397
- });
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);
398
435
 
399
- // Mint the LP
400
- (lpTokenId, , , ) = INonfungiblePositionManager(nonfungiblePositionManager).mint(params);
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
+ }
401
446
  }
402
447
 
403
448
  /// @dev Handles incoming currency transfers for buy orders; if WETH is the currency the caller has the option to send native-ETH
@@ -499,15 +544,31 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
499
544
  );
500
545
  }
501
546
 
547
+ /// @dev Collects and distributes accrued fees from all LP positions
502
548
  function _handleMarketRewards() internal returns (MarketRewards memory) {
503
- INonfungiblePositionManager.CollectParams memory params = INonfungiblePositionManager.CollectParams({
504
- tokenId: lpTokenId,
505
- recipient: address(this),
506
- amount0Max: type(uint128).max,
507
- amount1Max: type(uint128).max
508
- });
509
-
510
- (uint256 totalAmountToken0, uint256 totalAmountToken1) = INonfungiblePositionManager(nonfungiblePositionManager).collect(params);
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
+ }
511
572
 
512
573
  address token0 = currency < address(this) ? currency : address(this);
513
574
  address token1 = currency < address(this) ? address(this) : currency;
@@ -524,9 +585,11 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
524
585
 
525
586
  function _transferMarketRewards(address token, uint256 totalAmount, MarketRewards memory rewards) internal returns (MarketRewards memory) {
526
587
  if (totalAmount > 0) {
588
+ address dopplerRecipient = IAirlock(airlock).owner();
589
+ uint256 dopplerPayout = _calculateReward(totalAmount, DOPPLER_MARKET_REWARD_BPS);
527
590
  uint256 creatorPayout = _calculateReward(totalAmount, CREATOR_MARKET_REWARD_BPS);
528
591
  uint256 platformReferrerPayout = _calculateReward(totalAmount, PLATFORM_REFERRER_MARKET_REWARD_BPS);
529
- uint256 protocolPayout = totalAmount - creatorPayout - platformReferrerPayout;
592
+ uint256 protocolPayout = totalAmount - creatorPayout - platformReferrerPayout - dopplerPayout;
530
593
 
531
594
  if (token == WETH) {
532
595
  IWETH(WETH).withdraw(totalAmount);
@@ -536,22 +599,26 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
536
599
  rewards.platformReferrerAmountCurrency = platformReferrerPayout;
537
600
  rewards.protocolAmountCurrency = protocolPayout;
538
601
 
539
- address[] memory recipients = new address[](3);
602
+ address[] memory recipients = new address[](4);
540
603
  recipients[0] = payoutRecipient;
541
604
  recipients[1] = platformReferrer;
542
605
  recipients[2] = protocolRewardRecipient;
606
+ recipients[3] = dopplerRecipient;
543
607
 
544
- uint256[] memory amounts = new uint256[](3);
608
+ uint256[] memory amounts = new uint256[](4);
545
609
  amounts[0] = rewards.creatorPayoutAmountCurrency;
546
610
  amounts[1] = rewards.platformReferrerAmountCurrency;
547
611
  amounts[2] = rewards.protocolAmountCurrency;
612
+ amounts[3] = dopplerPayout;
548
613
 
549
- bytes4[] memory reasons = new bytes4[](3);
614
+ bytes4[] memory reasons = new bytes4[](4);
550
615
  reasons[0] = bytes4(keccak256("COIN_CREATOR_MARKET_REWARD"));
551
616
  reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_MARKET_REWARD"));
552
617
  reasons[2] = bytes4(keccak256("COIN_PROTOCOL_MARKET_REWARD"));
618
+ reasons[3] = bytes4(keccak256("COIN_DOPPLER_MARKET_REWARD"));
553
619
 
554
620
  IProtocolRewards(protocolRewards).depositBatch{value: totalAmount}(recipients, amounts, reasons, "");
621
+ IProtocolRewards(protocolRewards).withdrawFor(dopplerRecipient, dopplerPayout);
555
622
  } else if (token == address(this)) {
556
623
  rewards.totalAmountCoin = totalAmount;
557
624
  rewards.creatorPayoutAmountCoin = creatorPayout;
@@ -561,6 +628,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
561
628
  _transfer(address(this), payoutRecipient, rewards.creatorPayoutAmountCoin);
562
629
  _transfer(address(this), platformReferrer, rewards.platformReferrerAmountCoin);
563
630
  _transfer(address(this), protocolRewardRecipient, rewards.protocolAmountCoin);
631
+ _transfer(address(this), dopplerRecipient, dopplerPayout);
564
632
  } else {
565
633
  rewards.totalAmountCurrency = totalAmount;
566
634
  rewards.creatorPayoutAmountCurrency = creatorPayout;
@@ -570,6 +638,7 @@ contract Coin is ICoin, CoinConstants, ContractVersionBase, ERC20PermitUpgradeab
570
638
  IERC20(currency).safeTransfer(payoutRecipient, creatorPayout);
571
639
  IERC20(currency).safeTransfer(platformReferrer, platformReferrerPayout);
572
640
  IERC20(currency).safeTransfer(protocolRewardRecipient, protocolPayout);
641
+ IERC20(currency).safeTransfer(dopplerRecipient, dopplerPayout);
573
642
  }
574
643
  }
575
644
 
@@ -8,6 +8,7 @@ import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/U
8
8
  import {ERC1967Utils} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Utils.sol";
9
9
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
10
10
  import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
11
+ import {CoinConfigurationVersions} from "./libs/CoinConfigurationVersions.sol";
11
12
 
12
13
  import {IZoraFactory} from "./interfaces/IZoraFactory.sol";
13
14
  import {Coin} from "./Coin.sol";
@@ -22,6 +23,49 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
22
23
  coinImpl = _coinImpl;
23
24
  }
24
25
 
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
35
+ function deploy(
36
+ address payoutRecipient,
37
+ address[] memory owners,
38
+ string memory uri,
39
+ string memory name,
40
+ string memory symbol,
41
+ bytes memory poolConfig,
42
+ address platformReferrer,
43
+ uint256 orderSize
44
+ ) 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);
50
+
51
+ uint256 coinsPurchased = _handleFirstOrder(coin, orderSize);
52
+
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
+ return (address(coin), coinsPurchased);
67
+ }
68
+
25
69
  /// @notice Creates a new coin contract
26
70
  /// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
27
71
  /// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
@@ -47,7 +91,9 @@ contract ZoraFactoryImpl is IZoraFactory, UUPSUpgradeable, ReentrancyGuardUpgrad
47
91
 
48
92
  Coin coin = Coin(payable(Clones.cloneDeterministic(coinImpl, salt)));
49
93
 
50
- coin.initialize(payoutRecipient, owners, uri, name, symbol, platformReferrer, currency, tickLower);
94
+ bytes memory poolConfig = abi.encode(CoinConfigurationVersions.LEGACY_POOL_VERSION, currency, tickLower);
95
+
96
+ coin.initialize(payoutRecipient, owners, uri, name, symbol, poolConfig, platformReferrer);
51
97
 
52
98
  uint256 coinsPurchased = _handleFirstOrder(coin, orderSize);
53
99
 
@@ -0,0 +1,6 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ interface IAirlock {
5
+ function owner() external view returns (address);
6
+ }
@@ -2,10 +2,20 @@
2
2
  pragma solidity ^0.8.23;
3
3
 
4
4
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
5
- import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
6
5
  import {IERC7572} from "./IERC7572.sol";
6
+ import {IDopplerErrors} from "./IDopplerErrors.sol";
7
+
8
+ /// @notice The configuration of the pool
9
+ /// @dev This is used to configure the pool's liquidity positions
10
+ struct PoolConfiguration {
11
+ uint8 version;
12
+ int24 tickLower;
13
+ int24 tickUpper;
14
+ uint16 numPositions;
15
+ uint256 maxDiscoverySupplyShare;
16
+ }
7
17
 
8
- interface ICoin is IERC165, IERC721Receiver, IERC7572 {
18
+ interface ICoin is IERC165, IERC7572, IDopplerErrors {
9
19
  /// @notice Thrown when an operation is attempted with a zero address
10
20
  error AddressZero();
11
21
 
@@ -57,6 +67,12 @@ interface ICoin is IERC165, IERC721Receiver, IERC7572 {
57
67
  /// @notice Thrown when the lower tick is not set to the default value
58
68
  error InvalidWethLowerTick();
59
69
 
70
+ /// @notice Thrown when a legacy pool does not have one discovery position
71
+ error LegacyPoolMustHaveOneDiscoveryPosition();
72
+
73
+ /// @notice Thrown when a Doppler pool does not have more than 2 discovery positions
74
+ error DopplerPoolMustHaveMoreThan2DiscoveryPositions();
75
+
60
76
  /// @notice The rewards accrued from the market's liquidity position
61
77
  struct MarketRewards {
62
78
  uint256 totalAmountCurrency;
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ interface IDopplerErrors {
5
+ error NumDiscoveryPositionsOutOfRange();
6
+
7
+ error CannotMintZeroLiquidity();
8
+
9
+ /// @notice Thrown when the tick range is misordered
10
+ error InvalidTickRangeMisordered(int24 tickLower, int24 tickUpper);
11
+
12
+ /// @notice Thrown when the max share to be sold exceeds the maximum unit
13
+ error MaxShareToBeSoldExceeded(uint256 value, uint256 limit);
14
+ }
@@ -94,6 +94,8 @@ interface INonfungiblePositionManager {
94
94
 
95
95
  function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
96
96
 
97
+ function factory() external view returns (address);
98
+
97
99
  /// @notice Emitted when tokens are collected for a position NFT
98
100
  /// @dev The amounts reported may not be exactly equivalent to the amounts transferred, due to rounding behavior
99
101
  /// @param tokenId The ID of the token for which underlying tokens were collected
@@ -0,0 +1,64 @@
1
+ // SPDX-License-Identifier: GPL-2.0-or-later
2
+ pragma solidity >=0.5.0;
3
+
4
+ /// @title The interface for the Uniswap V3 Factory
5
+ /// @notice The Uniswap V3 Factory facilitates creation of Uniswap V3 pools and control over the protocol fees
6
+ interface IUniswapV3Factory {
7
+ /// @notice Emitted when the owner of the factory is changed
8
+ /// @param oldOwner The owner before the owner was changed
9
+ /// @param newOwner The owner after the owner was changed
10
+ event OwnerChanged(address indexed oldOwner, address indexed newOwner);
11
+
12
+ /// @notice Emitted when a pool is created
13
+ /// @param token0 The first token of the pool by address sort order
14
+ /// @param token1 The second token of the pool by address sort order
15
+ /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
16
+ /// @param tickSpacing The minimum number of ticks between initialized ticks
17
+ /// @param pool The address of the created pool
18
+ event PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool);
19
+
20
+ /// @notice Emitted when a new fee amount is enabled for pool creation via the factory
21
+ /// @param fee The enabled fee, denominated in hundredths of a bip
22
+ /// @param tickSpacing The minimum number of ticks between initialized ticks for pools created with the given fee
23
+ event FeeAmountEnabled(uint24 indexed fee, int24 indexed tickSpacing);
24
+
25
+ /// @notice Returns the current owner of the factory
26
+ /// @dev Can be changed by the current owner via setOwner
27
+ /// @return The address of the factory owner
28
+ function owner() external view returns (address);
29
+
30
+ /// @notice Returns the tick spacing for a given fee amount, if enabled, or 0 if not enabled
31
+ /// @dev A fee amount can never be removed, so this value should be hard coded or cached in the calling context
32
+ /// @param fee The enabled fee, denominated in hundredths of a bip. Returns 0 in case of unenabled fee
33
+ /// @return The tick spacing
34
+ function feeAmountTickSpacing(uint24 fee) external view returns (int24);
35
+
36
+ /// @notice Returns the pool address for a given pair of tokens and a fee, or address 0 if it does not exist
37
+ /// @dev tokenA and tokenB may be passed in either token0/token1 or token1/token0 order
38
+ /// @param tokenA The contract address of either token0 or token1
39
+ /// @param tokenB The contract address of the other token
40
+ /// @param fee The fee collected upon every swap in the pool, denominated in hundredths of a bip
41
+ /// @return pool The pool address
42
+ function getPool(address tokenA, address tokenB, uint24 fee) external view returns (address pool);
43
+
44
+ /// @notice Creates a pool for the given two tokens and fee
45
+ /// @param tokenA One of the two tokens in the desired pool
46
+ /// @param tokenB The other of the two tokens in the desired pool
47
+ /// @param fee The desired fee for the pool
48
+ /// @dev tokenA and tokenB may be passed in either order: token0/token1 or token1/token0. tickSpacing is retrieved
49
+ /// from the fee. The call will revert if the pool already exists, the fee is invalid, or the token arguments
50
+ /// are invalid.
51
+ /// @return pool The address of the newly created pool
52
+ function createPool(address tokenA, address tokenB, uint24 fee) external returns (address pool);
53
+
54
+ /// @notice Updates the owner of the factory
55
+ /// @dev Must be called by the current owner
56
+ /// @param _owner The new owner of the factory
57
+ function setOwner(address _owner) external;
58
+
59
+ /// @notice Enables a fee amount with the given tickSpacing
60
+ /// @dev Fee amounts may never be removed once enabled
61
+ /// @param fee The fee amount to enable, denominated in hundredths of a bip (i.e. 1e-6)
62
+ /// @param tickSpacing The spacing between ticks to be enforced for all pools created with the given fee amount
63
+ function enableFeeAmount(uint24 fee, int24 tickSpacing) external;
64
+ }