@zoralabs/coins 0.7.0 → 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 +34 -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 +27 -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 +20 -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 +35 -17
  97. package/test/CoinDopplerUniV3.t.sol +14 -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
@@ -0,0 +1,449 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import {ISwapRouter} from "../interfaces/ISwapRouter.sol";
7
+ import {IWETH} from "../interfaces/IWETH.sol";
8
+ import {MarketConstants} from "./MarketConstants.sol";
9
+ import {CoinConstants} from "./CoinConstants.sol";
10
+ import {ICoin} from "../interfaces/ICoin.sol";
11
+ import {IProtocolRewards} from "../interfaces/IProtocolRewards.sol";
12
+ import {LpPosition} from "../types/LpPosition.sol";
13
+ import {PoolConfiguration} from "../interfaces/ICoin.sol";
14
+ import {IUniswapV3Pool} from "../interfaces/IUniswapV3Pool.sol";
15
+ import {IAirlock} from "../interfaces/IAirlock.sol";
16
+ import {UniV3Config, CoinV3Config} from "./CoinSetupV3.sol";
17
+ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
18
+ import {CoinLegacyMarket} from "./CoinLegacyMarket.sol";
19
+ import {CoinDopplerUniV3} from "./CoinDopplerUniV3.sol";
20
+ import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
21
+
22
+ struct CoinConfig {
23
+ address protocolRewardRecipient;
24
+ address platformReferrer;
25
+ address currency;
26
+ address payoutRecipient;
27
+ address protocolRewards;
28
+ address poolAddress;
29
+ PoolConfiguration poolConfiguration;
30
+ UniV3Config uniswapV3Config;
31
+ }
32
+
33
+ library UniV3BuySell {
34
+ using SafeERC20 for IERC20;
35
+
36
+ error AddressZero();
37
+ error InvalidPoolVersion();
38
+
39
+ function _handleBuy(
40
+ address recipient,
41
+ uint256 orderSize,
42
+ uint256 minAmountOut,
43
+ uint160 sqrtPriceLimitX96,
44
+ address tradeReferrer,
45
+ address coin,
46
+ CoinConfig memory coinConfig
47
+ ) internal returns (uint256 amountOut, uint256 tradeReward, uint256 trueOrderSize) {
48
+ if (recipient == address(0)) {
49
+ revert AddressZero();
50
+ }
51
+
52
+ // Calculate the trade reward
53
+ tradeReward = _calculateReward(orderSize, CoinConstants.TOTAL_FEE_BPS);
54
+
55
+ // Calculate the remaining size
56
+ trueOrderSize = orderSize - tradeReward;
57
+
58
+ // Handle incoming currency
59
+ _handleIncomingCurrency(orderSize, trueOrderSize, coinConfig.currency, coinConfig.uniswapV3Config.weth, coinConfig.uniswapV3Config.swapRouter);
60
+
61
+ // Set up the swap parameters
62
+ ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
63
+ tokenIn: coinConfig.currency,
64
+ tokenOut: coin,
65
+ fee: MarketConstants.LP_FEE,
66
+ recipient: recipient,
67
+ amountIn: trueOrderSize,
68
+ amountOutMinimum: minAmountOut,
69
+ sqrtPriceLimitX96: sqrtPriceLimitX96
70
+ });
71
+
72
+ // Execute the swap
73
+ amountOut = ISwapRouter(coinConfig.uniswapV3Config.swapRouter).exactInputSingle(params);
74
+
75
+ _handleTradeRewards(tradeReward, tradeReferrer, coinConfig);
76
+ }
77
+
78
+ /// @notice Executes a buy order
79
+ /// @param recipient The recipient address of the coins
80
+ /// @param orderSize The amount of coins to buy
81
+ /// @param tradeReferrer The address of the trade referrer
82
+ /// @param sqrtPriceLimitX96 The price limit for Uniswap V3 pool swap
83
+ function buy(
84
+ address recipient,
85
+ uint256 orderSize,
86
+ uint256 minAmountOut,
87
+ uint160 sqrtPriceLimitX96,
88
+ address tradeReferrer,
89
+ address coin,
90
+ CoinConfig memory coinConfig
91
+ ) internal returns (uint256, uint256) {
92
+ (uint256 amountOut, uint256 tradeReward, uint256 trueOrderSize) = _handleBuy(
93
+ recipient,
94
+ orderSize,
95
+ minAmountOut,
96
+ sqrtPriceLimitX96,
97
+ tradeReferrer,
98
+ coin,
99
+ coinConfig
100
+ );
101
+
102
+ handleMarketRewards(coin, coinConfig);
103
+
104
+ emit ICoin.CoinBuy(msg.sender, recipient, tradeReferrer, amountOut, coinConfig.currency, tradeReward, trueOrderSize);
105
+
106
+ return (orderSize, amountOut);
107
+ }
108
+
109
+ function _handleSell(
110
+ address recipient,
111
+ uint256 beforeCoinBalance,
112
+ uint256 orderSize,
113
+ uint256 minAmountOut,
114
+ uint160 sqrtPriceLimitX96,
115
+ address tradeReferrer,
116
+ CoinConfig memory coinConfig
117
+ ) internal returns (uint256 payoutSize, uint256 tradeReward, uint256 trueOrderSize) {
118
+ // Ensure the recipient is not the zero address
119
+ if (recipient == address(0)) {
120
+ revert AddressZero();
121
+ }
122
+
123
+ // Set the swap parameters
124
+ ISwapRouter.ExactInputSingleParams memory params = ISwapRouter.ExactInputSingleParams({
125
+ tokenIn: address(this),
126
+ tokenOut: coinConfig.currency,
127
+ fee: MarketConstants.LP_FEE,
128
+ recipient: address(this),
129
+ amountIn: orderSize,
130
+ amountOutMinimum: minAmountOut,
131
+ sqrtPriceLimitX96: sqrtPriceLimitX96
132
+ });
133
+
134
+ // Execute the swap
135
+ uint256 amountOut = ISwapRouter(coinConfig.uniswapV3Config.swapRouter).exactInputSingle(params);
136
+
137
+ // Record the coin balance of this contract after the swap
138
+ uint256 afterCoinBalance = IERC20(address(this)).balanceOf(address(this));
139
+
140
+ trueOrderSize = orderSize;
141
+
142
+ // If the swap was partially executed:
143
+ if (afterCoinBalance > beforeCoinBalance) {
144
+ // Calculate the refund
145
+ uint256 coinRefund = afterCoinBalance - beforeCoinBalance;
146
+
147
+ // Update the order size
148
+ trueOrderSize -= coinRefund;
149
+
150
+ // Transfer the refund back to the seller
151
+ IERC20(address(this)).safeTransfer(recipient, coinRefund);
152
+ }
153
+
154
+ // If currency is WETH, convert to ETH
155
+ if (coinConfig.currency == coinConfig.uniswapV3Config.weth) {
156
+ IWETH(coinConfig.uniswapV3Config.weth).withdraw(amountOut);
157
+ }
158
+
159
+ // Calculate the trade reward
160
+ tradeReward = _calculateReward(amountOut, CoinConstants.TOTAL_FEE_BPS);
161
+
162
+ // Calculate the payout after the fee
163
+ payoutSize = amountOut - tradeReward;
164
+
165
+ _handlePayout(payoutSize, recipient, coinConfig.currency, coinConfig.uniswapV3Config.weth);
166
+
167
+ _handleTradeRewards(tradeReward, tradeReferrer, coinConfig);
168
+ }
169
+
170
+ /// @notice Executes a sell order
171
+ /// @param recipient The recipient of the currency
172
+ /// @param _orderSize The amount of coins to sell
173
+ /// @param minAmountOut The minimum amount of currency to receive
174
+ /// @param sqrtPriceLimitX96 The price limit for the swap
175
+ /// @param tradeReferrer The address of the trade referrer
176
+ function sell(
177
+ address recipient,
178
+ uint256 beforeCoinBalance,
179
+ uint256 _orderSize,
180
+ uint256 minAmountOut,
181
+ uint160 sqrtPriceLimitX96,
182
+ address tradeReferrer,
183
+ CoinConfig memory coinConfig
184
+ ) internal returns (uint256 trueOrderSize, uint256 payoutSize) {
185
+ uint256 tradeReward;
186
+ (payoutSize, tradeReward, trueOrderSize) = _handleSell(
187
+ recipient,
188
+ beforeCoinBalance,
189
+ _orderSize,
190
+ minAmountOut,
191
+ sqrtPriceLimitX96,
192
+ tradeReferrer,
193
+ coinConfig
194
+ );
195
+
196
+ handleMarketRewards(address(this), coinConfig);
197
+
198
+ emit ICoin.CoinSell(msg.sender, recipient, tradeReferrer, trueOrderSize, coinConfig.currency, tradeReward, payoutSize);
199
+ }
200
+
201
+ /// @dev Handles incoming currency transfers for buy orders; if WETH is the currency the caller has the option to send native-ETH
202
+ /// @param orderSize The total size of the order in the currency
203
+ /// @param trueOrderSize The actual amount being used for the swap after fees
204
+ function _handleIncomingCurrency(uint256 orderSize, uint256 trueOrderSize, address currency, address weth, address swapRouter) internal {
205
+ if (currency == weth && msg.value > 0) {
206
+ if (msg.value != orderSize) {
207
+ revert ICoin.EthAmountMismatch();
208
+ }
209
+
210
+ if (msg.value < CoinConstants.MIN_ORDER_SIZE) {
211
+ revert ICoin.EthAmountTooSmall();
212
+ }
213
+
214
+ IWETH(weth).deposit{value: trueOrderSize}();
215
+ IWETH(weth).approve(swapRouter, trueOrderSize);
216
+ } else {
217
+ // Ensure ETH is not sent with a non-ETH pair
218
+ if (msg.value != 0) {
219
+ revert ICoin.EthTransferInvalid();
220
+ }
221
+
222
+ uint256 beforeBalance = IERC20(currency).balanceOf(address(this));
223
+ IERC20(currency).safeTransferFrom(msg.sender, address(this), orderSize);
224
+ uint256 afterBalance = IERC20(currency).balanceOf(address(this));
225
+
226
+ if ((afterBalance - beforeBalance) != orderSize) {
227
+ revert ICoin.ERC20TransferAmountMismatch();
228
+ }
229
+
230
+ IERC20(currency).approve(swapRouter, trueOrderSize);
231
+ }
232
+ }
233
+
234
+ /// @dev Handles sending ETH and ERC20 payouts and refunds to recipients
235
+ /// @param orderPayout The amount of currency to pay out
236
+ /// @param recipient The address to receive the payout
237
+ function _handlePayout(uint256 orderPayout, address recipient, address currency, address weth) internal {
238
+ if (currency == weth) {
239
+ Address.sendValue(payable(recipient), orderPayout);
240
+ } else {
241
+ IERC20(currency).safeTransfer(recipient, orderPayout);
242
+ }
243
+ }
244
+
245
+ /// @dev Handles calculating and depositing fees to an escrow protocol rewards contract
246
+ function _handleTradeRewards(uint256 totalValue, address _tradeReferrer, CoinConfig memory coinConfig) internal {
247
+ address protocolRewardRecipient = coinConfig.protocolRewardRecipient;
248
+ address platformReferrer = coinConfig.platformReferrer;
249
+ address currency = coinConfig.currency;
250
+ address weth = coinConfig.uniswapV3Config.weth;
251
+ address payoutRecipient = coinConfig.payoutRecipient;
252
+ IProtocolRewards protocolRewards = IProtocolRewards(coinConfig.protocolRewards);
253
+
254
+ if (_tradeReferrer == address(0)) {
255
+ _tradeReferrer = protocolRewardRecipient;
256
+ }
257
+
258
+ uint256 tokenCreatorFee = _calculateReward(totalValue, CoinConstants.TOKEN_CREATOR_FEE_BPS);
259
+ uint256 platformReferrerFee = _calculateReward(totalValue, CoinConstants.PLATFORM_REFERRER_FEE_BPS);
260
+ uint256 tradeReferrerFee = _calculateReward(totalValue, CoinConstants.TRADE_REFERRER_FEE_BPS);
261
+ uint256 protocolFee = totalValue - tokenCreatorFee - platformReferrerFee - tradeReferrerFee;
262
+
263
+ if (currency == weth) {
264
+ address[] memory recipients = new address[](4);
265
+ uint256[] memory amounts = new uint256[](4);
266
+ bytes4[] memory reasons = new bytes4[](4);
267
+
268
+ recipients[0] = payoutRecipient;
269
+ amounts[0] = tokenCreatorFee;
270
+ reasons[0] = bytes4(keccak256("COIN_CREATOR_REWARD"));
271
+
272
+ recipients[1] = platformReferrer;
273
+ amounts[1] = platformReferrerFee;
274
+ reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_REWARD"));
275
+
276
+ recipients[2] = _tradeReferrer;
277
+ amounts[2] = tradeReferrerFee;
278
+ reasons[2] = bytes4(keccak256("COIN_TRADE_REFERRER_REWARD"));
279
+
280
+ recipients[3] = protocolRewardRecipient;
281
+ amounts[3] = protocolFee;
282
+ reasons[3] = bytes4(keccak256("COIN_PROTOCOL_REWARD"));
283
+
284
+ IProtocolRewards(protocolRewards).depositBatch{value: totalValue}(recipients, amounts, reasons, "");
285
+ }
286
+
287
+ if (currency != weth) {
288
+ IERC20(currency).safeTransfer(payoutRecipient, tokenCreatorFee);
289
+ IERC20(currency).safeTransfer(platformReferrer, platformReferrerFee);
290
+ IERC20(currency).safeTransfer(_tradeReferrer, tradeReferrerFee);
291
+ IERC20(currency).safeTransfer(protocolRewardRecipient, protocolFee);
292
+ }
293
+
294
+ emit ICoin.CoinTradeRewards(
295
+ payoutRecipient,
296
+ platformReferrer,
297
+ _tradeReferrer,
298
+ protocolRewardRecipient,
299
+ tokenCreatorFee,
300
+ platformReferrerFee,
301
+ tradeReferrerFee,
302
+ protocolFee,
303
+ currency
304
+ );
305
+ }
306
+
307
+ function _distributeMarketRewards(
308
+ LpPosition[] memory positions,
309
+ address poolAddress,
310
+ address currency,
311
+ address coin,
312
+ CoinConfig memory coinConfig
313
+ ) internal returns (ICoin.MarketRewards memory) {
314
+ uint256 totalAmountToken0;
315
+ uint256 totalAmountToken1;
316
+ uint256 amount0;
317
+ uint256 amount1;
318
+
319
+ for (uint256 i; i < positions.length; i++) {
320
+ // Must burn to update the collect mapping on the pool
321
+ IUniswapV3Pool(poolAddress).burn(positions[i].tickLower, positions[i].tickUpper, 0);
322
+
323
+ (amount0, amount1) = IUniswapV3Pool(poolAddress).collect(
324
+ address(this),
325
+ positions[i].tickLower,
326
+ positions[i].tickUpper,
327
+ type(uint128).max,
328
+ type(uint128).max
329
+ );
330
+
331
+ totalAmountToken0 += amount0;
332
+ totalAmountToken1 += amount1;
333
+ }
334
+
335
+ address token0 = currency < address(this) ? currency : address(this);
336
+ address token1 = currency < address(this) ? address(this) : currency;
337
+
338
+ ICoin.MarketRewards memory rewards;
339
+
340
+ rewards = _transferMarketRewards(token0, totalAmountToken0, rewards, coin, coinConfig);
341
+ rewards = _transferMarketRewards(token1, totalAmountToken1, rewards, coin, coinConfig);
342
+
343
+ emit ICoin.CoinMarketRewards(coinConfig.payoutRecipient, coinConfig.platformReferrer, coinConfig.protocolRewardRecipient, coinConfig.currency, rewards);
344
+
345
+ return rewards;
346
+ }
347
+
348
+ /// @dev Collects and distributes accrued fees from all LP positions
349
+ function handleMarketRewards(address coin, CoinConfig memory coinConfig) internal returns (ICoin.MarketRewards memory) {
350
+ address poolAddress = coinConfig.poolAddress;
351
+ address currency = coinConfig.currency;
352
+
353
+ bool isCoinToken0 = coin < currency;
354
+ LpPosition[] memory positions = calculatePositions(isCoinToken0, coinConfig.poolConfiguration);
355
+
356
+ return _distributeMarketRewards(positions, poolAddress, currency, coin, coinConfig);
357
+ }
358
+
359
+ function _transferMarketRewards(
360
+ address token,
361
+ uint256 totalAmount,
362
+ ICoin.MarketRewards memory rewards,
363
+ address coin,
364
+ CoinConfig memory coinConfig
365
+ ) internal returns (ICoin.MarketRewards memory) {
366
+ address payoutRecipient = coinConfig.payoutRecipient;
367
+ address platformReferrer = coinConfig.platformReferrer;
368
+ address protocolRewardRecipient = coinConfig.protocolRewardRecipient;
369
+ address currency = coinConfig.currency;
370
+ address weth = coinConfig.uniswapV3Config.weth;
371
+ address airlock = coinConfig.uniswapV3Config.airlock;
372
+ address protocolRewards = coinConfig.protocolRewards;
373
+
374
+ if (totalAmount > 0) {
375
+ address dopplerRecipient = IAirlock(airlock).owner();
376
+ uint256 dopplerPayout = _calculateReward(totalAmount, CoinConstants.DOPPLER_MARKET_REWARD_BPS);
377
+ uint256 creatorPayout = _calculateReward(totalAmount, CoinConstants.CREATOR_MARKET_REWARD_BPS);
378
+ uint256 platformReferrerPayout = _calculateReward(totalAmount, CoinConstants.PLATFORM_REFERRER_MARKET_REWARD_BPS);
379
+ uint256 protocolPayout = totalAmount - creatorPayout - platformReferrerPayout - dopplerPayout;
380
+
381
+ if (token == weth) {
382
+ IWETH(weth).withdraw(totalAmount);
383
+
384
+ rewards.totalAmountCurrency = totalAmount;
385
+ rewards.creatorPayoutAmountCurrency = creatorPayout;
386
+ rewards.platformReferrerAmountCurrency = platformReferrerPayout;
387
+ rewards.protocolAmountCurrency = protocolPayout;
388
+
389
+ address[] memory recipients = new address[](4);
390
+ recipients[0] = payoutRecipient;
391
+ recipients[1] = platformReferrer;
392
+ recipients[2] = protocolRewardRecipient;
393
+ recipients[3] = dopplerRecipient;
394
+
395
+ uint256[] memory amounts = new uint256[](4);
396
+ amounts[0] = rewards.creatorPayoutAmountCurrency;
397
+ amounts[1] = rewards.platformReferrerAmountCurrency;
398
+ amounts[2] = rewards.protocolAmountCurrency;
399
+ amounts[3] = dopplerPayout;
400
+
401
+ bytes4[] memory reasons = new bytes4[](4);
402
+ reasons[0] = bytes4(keccak256("COIN_CREATOR_MARKET_REWARD"));
403
+ reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_MARKET_REWARD"));
404
+ reasons[2] = bytes4(keccak256("COIN_PROTOCOL_MARKET_REWARD"));
405
+ reasons[3] = bytes4(keccak256("COIN_DOPPLER_MARKET_REWARD"));
406
+
407
+ IProtocolRewards(protocolRewards).depositBatch{value: totalAmount}(recipients, amounts, reasons, "");
408
+ IProtocolRewards(protocolRewards).withdrawFor(dopplerRecipient, dopplerPayout);
409
+ } else if (token == coin) {
410
+ rewards.totalAmountCoin = totalAmount;
411
+ rewards.creatorPayoutAmountCoin = creatorPayout;
412
+ rewards.platformReferrerAmountCoin = platformReferrerPayout;
413
+ rewards.protocolAmountCoin = protocolPayout;
414
+
415
+ IERC20(coin).safeTransfer(payoutRecipient, rewards.creatorPayoutAmountCoin);
416
+ IERC20(coin).safeTransfer(platformReferrer, rewards.platformReferrerAmountCoin);
417
+ IERC20(coin).safeTransfer(protocolRewardRecipient, rewards.protocolAmountCoin);
418
+ IERC20(coin).safeTransfer(dopplerRecipient, dopplerPayout);
419
+ } else {
420
+ rewards.totalAmountCurrency = totalAmount;
421
+ rewards.creatorPayoutAmountCurrency = creatorPayout;
422
+ rewards.platformReferrerAmountCurrency = platformReferrerPayout;
423
+ rewards.protocolAmountCurrency = protocolPayout;
424
+
425
+ IERC20(currency).safeTransfer(payoutRecipient, creatorPayout);
426
+ IERC20(currency).safeTransfer(platformReferrer, platformReferrerPayout);
427
+ IERC20(currency).safeTransfer(protocolRewardRecipient, protocolPayout);
428
+ IERC20(currency).safeTransfer(dopplerRecipient, dopplerPayout);
429
+ }
430
+ }
431
+
432
+ return rewards;
433
+ }
434
+
435
+ /// @dev Utility for computing amounts in basis points.
436
+ function _calculateReward(uint256 amount, uint256 bps) internal pure returns (uint256) {
437
+ return (amount * bps) / 10_000;
438
+ }
439
+
440
+ function calculatePositions(bool isCoinToken0, PoolConfiguration memory poolConfiguration) internal pure returns (LpPosition[] memory positions) {
441
+ if (poolConfiguration.version == CoinConfigurationVersions.LEGACY_POOL_VERSION) {
442
+ positions = CoinLegacyMarket.calculatePositions(isCoinToken0, poolConfiguration);
443
+ } else if (poolConfiguration.version == CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION) {
444
+ positions = CoinDopplerUniV3.calculatePositions(isCoinToken0, poolConfiguration);
445
+ } else {
446
+ revert InvalidPoolVersion();
447
+ }
448
+ }
449
+ }
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ library UniV3Errors {
5
+ error InvalidPoolAddress();
6
+ error InvalidCurrency();
7
+ error InvalidWeth();
8
+ error InvalidTickLower();
9
+ error InvalidTickUpper();
10
+ error InvalidUniswapV3Factory();
11
+ }
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
9
9
  contract ContractVersionBase is IVersionedContract {
10
10
  /// @notice The version of the contract
11
11
  function contractVersion() external pure override returns (string memory) {
12
- return "0.7.0";
12
+ return "0.9.0";
13
13
  }
14
14
  }
package/test/Coin.t.sol CHANGED
@@ -3,6 +3,8 @@ pragma solidity ^0.8.13;
3
3
 
4
4
  import "./utils/BaseTest.sol";
5
5
  import {ISwapRouter} from "../src/interfaces/ISwapRouter.sol";
6
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
7
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
6
8
 
7
9
  contract CoinTest is BaseTest {
8
10
  function setUp() public override {
@@ -12,19 +14,19 @@ contract CoinTest is BaseTest {
12
14
  }
13
15
 
14
16
  function test_contract_version() public view {
15
- assertEq(coin.contractVersion(), "0.7.0");
17
+ assertEq(coin.contractVersion(), "0.8.0");
16
18
  }
17
19
 
18
20
  function test_supply_constants() public view {
19
- assertEq(MAX_TOTAL_SUPPLY, POOL_LAUNCH_SUPPLY + CREATOR_LAUNCH_REWARD);
21
+ assertEq(CoinConstants.MAX_TOTAL_SUPPLY, CoinConstants.POOL_LAUNCH_SUPPLY + CoinConstants.CREATOR_LAUNCH_REWARD);
20
22
 
21
- assertEq(MAX_TOTAL_SUPPLY, 1_000_000_000e18);
22
- assertEq(POOL_LAUNCH_SUPPLY, 990_000_000e18);
23
- assertEq(CREATOR_LAUNCH_REWARD, 10_000_000e18);
23
+ assertEq(CoinConstants.MAX_TOTAL_SUPPLY, 1_000_000_000e18);
24
+ assertEq(CoinConstants.POOL_LAUNCH_SUPPLY, 990_000_000e18);
25
+ assertEq(CoinConstants.CREATOR_LAUNCH_REWARD, 10_000_000e18);
24
26
 
25
- assertEq(coin.totalSupply(), MAX_TOTAL_SUPPLY);
26
- assertEq(coin.balanceOf(coin.payoutRecipient()), CREATOR_LAUNCH_REWARD);
27
- assertApproxEqAbs(coin.balanceOf(address(pool)), POOL_LAUNCH_SUPPLY, 1e18);
27
+ assertEq(coin.totalSupply(), CoinConstants.MAX_TOTAL_SUPPLY);
28
+ assertEq(coin.balanceOf(coin.payoutRecipient()), CoinConstants.CREATOR_LAUNCH_REWARD);
29
+ assertApproxEqAbs(coin.balanceOf(address(pool)), CoinConstants.POOL_LAUNCH_SUPPLY, 1e18);
28
30
  }
29
31
 
30
32
  function test_constructor_validation() public {
@@ -91,6 +93,20 @@ contract CoinTest is BaseTest {
91
93
  assertEq(coin.symbol(), "INIT");
92
94
  }
93
95
 
96
+ function test_invalid_pool_config_version() public {
97
+ bytes memory poolConfig = abi.encode(0, address(weth));
98
+
99
+ vm.expectRevert(abi.encodeWithSignature("InvalidPoolVersion()"));
100
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
101
+ }
102
+
103
+ function test_invalid_pool_config_currency() public {
104
+ bytes memory poolConfig = abi.encode(CoinConfigurationVersions.LEGACY_POOL_VERSION);
105
+
106
+ vm.expectRevert();
107
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
108
+ }
109
+
94
110
  function test_revert_already_initialized() public {
95
111
  address[] memory owners = new address[](1);
96
112
  owners[0] = users.creator;
@@ -147,7 +163,7 @@ contract CoinTest is BaseTest {
147
163
  }
148
164
 
149
165
  function test_buy_with_eth_fuzz(uint256 ethOrderSize) public {
150
- vm.assume(ethOrderSize >= MIN_ORDER_SIZE);
166
+ vm.assume(ethOrderSize >= CoinConstants.MIN_ORDER_SIZE);
151
167
  vm.assume(ethOrderSize < 10 ether);
152
168
 
153
169
  uint256 platformReferrerBalanceBeforeSale = users.platformReferrer.balance;
@@ -168,11 +184,11 @@ contract CoinTest is BaseTest {
168
184
 
169
185
  function test_buy_with_eth_too_small() public {
170
186
  vm.expectRevert(abi.encodeWithSelector(ICoin.EthAmountTooSmall.selector));
171
- coin.buy{value: MIN_ORDER_SIZE - 1}(users.coinRecipient, MIN_ORDER_SIZE - 1, 0, 0, users.tradeReferrer);
187
+ coin.buy{value: CoinConstants.MIN_ORDER_SIZE - 1}(users.coinRecipient, CoinConstants.MIN_ORDER_SIZE - 1, 0, 0, users.tradeReferrer);
172
188
  }
173
189
 
174
190
  function test_buy_with_minimum_eth() public {
175
- uint256 minEth = MIN_ORDER_SIZE;
191
+ uint256 minEth = CoinConstants.MIN_ORDER_SIZE;
176
192
  vm.deal(users.buyer, minEth);
177
193
  vm.prank(users.buyer);
178
194
  coin.buy{value: minEth}(users.coinRecipient, minEth, 0, 0, users.tradeReferrer);
@@ -221,7 +237,7 @@ contract CoinTest is BaseTest {
221
237
  }
222
238
 
223
239
  function test_buy_validate_return_amounts(uint256 orderSize) public {
224
- vm.assume(orderSize >= MIN_ORDER_SIZE);
240
+ vm.assume(orderSize >= CoinConstants.MIN_ORDER_SIZE);
225
241
  vm.assume(orderSize < 10 ether);
226
242
 
227
243
  vm.deal(users.buyer, orderSize);
@@ -326,7 +342,7 @@ contract CoinTest is BaseTest {
326
342
 
327
343
  function test_sell_for_eth_fuzz(uint256 ethOrderSize) public {
328
344
  vm.assume(ethOrderSize < 10 ether);
329
- vm.assume(ethOrderSize >= MIN_ORDER_SIZE);
345
+ vm.assume(ethOrderSize >= CoinConstants.MIN_ORDER_SIZE);
330
346
 
331
347
  vm.deal(users.buyer, ethOrderSize);
332
348
  vm.prank(users.buyer);
@@ -396,14 +412,14 @@ contract CoinTest is BaseTest {
396
412
  coin.buy{value: 0.001 ether}(users.creator, 0.001 ether, 0, 0, users.tradeReferrer);
397
413
 
398
414
  uint256 beforeBalance = coin.balanceOf(users.creator);
399
- assertEq(beforeBalance, 11077349369032224007213331); // 11,077,349 coins
415
+ assertEq(beforeBalance, 11077349369032224007213331, "before balance"); // 11,077,349 coins
400
416
 
401
417
  vm.prank(users.creator);
402
418
  (uint256 amountSold, ) = coin.sell(users.creator, beforeBalance, 0, 0, users.tradeReferrer);
403
- assertEq(amountSold, 1088231685891135360821548); // 1,088,232 coins (max that could be sold)
419
+ assertEq(amountSold, 1088231685891135360821548, "amountSold"); // 1,088,232 coins (max that could be sold)
404
420
 
405
421
  uint256 afterBalance = coin.balanceOf(users.creator);
406
- assertEq(afterBalance, 9994558841570544323195890); // 9,994,559 coins
422
+ assertEq(afterBalance, 9994558841570544323195890, "after balance"); // 9,994,559 coins
407
423
 
408
424
  uint256 expectedMarketReward = 5441158429455676804107; // 5,441 coins
409
425
 
@@ -497,7 +513,9 @@ contract CoinTest is BaseTest {
497
513
  function test_eth_transfer_fail() public {
498
514
  vm.deal(users.buyer, 1 ether);
499
515
  vm.prank(users.buyer);
500
- coin.buy{value: 1 ether}(users.coinRecipient, 1 ether, 0, 0, users.tradeReferrer);
516
+ (, uint256 amountOut) = coin.buy{value: 1 ether}(users.coinRecipient, 1 ether, 0, 0, users.tradeReferrer);
517
+
518
+ assertEq(coin.balanceOf(users.coinRecipient), amountOut);
501
519
 
502
520
  // Recipient reverts on ETH receive
503
521
  address payable badRecipient = payable(makeAddr("badRecipient"));
@@ -5,6 +5,7 @@ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.s
5
5
  import {MarketConstants} from "../src/libs/MarketConstants.sol";
6
6
  import {BaseTest} from "./utils/BaseTest.sol";
7
7
  import {Coin} from "../src/Coin.sol";
8
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
8
9
  import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
9
10
  import {LpPosition} from "../src/types/LpPosition.sol";
10
11
  import {IDopplerErrors} from "../src/interfaces/IDopplerErrors.sol";
@@ -17,17 +18,6 @@ contract DopplerUniswapV3Test is BaseTest {
17
18
  uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
18
19
  uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
19
20
 
20
- function _generatePoolConfig(
21
- uint8 version_,
22
- address currency_,
23
- int24 tickLower_,
24
- int24 tickUpper_,
25
- uint16 numDiscoveryPositions_,
26
- uint256 maxDiscoverySupplyShare_
27
- ) internal pure returns (bytes memory) {
28
- return abi.encode(version_, currency_, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_);
29
- }
30
-
31
21
  function _deployCoin(bytes memory poolConfig_) internal {
32
22
  vm.prank(users.creator);
33
23
  (address coinAddress, ) = factory.deploy(
@@ -87,7 +77,7 @@ contract DopplerUniswapV3Test is BaseTest {
87
77
  assertTrue(isInitialized);
88
78
  assertFalse(isExited);
89
79
  assertEq(maxShareToBeSold, 0);
90
- assertEq(totalTokensOnBondingCurve, POOL_LAUNCH_SUPPLY);
80
+ assertEq(totalTokensOnBondingCurve, CoinConstants.POOL_LAUNCH_SUPPLY);
91
81
 
92
82
  bool isCoinToken0 = address(coin) < address(weth);
93
83
 
@@ -116,7 +106,7 @@ contract DopplerUniswapV3Test is BaseTest {
116
106
  }
117
107
 
118
108
  function test_deploy_legacy_eth_config_with_prebuy(uint256 initialOrderSize) public {
119
- vm.assume(initialOrderSize > MIN_ORDER_SIZE);
109
+ vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
120
110
  vm.assume(initialOrderSize < 10 ether);
121
111
 
122
112
  vm.deal(users.creator, initialOrderSize);
@@ -176,7 +166,7 @@ contract DopplerUniswapV3Test is BaseTest {
176
166
  vm.label(address(pool), "POOL");
177
167
 
178
168
  assertEq(coin.currency(), address(usdc), "currency");
179
- assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased);
169
+ assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased);
180
170
  }
181
171
 
182
172
  function test_deploy_doppler_eth() public {
@@ -209,11 +199,11 @@ contract DopplerUniswapV3Test is BaseTest {
209
199
  assertTrue(isInitialized, "poolState.isInitialized");
210
200
  assertFalse(isExited, "poolState.isExited");
211
201
  assertEq(maxShareToBeSold, DEFAULT_DISCOVERY_SUPPLY_SHARE, "poolState.maxShareToBeSold");
212
- assertEq(totalTokensOnBondingCurve, POOL_LAUNCH_SUPPLY, "poolState.totalTokensOnBondingCurve");
202
+ assertEq(totalTokensOnBondingCurve, CoinConstants.POOL_LAUNCH_SUPPLY, "poolState.totalTokensOnBondingCurve");
213
203
  }
214
204
 
215
205
  function test_deploy_doppler_eth_with_prebuy(uint256 initialOrderSize) public {
216
- vm.assume(initialOrderSize > MIN_ORDER_SIZE);
206
+ vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
217
207
  vm.assume(initialOrderSize < 1 ether);
218
208
 
219
209
  vm.deal(users.creator, initialOrderSize);
@@ -244,10 +234,17 @@ contract DopplerUniswapV3Test is BaseTest {
244
234
 
245
235
  assertEq(coin.currency(), address(weth), "currency");
246
236
  assertGt(coinsPurchased, 0, "coinsPurchased > 0");
247
- assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
237
+ assertEq(coin.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
248
238
  assertGt(weth.balanceOf(address(pool)), 0, "Pool WETH balance");
249
239
  }
250
240
 
241
+ function test_invalid_pool_config() public {
242
+ bytes memory poolConfig = _generatePoolConfig(CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION, address(weth), -100, 100, 0, 10);
243
+
244
+ vm.expectRevert(abi.encodeWithSignature("NumDiscoveryPositionsOutOfRange()"));
245
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
246
+ }
247
+
251
248
  function test_revert_deploy_invalid_discovery_supply_share() public {
252
249
  bytes memory poolConfig = _generatePoolConfig(
253
250
  CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,