@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
@@ -0,0 +1,452 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
5
+ import {MarketConstants} from "../src/libs/MarketConstants.sol";
6
+ import {BaseTest} from "./utils/BaseTest.sol";
7
+ import {Coin} from "../src/Coin.sol";
8
+ import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
9
+ import {LpPosition} from "../src/types/LpPosition.sol";
10
+ import {IDopplerErrors} from "../src/interfaces/IDopplerErrors.sol";
11
+ import {CoinDopplerUniV3} from "../src/libs/CoinDopplerUniV3.sol";
12
+ import {TickMath} from "../src/utils/uniswap/TickMath.sol";
13
+
14
+ contract DopplerUniswapV3Test is BaseTest {
15
+ int24 internal constant DEFAULT_DISCOVERY_TICK_LOWER = -777000;
16
+ int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
17
+ uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
18
+ uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
19
+
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
+ function _deployCoin(bytes memory poolConfig_) internal {
32
+ vm.prank(users.creator);
33
+ (address coinAddress, ) = factory.deploy(
34
+ users.creator,
35
+ _getDefaultOwners(),
36
+ "https://test.com",
37
+ "Testcoin",
38
+ "TEST",
39
+ poolConfig_,
40
+ users.platformReferrer,
41
+ 0
42
+ );
43
+
44
+ coin = Coin(payable(coinAddress));
45
+ pool = IUniswapV3Pool(coin.poolAddress());
46
+
47
+ vm.label(address(coin), "COIN");
48
+ vm.label(address(pool), "POOL");
49
+ }
50
+
51
+ function setUp() public override {
52
+ super.setUp();
53
+ }
54
+
55
+ function test_deploy_legacy_eth_config() public {
56
+ bytes memory poolConfig = _generatePoolConfig(
57
+ CoinConfigurationVersions.LEGACY_POOL_VERSION,
58
+ address(weth),
59
+ MarketConstants.LP_TICK_LOWER_WETH,
60
+ 0,
61
+ 1,
62
+ 0
63
+ );
64
+
65
+ _deployCoin(poolConfig);
66
+
67
+ assertEq(coin.currency(), address(weth), "currency");
68
+ assertEq(coin.totalSupply(), 1_000_000_000e18, "totalSupply");
69
+ assertEq(coin.balanceOf(users.creator), 10_000_000e18, "balanceOf creator");
70
+ assertGt(coin.balanceOf(coin.poolAddress()), 989_999_999e18, "balanceOf pool");
71
+
72
+ (
73
+ address asset,
74
+ address numeraire,
75
+ int24 tickLower,
76
+ int24 tickUpper,
77
+ uint16 numPositions,
78
+ bool isInitialized,
79
+ bool isExited,
80
+ uint256 maxShareToBeSold,
81
+ uint256 totalTokensOnBondingCurve
82
+ ) = coin.poolState();
83
+
84
+ assertEq(asset, address(coin));
85
+ assertEq(numeraire, address(weth));
86
+ assertEq(numPositions, 1);
87
+ assertTrue(isInitialized);
88
+ assertFalse(isExited);
89
+ assertEq(maxShareToBeSold, 0);
90
+ assertEq(totalTokensOnBondingCurve, POOL_LAUNCH_SUPPLY);
91
+
92
+ bool isCoinToken0 = address(coin) < address(weth);
93
+
94
+ if (isCoinToken0) {
95
+ assertEq(tickLower, MarketConstants.LP_TICK_LOWER_WETH);
96
+ assertEq(tickUpper, MarketConstants.LP_TICK_UPPER);
97
+ } else {
98
+ assertEq(tickLower, -MarketConstants.LP_TICK_UPPER);
99
+ assertEq(tickUpper, -MarketConstants.LP_TICK_LOWER_WETH);
100
+ }
101
+ }
102
+
103
+ function test_invalid_tick_range() public {
104
+ bytes memory poolConfig = _generatePoolConfig(CoinConfigurationVersions.LEGACY_POOL_VERSION, address(weth), -100, 100, 100, 0.5e18);
105
+
106
+ vm.expectRevert();
107
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
108
+ }
109
+
110
+ function test_inverted_tick_range_revert() public {
111
+ // These tick ranges are flipped
112
+ bytes memory poolConfig = _generatePoolConfig(CoinConfigurationVersions.LEGACY_POOL_VERSION, address(weth), 10000, -10000, 100, 0.5e18);
113
+
114
+ vm.expectRevert(abi.encodeWithSignature("InvalidWethLowerTick()"));
115
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
116
+ }
117
+
118
+ function test_deploy_legacy_eth_config_with_prebuy(uint256 initialOrderSize) public {
119
+ vm.assume(initialOrderSize > MIN_ORDER_SIZE);
120
+ vm.assume(initialOrderSize < 10 ether);
121
+
122
+ vm.deal(users.creator, initialOrderSize);
123
+
124
+ bytes memory poolConfig = _generatePoolConfig(
125
+ CoinConfigurationVersions.LEGACY_POOL_VERSION,
126
+ address(weth),
127
+ MarketConstants.LP_TICK_LOWER_WETH,
128
+ 0,
129
+ 0,
130
+ 0
131
+ );
132
+
133
+ vm.prank(users.creator);
134
+ (address coinAddress, ) = factory.deploy{value: initialOrderSize}(
135
+ users.creator,
136
+ _getDefaultOwners(),
137
+ "https://test.com",
138
+ "Testcoin",
139
+ "TEST",
140
+ poolConfig,
141
+ users.platformReferrer,
142
+ initialOrderSize
143
+ );
144
+
145
+ coin = Coin(payable(coinAddress));
146
+ pool = IUniswapV3Pool(coin.poolAddress());
147
+
148
+ assertGt(coin.balanceOf(users.creator), 10_000_000e18, "balanceOf creator");
149
+ }
150
+
151
+ function test_deploy_legacy_usdc_config_with_prebuy() public {
152
+ address[] memory owners = new address[](1);
153
+ owners[0] = users.creator;
154
+
155
+ uint256 orderSize = dealUSDC(users.creator, 100);
156
+
157
+ vm.prank(users.creator);
158
+ usdc.approve(address(factory), orderSize);
159
+
160
+ bytes memory poolConfig = _generatePoolConfig(CoinConfigurationVersions.LEGACY_POOL_VERSION, address(usdc), USDC_TICK_LOWER, 0, 0, 0);
161
+
162
+ vm.prank(users.creator);
163
+ (address coinAddress, uint256 coinsPurchased) = factory.deploy(
164
+ users.creator,
165
+ owners,
166
+ "https://testcoinusdcpair.com",
167
+ "Testcoinusdcpair",
168
+ "TESTCOINUSDCPAIR",
169
+ poolConfig,
170
+ users.platformReferrer,
171
+ orderSize
172
+ );
173
+ coin = Coin(payable(coinAddress));
174
+ pool = IUniswapV3Pool(coin.poolAddress());
175
+ vm.label(address(coin), "COIN");
176
+ vm.label(address(pool), "POOL");
177
+
178
+ assertEq(coin.currency(), address(usdc), "currency");
179
+ assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased);
180
+ }
181
+
182
+ function test_deploy_doppler_eth() public {
183
+ bytes memory poolConfig = _generatePoolConfig(
184
+ CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,
185
+ address(weth),
186
+ DEFAULT_DISCOVERY_TICK_LOWER,
187
+ DEFAULT_DISCOVERY_TICK_UPPER,
188
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
189
+ DEFAULT_DISCOVERY_SUPPLY_SHARE
190
+ );
191
+
192
+ _deployCoin(poolConfig);
193
+
194
+ (
195
+ address asset,
196
+ address numeraire,
197
+ ,
198
+ ,
199
+ uint16 numPositions,
200
+ bool isInitialized,
201
+ bool isExited,
202
+ uint256 maxShareToBeSold,
203
+ uint256 totalTokensOnBondingCurve
204
+ ) = coin.poolState();
205
+
206
+ assertEq(asset, address(coin), "poolState.asset");
207
+ assertEq(numeraire, address(weth), "poolState.numeraire");
208
+ assertEq(numPositions, DEFAULT_NUM_DISCOVERY_POSITIONS, "poolState.numPositions");
209
+ assertTrue(isInitialized, "poolState.isInitialized");
210
+ assertFalse(isExited, "poolState.isExited");
211
+ assertEq(maxShareToBeSold, DEFAULT_DISCOVERY_SUPPLY_SHARE, "poolState.maxShareToBeSold");
212
+ assertEq(totalTokensOnBondingCurve, POOL_LAUNCH_SUPPLY, "poolState.totalTokensOnBondingCurve");
213
+ }
214
+
215
+ function test_deploy_doppler_eth_with_prebuy(uint256 initialOrderSize) public {
216
+ vm.assume(initialOrderSize > MIN_ORDER_SIZE);
217
+ vm.assume(initialOrderSize < 1 ether);
218
+
219
+ vm.deal(users.creator, initialOrderSize);
220
+
221
+ bytes memory poolConfig = _generatePoolConfig(
222
+ CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,
223
+ address(weth),
224
+ DEFAULT_DISCOVERY_TICK_LOWER,
225
+ DEFAULT_DISCOVERY_TICK_UPPER,
226
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
227
+ DEFAULT_DISCOVERY_SUPPLY_SHARE
228
+ );
229
+
230
+ vm.prank(users.creator);
231
+ (address coinAddress, uint256 coinsPurchased) = factory.deploy{value: initialOrderSize}(
232
+ users.creator,
233
+ _getDefaultOwners(),
234
+ "https://test.com",
235
+ "Testcoin",
236
+ "TEST",
237
+ poolConfig,
238
+ users.platformReferrer,
239
+ initialOrderSize
240
+ );
241
+
242
+ coin = Coin(payable(coinAddress));
243
+ pool = IUniswapV3Pool(coin.poolAddress());
244
+
245
+ assertEq(coin.currency(), address(weth), "currency");
246
+ assertGt(coinsPurchased, 0, "coinsPurchased > 0");
247
+ assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
248
+ assertGt(weth.balanceOf(address(pool)), 0, "Pool WETH balance");
249
+ }
250
+
251
+ function test_revert_deploy_invalid_discovery_supply_share() public {
252
+ bytes memory poolConfig = _generatePoolConfig(
253
+ CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,
254
+ address(weth),
255
+ DEFAULT_DISCOVERY_TICK_LOWER,
256
+ DEFAULT_DISCOVERY_TICK_UPPER,
257
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
258
+ 1.1e18
259
+ );
260
+
261
+ vm.expectRevert(abi.encodeWithSelector(IDopplerErrors.MaxShareToBeSoldExceeded.selector, 1.1e18, 1e18));
262
+ factory.deploy(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, users.platformReferrer, 0);
263
+ }
264
+
265
+ function test_alignTick_isToken0_positive() public pure {
266
+ int24 tick = 12345;
267
+ int24 TICK_SPACING = 60;
268
+ int24 expected = 12300;
269
+
270
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(true, tick, TICK_SPACING), expected, "Align positive tick (token0)");
271
+ }
272
+
273
+ function test_alignTick_isToken0_negative() public pure {
274
+ int24 tick = -12345;
275
+ int24 TICK_SPACING = 60;
276
+ int24 expected = -12360;
277
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(true, tick, TICK_SPACING), expected, "Align negative tick (token0)");
278
+ }
279
+
280
+ function test_alignTick_isToken1_negative() public pure {
281
+ int24 tick = -12345;
282
+ int24 TICK_SPACING = 60;
283
+ int24 expected = -12300;
284
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(false, tick, TICK_SPACING), expected, "Align negative tick (token1)");
285
+ }
286
+
287
+ function test_alignTick_isToken1_zero() public pure {
288
+ int24 tick = 0;
289
+ int24 expected = 0;
290
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(false, tick, MarketConstants.TICK_SPACING), expected, "Align zero tick (token1)");
291
+ }
292
+
293
+ // Additional tick alignment test for full branch coverage
294
+ function test_alignTick_isToken0_zero() public pure {
295
+ int24 tick = 0;
296
+ int24 expected = 0;
297
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(true, tick, MarketConstants.TICK_SPACING), expected, "Align zero tick (token0)");
298
+ }
299
+
300
+ function test_alignTick_isToken1_positive() public pure {
301
+ int24 tick = 12345;
302
+ int24 expected = 12400; // Round up for token1
303
+ assertEq(CoinDopplerUniV3.alignTickToTickSpacing(false, tick, MarketConstants.TICK_SPACING), expected, "Align positive tick (token1)");
304
+ }
305
+
306
+ function test_calculateLpTail_isToken0() public pure {
307
+ int24 tickLower = DEFAULT_DISCOVERY_TICK_LOWER;
308
+ int24 tickUpper = DEFAULT_DISCOVERY_TICK_UPPER;
309
+ uint256 tailSupply = 1e18;
310
+ bool isToken0 = true;
311
+
312
+ LpPosition memory tail = CoinDopplerUniV3.calculateLpTail(tickLower, tickUpper, isToken0, tailSupply, MarketConstants.TICK_SPACING);
313
+
314
+ int24 expectedPosTickLower = tickUpper;
315
+ int24 expectedPosTickUpper = CoinDopplerUniV3.alignTickToTickSpacing(true, TickMath.MAX_TICK, MarketConstants.TICK_SPACING);
316
+
317
+ assertEq(tail.tickLower, expectedPosTickLower, "Tail tickLower (token0)");
318
+ assertEq(tail.tickUpper, expectedPosTickUpper, "Tail tickUpper (token0)");
319
+ assertGt(tail.liquidity, 0, "Tail liquidity should be > 0 (token0)");
320
+ }
321
+
322
+ function test_calculateLpTail_isToken1() public pure {
323
+ int24 tickLower = DEFAULT_DISCOVERY_TICK_LOWER;
324
+ int24 tickUpper = DEFAULT_DISCOVERY_TICK_UPPER;
325
+ uint256 tailSupply = 1e18;
326
+ bool isToken0 = false;
327
+
328
+ LpPosition memory tail = CoinDopplerUniV3.calculateLpTail(tickLower, tickUpper, isToken0, tailSupply, MarketConstants.TICK_SPACING);
329
+
330
+ int24 expectedPosTickLower = CoinDopplerUniV3.alignTickToTickSpacing(false, TickMath.MIN_TICK, MarketConstants.TICK_SPACING);
331
+ int24 expectedPosTickUpper = tickLower;
332
+
333
+ assertEq(tail.tickLower, expectedPosTickLower, "Tail tickLower (token1)");
334
+ assertEq(tail.tickUpper, expectedPosTickUpper, "Tail tickUpper (token1)");
335
+ assertGt(tail.liquidity, 0, "Tail liquidity should be > 0 (token1)");
336
+ }
337
+
338
+ function test_calculateLogNormalDistribution_isToken0() public pure {
339
+ int24 tickLower = -60000;
340
+ int24 tickUpper = 0;
341
+ bool isToken0 = true;
342
+ uint256 discoverySupply = 100e18;
343
+ LpPosition[] memory newPositions = new LpPosition[](DEFAULT_NUM_DISCOVERY_POSITIONS);
344
+
345
+ (LpPosition[] memory positions, uint256 totalAssetsSold) = CoinDopplerUniV3.calculateLogNormalDistribution(
346
+ tickLower,
347
+ tickUpper,
348
+ MarketConstants.TICK_SPACING,
349
+ isToken0,
350
+ discoverySupply,
351
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
352
+ newPositions
353
+ );
354
+
355
+ assertEq(positions.length, DEFAULT_NUM_DISCOVERY_POSITIONS, "Correct number of positions (token0)");
356
+ assertTrue(totalAssetsSold <= discoverySupply, "Total assets sold <= discovery supply (token0)");
357
+ assertTrue(totalAssetsSold > 0, "Total assets sold > 0 (token0)");
358
+
359
+ int24 expectedFarTick = tickUpper; // 0
360
+ for (uint i = 0; i < DEFAULT_NUM_DISCOVERY_POSITIONS; i++) {
361
+ assertTrue(positions[i].liquidity > 0, "Position liquidity > 0 (token0)");
362
+ assertTrue(positions[i].tickLower <= positions[i].tickUpper, "Tick order check (token0)");
363
+ assertEq(positions[i].tickLower % MarketConstants.TICK_SPACING, 0, "Lower tick alignment (token0)");
364
+ assertEq(positions[i].tickUpper % MarketConstants.TICK_SPACING, 0, "Upper tick alignment (token0)");
365
+ assertEq(positions[i].tickUpper, expectedFarTick, "Far tick check (token0)");
366
+ }
367
+ }
368
+
369
+ function test_calculateLogNormalDistribution_isToken1() public pure {
370
+ int24 tickLower = -60000;
371
+ int24 tickUpper = 0;
372
+ bool isToken0 = false;
373
+ uint256 discoverySupply = 100e18;
374
+ LpPosition[] memory newPositions = new LpPosition[](DEFAULT_NUM_DISCOVERY_POSITIONS);
375
+
376
+ (LpPosition[] memory positions, uint256 totalAssetsSold) = CoinDopplerUniV3.calculateLogNormalDistribution(
377
+ tickLower,
378
+ tickUpper,
379
+ MarketConstants.TICK_SPACING,
380
+ isToken0,
381
+ discoverySupply,
382
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
383
+ newPositions
384
+ );
385
+
386
+ assertEq(positions.length, DEFAULT_NUM_DISCOVERY_POSITIONS, "Correct number of positions (token1)");
387
+ assertTrue(totalAssetsSold <= discoverySupply, "Total assets sold <= discovery supply (token1)");
388
+ assertTrue(totalAssetsSold > 0, "Total assets sold > 0 (token1)");
389
+
390
+ int24 expectedFarTick = tickLower; // -60000
391
+ for (uint i = 0; i < DEFAULT_NUM_DISCOVERY_POSITIONS; i++) {
392
+ assertTrue(positions[i].liquidity > 0, "Position liquidity > 0 (token1)");
393
+ assertTrue(positions[i].tickLower <= positions[i].tickUpper, "Tick order check (token1)");
394
+ assertEq(positions[i].tickLower % MarketConstants.TICK_SPACING, 0, "Lower tick alignment (token1)");
395
+ assertEq(positions[i].tickUpper % MarketConstants.TICK_SPACING, 0, "Upper tick alignment (token1)");
396
+ assertEq(positions[i].tickLower, expectedFarTick, "Far tick check (token1)");
397
+ }
398
+ }
399
+
400
+ function test_calculateLogNormalDistribution_zeroDiscoverySupply() public pure {
401
+ int24 tickLower = -60000;
402
+ int24 tickUpper = 0;
403
+ bool isToken0 = true;
404
+ uint256 discoverySupply = 0;
405
+ LpPosition[] memory newPositions = new LpPosition[](DEFAULT_NUM_DISCOVERY_POSITIONS);
406
+
407
+ (LpPosition[] memory positions, uint256 totalAssetsSold) = CoinDopplerUniV3.calculateLogNormalDistribution(
408
+ tickLower,
409
+ tickUpper,
410
+ MarketConstants.TICK_SPACING,
411
+ isToken0,
412
+ discoverySupply,
413
+ DEFAULT_NUM_DISCOVERY_POSITIONS,
414
+ newPositions
415
+ );
416
+
417
+ assertEq(positions.length, DEFAULT_NUM_DISCOVERY_POSITIONS, "Correct number of positions (zero supply)");
418
+ assertEq(totalAssetsSold, 0, "Total assets sold is 0 (zero supply)");
419
+
420
+ for (uint i = 0; i < DEFAULT_NUM_DISCOVERY_POSITIONS; i++) {
421
+ assertEq(positions[i].liquidity, 0, "Position liquidity is 0 (zero supply)");
422
+ assertTrue(positions[i].tickLower <= positions[i].tickUpper, "Tick order check (zero supply)");
423
+ assertEq(positions[i].tickLower % MarketConstants.TICK_SPACING, 0, "Lower tick alignment (zero supply)");
424
+ assertEq(positions[i].tickUpper % MarketConstants.TICK_SPACING, 0, "Upper tick alignment (zero supply)");
425
+ }
426
+ }
427
+
428
+ function test_calculateLogNormalDistribution_startingTickEqualsFarTick() public pure {
429
+ // Will force startingTick == farTick
430
+ int24 tickLower = 0;
431
+ int24 tickUpper = 0;
432
+
433
+ bool isToken0 = true;
434
+ uint256 discoverySupply = 100e18;
435
+ uint16 totalPositions = 1;
436
+ LpPosition[] memory newPositions = new LpPosition[](totalPositions);
437
+
438
+ (LpPosition[] memory positions, uint256 totalAssetsSold) = CoinDopplerUniV3.calculateLogNormalDistribution(
439
+ tickLower,
440
+ tickUpper,
441
+ MarketConstants.TICK_SPACING,
442
+ isToken0,
443
+ discoverySupply,
444
+ totalPositions,
445
+ newPositions
446
+ );
447
+
448
+ assertEq(positions.length, totalPositions, "Correct number of positions");
449
+ assertEq(totalAssetsSold, 0, "No assets sold when startingTick equals farTick");
450
+ assertEq(positions[0].liquidity, 0, "Position should have zero liquidity");
451
+ }
452
+ }
@@ -25,7 +25,7 @@ contract FactoryTest is BaseTest {
25
25
  "TEST2",
26
26
  users.platformReferrer,
27
27
  address(weth),
28
- LP_TICK_LOWER_WETH,
28
+ MarketConstants.LP_TICK_LOWER_WETH,
29
29
  0
30
30
  );
31
31
  coin = Coin(payable(coinAddress));
@@ -74,7 +74,7 @@ contract FactoryTest is BaseTest {
74
74
  "TEST2",
75
75
  users.platformReferrer,
76
76
  address(weth),
77
- LP_TICK_LOWER_WETH,
77
+ MarketConstants.LP_TICK_LOWER_WETH,
78
78
  initialOrderSize
79
79
  );
80
80
  coin = Coin(payable(coinAddress));
@@ -120,7 +120,7 @@ contract FactoryTest is BaseTest {
120
120
  "TEST2",
121
121
  users.platformReferrer,
122
122
  address(weth),
123
- LP_TICK_LOWER_WETH,
123
+ MarketConstants.LP_TICK_LOWER_WETH,
124
124
  initialOrderSize
125
125
  );
126
126
  }
@@ -140,7 +140,7 @@ contract FactoryTest is BaseTest {
140
140
  "TEST2",
141
141
  users.platformReferrer,
142
142
  address(weth),
143
- LP_TICK_LOWER_WETH,
143
+ MarketConstants.LP_TICK_LOWER_WETH,
144
144
  orderSize
145
145
  );
146
146
  coin = Coin(payable(coinAddress));
@@ -173,6 +173,39 @@ contract FactoryTest is BaseTest {
173
173
  assertEq(coin.payoutRecipient(), users.creator, "payoutRecipient");
174
174
  }
175
175
 
176
+ function test_deploy_with_usdc_order() public {
177
+ address[] memory owners = new address[](1);
178
+ owners[0] = users.creator;
179
+
180
+ uint256 orderSize = dealUSDC(users.creator, 100);
181
+
182
+ vm.prank(users.creator);
183
+ usdc.approve(address(factory), orderSize);
184
+
185
+ assertEq(usdc.balanceOf(users.creator), orderSize);
186
+ assertEq(usdc.allowance(users.creator, address(factory)), orderSize);
187
+
188
+ vm.prank(users.creator);
189
+ (address coinAddress, uint256 coinsPurchased) = factory.deploy(
190
+ users.creator,
191
+ owners,
192
+ "https://testcoinusdcpair.com",
193
+ "Testcoinusdcpair",
194
+ "TESTCOINUSDCPAIR",
195
+ users.platformReferrer,
196
+ USDC_ADDRESS,
197
+ USDC_TICK_LOWER,
198
+ orderSize
199
+ );
200
+ coin = Coin(payable(coinAddress));
201
+ pool = IUniswapV3Pool(coin.poolAddress());
202
+ vm.label(address(coin), "COIN");
203
+ vm.label(address(pool), "POOL");
204
+
205
+ assertEq(coin.currency(), USDC_ADDRESS, "currency");
206
+ assertEq(coin.balanceOf(users.creator), CREATOR_LAUNCH_REWARD + coinsPurchased);
207
+ }
208
+
176
209
  function test_deploy_with_usdc_revert_payout_recipient_zero() public {
177
210
  address[] memory owners = new address[](1);
178
211
  owners[0] = users.creator;
@@ -234,8 +267,17 @@ contract FactoryTest is BaseTest {
234
267
  owners[0] = users.creator;
235
268
 
236
269
  vm.expectRevert(abi.encodeWithSelector(ICoin.InvalidWethLowerTick.selector));
237
-
238
- factory.deploy(users.creator, owners, "https://testcoin.com", "Testcoin", "TESTCOIN", users.platformReferrer, address(0), LP_TICK_LOWER_WETH - 1, 0);
270
+ factory.deploy(
271
+ users.creator,
272
+ owners,
273
+ "https://testcoin.com",
274
+ "Testcoin",
275
+ "TESTCOIN",
276
+ users.platformReferrer,
277
+ address(0),
278
+ MarketConstants.LP_TICK_LOWER_WETH + 1,
279
+ 0
280
+ );
239
281
  }
240
282
 
241
283
  function test_deploy_with_usdc_revert_invalid_eth_transfer() public {
@@ -274,7 +316,7 @@ contract FactoryTest is BaseTest {
274
316
  "TEST",
275
317
  users.platformReferrer,
276
318
  address(weth),
277
- LP_TICK_LOWER_WETH,
319
+ MarketConstants.LP_TICK_LOWER_WETH,
278
320
  0
279
321
  );
280
322
  coin = Coin(payable(coinAddress));
@@ -17,19 +17,23 @@ import {MultiOwnable} from "../../src/utils/MultiOwnable.sol";
17
17
  import {ICoin} from "../../src/interfaces/ICoin.sol";
18
18
  import {IERC7572} from "../../src/interfaces/IERC7572.sol";
19
19
  import {IWETH} from "../../src/interfaces/IWETH.sol";
20
+ import {IAirlock} from "../../src/interfaces/IAirlock.sol";
20
21
  import {INonfungiblePositionManager} from "../../src/interfaces/INonfungiblePositionManager.sol";
21
22
  import {ISwapRouter} from "../../src/interfaces/ISwapRouter.sol";
23
+ import {IUniswapV3Factory} from "../../src/interfaces/IUniswapV3Factory.sol";
22
24
  import {IUniswapV3Pool} from "../../src/interfaces/IUniswapV3Pool.sol";
23
25
  import {IProtocolRewards} from "../../src/interfaces/IProtocolRewards.sol";
24
26
  import {ProtocolRewards} from "../utils/ProtocolRewards.sol";
25
-
27
+ import {MarketConstants} from "../../src/libs/MarketConstants.sol";
26
28
  contract BaseTest is Test, CoinConstants {
27
29
  using stdStorage for StdStorage;
28
30
 
29
31
  address internal constant PROTOCOL_REWARDS = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
30
32
  address internal constant WETH_ADDRESS = 0x4200000000000000000000000000000000000006;
33
+ address internal constant V3_FACTORY = 0x33128a8fC17869897dcE68Ed026d694621f6FDfD;
31
34
  address internal constant NONFUNGIBLE_POSITION_MANAGER = 0x03a520b32C04BF3bEEf7BEb72E919cf822Ed34f1;
32
35
  address internal constant SWAP_ROUTER = 0x2626664c2603336E57B271c5C0b26F421741e481;
36
+ address internal constant DOPPLER_AIRLOCK = 0x660eAaEdEBc968f8f3694354FA8EC0b4c5Ba8D12;
33
37
  address internal constant USDC_ADDRESS = 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913;
34
38
  int24 internal constant USDC_TICK_LOWER = 57200;
35
39
 
@@ -48,8 +52,10 @@ contract BaseTest is Test, CoinConstants {
48
52
  IERC20Metadata internal usdc;
49
53
  IWETH internal weth;
50
54
  ProtocolRewards internal protocolRewards;
55
+ IUniswapV3Factory internal v3Factory;
51
56
  INonfungiblePositionManager internal nonfungiblePositionManager;
52
57
  ISwapRouter internal swapRouter;
58
+ IAirlock internal airlock;
53
59
  Users internal users;
54
60
 
55
61
  Coin internal coinImpl;
@@ -59,12 +65,14 @@ contract BaseTest is Test, CoinConstants {
59
65
  IUniswapV3Pool internal pool;
60
66
 
61
67
  function setUp() public virtual {
62
- forkId = vm.createSelectFork("base", 21179722);
68
+ forkId = vm.createSelectFork("base", 28415528);
63
69
 
64
70
  weth = IWETH(WETH_ADDRESS);
65
71
  usdc = IERC20Metadata(USDC_ADDRESS);
72
+ v3Factory = IUniswapV3Factory(V3_FACTORY);
66
73
  nonfungiblePositionManager = INonfungiblePositionManager(NONFUNGIBLE_POSITION_MANAGER);
67
74
  swapRouter = ISwapRouter(SWAP_ROUTER);
75
+ airlock = IAirlock(DOPPLER_AIRLOCK);
68
76
  protocolRewards = new ProtocolRewards();
69
77
 
70
78
  users = Users({
@@ -78,7 +86,7 @@ contract BaseTest is Test, CoinConstants {
78
86
  tradeReferrer: makeAddr("tradeReferrer")
79
87
  });
80
88
 
81
- coinImpl = new Coin(users.feeRecipient, address(protocolRewards), WETH_ADDRESS, NONFUNGIBLE_POSITION_MANAGER, SWAP_ROUTER);
89
+ coinImpl = new Coin(users.feeRecipient, address(protocolRewards), WETH_ADDRESS, V3_FACTORY, SWAP_ROUTER, DOPPLER_AIRLOCK);
82
90
  factoryImpl = new ZoraFactoryImpl(address(coinImpl));
83
91
  factory = ZoraFactoryImpl(address(new ZoraFactory(address(factoryImpl))));
84
92
 
@@ -87,9 +95,11 @@ contract BaseTest is Test, CoinConstants {
87
95
  vm.label(address(factory), "ZORA_FACTORY");
88
96
  vm.label(address(protocolRewards), "PROTOCOL_REWARDS");
89
97
  vm.label(address(nonfungiblePositionManager), "NONFUNGIBLE_POSITION_MANAGER");
98
+ vm.label(address(v3Factory), "V3_FACTORY");
90
99
  vm.label(address(swapRouter), "SWAP_ROUTER");
91
100
  vm.label(address(weth), "WETH");
92
101
  vm.label(address(usdc), "USDC");
102
+ vm.label(address(airlock), "AIRLOCK");
93
103
  }
94
104
 
95
105
  struct TradeRewards {
@@ -102,6 +112,7 @@ contract BaseTest is Test, CoinConstants {
102
112
  struct MarketRewards {
103
113
  uint256 creator;
104
114
  uint256 platformReferrer;
115
+ uint256 doppler;
105
116
  uint256 protocol;
106
117
  }
107
118
 
@@ -118,7 +129,7 @@ contract BaseTest is Test, CoinConstants {
118
129
  "TEST",
119
130
  users.platformReferrer,
120
131
  address(weth),
121
- LP_TICK_LOWER_WETH,
132
+ MarketConstants.LP_TICK_LOWER_WETH,
122
133
  0
123
134
  );
124
135
 
@@ -171,9 +182,10 @@ contract BaseTest is Test, CoinConstants {
171
182
  function _calculateMarketRewards(uint256 ethAmount) internal pure returns (MarketRewards memory) {
172
183
  uint256 creator = (ethAmount * 5000) / 10_000;
173
184
  uint256 platformReferrer = (ethAmount * 2500) / 10_000;
174
- uint256 protocol = ethAmount - creator - platformReferrer;
185
+ uint256 doppler = (ethAmount * 500) / 10_000;
186
+ uint256 protocol = ethAmount - creator - platformReferrer - doppler;
175
187
 
176
- return MarketRewards({creator: creator, platformReferrer: platformReferrer, protocol: protocol});
188
+ return MarketRewards({creator: creator, platformReferrer: platformReferrer, doppler: doppler, protocol: protocol});
177
189
  }
178
190
 
179
191
  function dealUSDC(address to, uint256 numUSDC) internal returns (uint256) {
@@ -182,5 +194,12 @@ contract BaseTest is Test, CoinConstants {
182
194
  return amount;
183
195
  }
184
196
 
185
- function uniswapV3SwapCallback(int256 amount0Delta, int256 amount1Delta, bytes calldata data) external {}
197
+ function _getDefaultOwners() internal view returns (address[] memory owners) {
198
+ owners = new address[](1);
199
+ owners[0] = users.creator;
200
+ }
201
+
202
+ function dopplerFeeRecipient() internal view returns (address) {
203
+ return airlock.owner();
204
+ }
186
205
  }
package/wagmi.config.ts CHANGED
@@ -8,7 +8,7 @@ export default defineConfig({
8
8
  deployments: {
9
9
  ZoraFactoryImpl: {
10
10
  84532: "0x777777751622c0d3258f214F9DF38E35BF45baF3",
11
- 8453: "0x777777751622c0d3258f214F9DF38E35BF45baF3",
11
+ 8453: "0x02B2705500096Ff83F9eF78873ca5DFB06C00Ddc",
12
12
  },
13
13
  },
14
14
  forge: {