@zoralabs/coins 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/.turbo/turbo-build$colon$js.log +99 -99
  2. package/CHANGELOG.md +44 -5
  3. package/README.md +4 -0
  4. package/abis/BaseCoin.json +0 -5
  5. package/abis/ContentCoin.json +0 -5
  6. package/abis/CreatorCoin.json +0 -5
  7. package/abis/FeeEstimatorHook.json +94 -1
  8. package/abis/IUpgradeableDestinationV4HookWithUpdateableFee.json +95 -0
  9. package/abis/IZoraFactory.json +69 -0
  10. package/abis/ZoraFactoryImpl.json +69 -0
  11. package/abis/ZoraV4CoinHook.json +94 -1
  12. package/addresses/8453.json +6 -6
  13. package/audits/report-cantinacode-zora-0827.pdf +3498 -4
  14. package/dist/index.cjs +21 -3
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.js +21 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/wagmiGenerated.d.ts +54 -12
  19. package/dist/wagmiGenerated.d.ts.map +1 -1
  20. package/foundry.toml +3 -3
  21. package/package/wagmiGenerated.ts +21 -3
  22. package/package.json +1 -1
  23. package/script/TestBackingCoinSwap.s.sol +0 -2
  24. package/script/TestV4Swap.s.sol +0 -2
  25. package/src/BaseCoin.sol +4 -12
  26. package/src/ContentCoin.sol +3 -4
  27. package/src/CreatorCoin.sol +8 -10
  28. package/src/ZoraFactoryImpl.sol +115 -83
  29. package/src/hook-registry/ZoraHookRegistry.sol +4 -0
  30. package/src/hooks/ZoraV4CoinHook.sol +66 -9
  31. package/src/interfaces/IUpgradeableV4Hook.sol +18 -0
  32. package/src/interfaces/IZoraFactory.sol +21 -2
  33. package/src/libs/CoinConstants.sol +51 -8
  34. package/src/libs/CoinDopplerMultiCurve.sol +11 -11
  35. package/src/libs/CoinRewardsV4.sol +26 -33
  36. package/src/libs/CoinSetup.sol +2 -9
  37. package/src/libs/DopplerMath.sol +2 -2
  38. package/src/libs/V4Liquidity.sol +79 -15
  39. package/src/version/ContractVersionBase.sol +1 -1
  40. package/test/Coin.t.sol +5 -5
  41. package/test/CoinRewardsV4.t.sol +33 -0
  42. package/test/CoinUniV4.t.sol +2 -3
  43. package/test/ContentCoinRewards.t.sol +43 -0
  44. package/test/CreatorCoin.t.sol +53 -29
  45. package/test/DeploymentHooks.t.sol +54 -2
  46. package/test/LiquidityMigration.t.sol +145 -7
  47. package/test/V4Liquidity.t.sol +178 -0
  48. package/test/utils/BaseTest.sol +0 -1
  49. package/test/utils/RewardTestHelpers.sol +4 -4
  50. package/abis/CoinConstants.json +0 -54
  51. package/abis/CoinRewardsV4.json +0 -67
  52. package/src/libs/CreatorCoinConstants.sol +0 -15
  53. package/src/libs/MarketConstants.sol +0 -23
  54. /package/abis/{VmContractHelper227.json → VmContractHelper226.json} +0 -0
@@ -28,8 +28,9 @@ import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
28
28
  import {Position} from "@uniswap/v4-core/src/libraries/Position.sol";
29
29
  import {BurnedPosition, Delta, MigratedLiquidityResult, IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
30
30
  import {PoolStateReader} from "../libs/PoolStateReader.sol";
31
- import {IUpgradeableDestinationV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
31
+ import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
32
32
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
33
+ import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
33
34
 
34
35
  // command = 1; mint
35
36
  struct MintCallbackData {
@@ -71,7 +72,7 @@ library V4Liquidity {
71
72
  address coin,
72
73
  address newHook,
73
74
  bytes calldata additionalData
74
- ) internal returns (PoolKey memory) {
75
+ ) internal returns (PoolKey memory newPoolKey) {
75
76
  bytes memory data = abi.encode(
76
77
  BURN_ALL_POSITIONS_CALLBACK_ID,
77
78
  abi.encode(BurnAllPositionsCallbackData({poolKey: poolKey, positions: positions, coin: coin, newHook: newHook}))
@@ -82,19 +83,36 @@ library V4Liquidity {
82
83
 
83
84
  MigratedLiquidityResult memory migratedLiquidityResult = abi.decode(result, (MigratedLiquidityResult));
84
85
 
85
- // Check if new hook supports the upgradeable destination interface
86
- require(IERC165(newHook).supportsInterface(type(IUpgradeableDestinationV4Hook).interfaceId), IUpgradeableV4Hook.InvalidNewHook(newHook));
87
- // Initialize new hook with migration data
88
- IUpgradeableDestinationV4Hook(address(newHook)).initializeFromMigration(
89
- poolKey,
90
- coin,
91
- migratedLiquidityResult.sqrtPriceX96,
92
- migratedLiquidityResult.burnedPositions,
93
- additionalData
94
- );
95
-
96
- return
97
- PoolKey({currency0: poolKey.currency0, currency1: poolKey.currency1, fee: poolKey.fee, tickSpacing: poolKey.tickSpacing, hooks: IHooks(newHook)});
86
+ newPoolKey.currency0 = poolKey.currency0;
87
+ newPoolKey.currency1 = poolKey.currency1;
88
+ newPoolKey.hooks = IHooks(newHook);
89
+
90
+ // Check if new hook supports the new interface first, then fall back to old interface
91
+ if (IERC165(newHook).supportsInterface(type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId)) {
92
+ // Use new interface with fee updates
93
+ (uint24 fee, int24 tickSpacing) = IUpgradeableDestinationV4HookWithUpdateableFee(address(newHook)).initializeFromMigrationWithUpdateableFee(
94
+ poolKey,
95
+ coin,
96
+ migratedLiquidityResult.sqrtPriceX96,
97
+ migratedLiquidityResult.burnedPositions,
98
+ additionalData
99
+ );
100
+ newPoolKey.fee = fee;
101
+ newPoolKey.tickSpacing = tickSpacing;
102
+ } else {
103
+ // Fall back to old interface for backward compatibility
104
+ require(IERC165(newHook).supportsInterface(type(IUpgradeableDestinationV4Hook).interfaceId), IUpgradeableV4Hook.InvalidNewHook(newHook));
105
+ IUpgradeableDestinationV4Hook(address(newHook)).initializeFromMigration(
106
+ poolKey,
107
+ coin,
108
+ migratedLiquidityResult.sqrtPriceX96,
109
+ migratedLiquidityResult.burnedPositions,
110
+ additionalData
111
+ );
112
+ // Keep existing fee and tick spacing when using old interface
113
+ newPoolKey.fee = poolKey.fee;
114
+ newPoolKey.tickSpacing = poolKey.tickSpacing;
115
+ }
98
116
  }
99
117
 
100
118
  /// @notice Handles the callback from the pool manager. Called by the hook upon unlock.
@@ -138,6 +156,41 @@ library V4Liquidity {
138
156
  return abi.encode(result);
139
157
  }
140
158
 
159
+ function dedupePositions(LpPosition[] memory positions) internal pure returns (LpPosition[] memory dedupedPositions) {
160
+ // Upper bound: no more than input length
161
+ dedupedPositions = new LpPosition[](positions.length);
162
+ uint outLen = 0;
163
+
164
+ // O(n²) approach: for each position, check if it already exists in output
165
+ // This is acceptable since position arrays are typically small (< 100 positions)
166
+
167
+ for (uint i = 0; i < positions.length; i++) {
168
+ int24 t0 = positions[i].tickLower;
169
+ int24 t1 = positions[i].tickUpper;
170
+ uint128 v = positions[i].liquidity;
171
+
172
+ bool duplicate = false;
173
+ for (uint j = 0; j < outLen; j++) {
174
+ LpPosition memory dedupedPosition = dedupedPositions[j];
175
+ if (dedupedPosition.tickLower == t0 && dedupedPosition.tickUpper == t1) {
176
+ dedupedPosition.liquidity += v;
177
+ duplicate = true;
178
+ break;
179
+ }
180
+ }
181
+
182
+ if (!duplicate) {
183
+ dedupedPositions[outLen] = LpPosition({tickLower: t0, tickUpper: t1, liquidity: v});
184
+ outLen++;
185
+ }
186
+ }
187
+
188
+ // Shrink to exact size by overwriting length field on the array
189
+ assembly {
190
+ mstore(dedupedPositions, outLen)
191
+ }
192
+ }
193
+
141
194
  function generatePositionsFromMigratedLiquidity(
142
195
  uint160 sqrtPriceX96,
143
196
  BurnedPosition[] calldata migratedLiquidity
@@ -198,6 +251,17 @@ library V4Liquidity {
198
251
  for (uint256 i; i < positions.length; i++) {
199
252
  uint128 liquidity = getLiquidity(poolManager, address(this), poolKey, positions[i].tickLower, positions[i].tickUpper);
200
253
 
254
+ // Skip positions that have no liquidity to avoid CannotUpdateEmptyPosition error
255
+ if (liquidity == 0) {
256
+ burnedPositions[i] = BurnedPosition({
257
+ tickLower: positions[i].tickLower,
258
+ tickUpper: positions[i].tickUpper,
259
+ amount0Received: 0,
260
+ amount1Received: 0
261
+ });
262
+ continue;
263
+ }
264
+
201
265
  ModifyLiquidityParams memory params = ModifyLiquidityParams({
202
266
  tickLower: positions[i].tickLower,
203
267
  tickUpper: positions[i].tickUpper,
@@ -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 "2.2.1";
12
+ return "2.3.0";
13
13
  }
14
14
  }
package/test/Coin.t.sol CHANGED
@@ -37,16 +37,16 @@ contract CoinTest is BaseTest {
37
37
  }
38
38
 
39
39
  function test_supply_constants() public {
40
- assertEq(CoinConstants.MAX_TOTAL_SUPPLY, CoinConstants.POOL_LAUNCH_SUPPLY + CoinConstants.CREATOR_LAUNCH_REWARD);
40
+ assertEq(CoinConstants.MAX_TOTAL_SUPPLY, CoinConstants.CONTENT_COIN_MARKET_SUPPLY + CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY);
41
41
 
42
42
  assertEq(CoinConstants.MAX_TOTAL_SUPPLY, 1_000_000_000e18);
43
- assertEq(CoinConstants.POOL_LAUNCH_SUPPLY, 990_000_000e18);
44
- assertEq(CoinConstants.CREATOR_LAUNCH_REWARD, 10_000_000e18);
43
+ assertEq(CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 990_000_000e18);
44
+ assertEq(CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY, 10_000_000e18);
45
45
 
46
46
  _deployV4Coin();
47
47
  assertEq(coinV4.totalSupply(), CoinConstants.MAX_TOTAL_SUPPLY);
48
- assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CREATOR_LAUNCH_REWARD);
49
- assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.POOL_LAUNCH_SUPPLY, 1e18);
48
+ assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY);
49
+ assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 1e18);
50
50
  }
51
51
 
52
52
  function test_initialize_validation() public {
@@ -0,0 +1,33 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.13;
3
+
4
+ import "forge-std/Test.sol";
5
+ import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
6
+ import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
7
+
8
+ contract CoinRewardsV4Test is Test {
9
+ function test_convertDeltaToPositiveUint128_success_with_valid_positive_values() public pure {
10
+ // Test with small positive value
11
+ int256 smallDelta = 1000;
12
+ uint128 result = CoinRewardsV4.convertDeltaToPositiveUint128(smallDelta);
13
+ assertEq(result, uint128(uint256(smallDelta)));
14
+
15
+ // Test with large but valid positive value (within uint128 range)
16
+ int256 largeDelta = int256(uint256(type(uint128).max));
17
+ uint128 result2 = CoinRewardsV4.convertDeltaToPositiveUint128(largeDelta);
18
+ assertEq(result2, type(uint128).max);
19
+
20
+ // Test with zero
21
+ int256 zeroDelta = 0;
22
+ uint128 result3 = CoinRewardsV4.convertDeltaToPositiveUint128(zeroDelta);
23
+ assertEq(result3, 0);
24
+ }
25
+
26
+ /// forge-config: default.allow_internal_expect_revert = true
27
+ function test_convertDeltaToPositiveUint128_edge_cases_and_reverts(int8 difference) public {
28
+ if (difference < 0) {
29
+ vm.expectRevert(SafeCast.SafeCastOverflow.selector);
30
+ }
31
+ CoinRewardsV4.convertDeltaToPositiveUint128(difference);
32
+ }
33
+ }
@@ -17,7 +17,6 @@ import {LpPosition} from "../src/types/LpPosition.sol";
17
17
  import {CoinCommon} from "../src/libs/CoinCommon.sol";
18
18
  import {IZoraV4CoinHook} from "../src/interfaces/IZoraV4CoinHook.sol";
19
19
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
20
- import {MarketConstants} from "../src/libs/MarketConstants.sol";
21
20
  import {IMsgSender} from "../src/interfaces/IMsgSender.sol";
22
21
  import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
23
22
  import {toBalanceDelta, BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
@@ -106,8 +105,8 @@ contract CoinUniV4Test is BaseTest {
106
105
 
107
106
  // Verify total supply equals maximum allowed
108
107
  assertEq(coinV4.totalSupply(), CoinConstants.MAX_TOTAL_SUPPLY, "total supply");
109
- assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), MarketConstants.CONTENT_COIN_MARKET_SUPPLY, 1000, "pool launch supply");
110
- assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CREATOR_LAUNCH_REWARD, "creator launch reward");
108
+ assertApproxEqAbs(coinV4.balanceOf(address(coinV4.poolManager())), CoinConstants.CONTENT_COIN_MARKET_SUPPLY, 1000, "pool launch supply");
109
+ assertEq(coinV4.balanceOf(coinV4.payoutRecipient()), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY, "creator launch reward");
111
110
  }
112
111
 
113
112
  function test_estimateLpFees() public {
@@ -317,4 +317,47 @@ contract ContentCoinRewardsTest is BaseTest {
317
317
 
318
318
  assertFalse(isLegacy, "Content coin should NOT be categorized as legacy creator coin");
319
319
  }
320
+
321
+ /// @notice Test reward distribution when platform referrer rejects ETH - should fallback to protocol recipient
322
+ function test_rewards_platform_referrer_eth_rejection_fallback() public {
323
+ // Deploy ETH-rejecting contract to use as platform referrer
324
+ EthRejectingContract ethRejecter = new EthRejectingContract();
325
+
326
+ // Deploy ETH-backed coin (like test_distributesMarketRewardsInEth)
327
+ address currency = address(0); // ETH backing
328
+ bytes32 salt = keccak256(abi.encodePacked("eth-reject-test"));
329
+ _deployV4Coin(currency, address(ethRejecter), salt); // ethRejecter becomes platform referrer
330
+
331
+ // Fund trader with ETH
332
+ uint128 ethAmount = 0.1 ether;
333
+ address trader = makeAddr("trader");
334
+ deal(trader, ethAmount);
335
+
336
+ // Record initial ETH balances
337
+ uint256 initialProtocolEth = coinV4.protocolRewardRecipient().balance;
338
+ uint256 initialRejecterEth = address(ethRejecter).balance;
339
+
340
+ // Execute ETH -> Coin trade using the working BaseTest function
341
+ _swapSomeCurrencyForCoin(coinV4, currency, ethAmount, trader);
342
+
343
+ // Calculate ETH balance deltas
344
+ uint256 protocolEthDelta = coinV4.protocolRewardRecipient().balance - initialProtocolEth;
345
+ uint256 rejecterEthDelta = address(ethRejecter).balance - initialRejecterEth;
346
+
347
+ // Verify ETH-rejecting contract got no ETH
348
+ assertEq(rejecterEthDelta, 0, "Platform referrer should receive no ETH");
349
+
350
+ // Verify protocol got ETH (backup mechanism worked)
351
+ assertGt(protocolEthDelta, 0, "Protocol should receive backup ETH from failed platform referrer");
352
+ }
353
+ }
354
+
355
+ // Contract that rejects ETH transfers (no payable functions)
356
+ contract EthRejectingContract {
357
+ // This contract has no receive() or fallback() payable functions
358
+ // so ETH transfers will fail
359
+ receive() external payable {
360
+ console.log("EthRejectingContract received ETH");
361
+ revert("EthRejectingContract received ETH");
362
+ }
320
363
  }
@@ -5,7 +5,7 @@ import "./utils/BaseTest.sol";
5
5
 
6
6
  import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
7
7
  import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
8
- import {CreatorCoinConstants} from "../src/libs/CreatorCoinConstants.sol";
8
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
9
9
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
10
10
  import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
11
11
 
@@ -65,11 +65,11 @@ contract CreatorCoinTest is BaseTest {
65
65
  assertEq(creatorCoin.name(), "Testcoin");
66
66
  assertEq(creatorCoin.symbol(), "TEST");
67
67
  assertEq(creatorCoin.payoutRecipient(), users.creator);
68
- assertEq(creatorCoin.currency(), CreatorCoinConstants.CURRENCY);
69
- assertEq(creatorCoin.totalSupply(), CreatorCoinConstants.TOTAL_SUPPLY);
68
+ assertEq(creatorCoin.currency(), CoinConstants.CREATOR_COIN_CURRENCY);
69
+ assertEq(creatorCoin.totalSupply(), CoinConstants.TOTAL_SUPPLY);
70
70
 
71
- assertEq(creatorCoin.balanceOf(address(creatorCoin)), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
72
- assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())), MarketConstants.CREATOR_COIN_MARKET_SUPPLY);
71
+ assertEq(creatorCoin.balanceOf(address(creatorCoin)), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
72
+ assertEq(creatorCoin.balanceOf(address(creatorCoin.poolManager())), CoinConstants.CREATOR_COIN_MARKET_SUPPLY);
73
73
  }
74
74
 
75
75
  function test_deploy_creator_coin_with_invalid_currency_reverts() public {
@@ -105,7 +105,7 @@ contract CreatorCoinTest is BaseTest {
105
105
  uint256 deploymentTime = block.timestamp;
106
106
 
107
107
  assertEq(creatorCoin.vestingStartTime(), deploymentTime);
108
- assertEq(creatorCoin.vestingEndTime(), deploymentTime + CreatorCoinConstants.CREATOR_VESTING_DURATION);
108
+ assertEq(creatorCoin.vestingEndTime(), deploymentTime + CoinConstants.CREATOR_VESTING_DURATION);
109
109
  assertEq(creatorCoin.totalClaimed(), 0);
110
110
  }
111
111
 
@@ -124,16 +124,16 @@ contract CreatorCoinTest is BaseTest {
124
124
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
125
125
 
126
126
  // After 1 year out of 5, should be able to claim 20% of vesting supply
127
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
127
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
128
128
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
129
129
  }
130
130
 
131
131
  function test_getClaimableAmount_after_half_vesting_period() public {
132
- uint256 halfVesting = CreatorCoinConstants.CREATOR_VESTING_DURATION / 2;
132
+ uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
133
133
  vm.warp(creatorCoin.vestingStartTime() + halfVesting);
134
134
 
135
135
  // After 2.5 years, should be able to claim 50% of vesting supply
136
- uint256 expectedClaimable = CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2;
136
+ uint256 expectedClaimable = CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2;
137
137
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
138
138
  }
139
139
 
@@ -141,14 +141,14 @@ contract CreatorCoinTest is BaseTest {
141
141
  vm.warp(creatorCoin.vestingEndTime());
142
142
 
143
143
  // After full vesting period, should be able to claim entire vesting supply
144
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
144
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
145
145
  }
146
146
 
147
147
  function test_getClaimableAmount_after_vesting_period_ends() public {
148
148
  vm.warp(creatorCoin.vestingEndTime() + 365 days);
149
149
 
150
150
  // Even after vesting ends, should still be able to claim entire vesting supply
151
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
151
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
152
152
  }
153
153
 
154
154
  function test_getClaimableAmount_after_one_day() public {
@@ -156,12 +156,12 @@ contract CreatorCoinTest is BaseTest {
156
156
  uint256 oneDay = 1 days;
157
157
  vm.warp(creatorCoin.vestingStartTime() + oneDay);
158
158
 
159
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneDay) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
159
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneDay) / CoinConstants.CREATOR_VESTING_DURATION;
160
160
  assertEq(creatorCoin.getClaimableAmount(), expectedClaimable);
161
161
 
162
162
  // Verify it's a small but non-zero amount
163
163
  assertGt(expectedClaimable, 0);
164
- assertLt(expectedClaimable, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000); // Less than 0.1%
164
+ assertLt(expectedClaimable, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000); // Less than 0.1%
165
165
  }
166
166
 
167
167
  function test_claimVesting_at_launch() public {
@@ -175,7 +175,7 @@ contract CreatorCoinTest is BaseTest {
175
175
  uint256 oneYear = 365 days;
176
176
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
177
177
 
178
- uint256 expectedClaimable = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
178
+ uint256 expectedClaimable = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
179
179
  uint256 initialCreatorBalance = creatorCoin.balanceOf(users.creator);
180
180
  uint256 initialContractBalance = creatorCoin.balanceOf(address(creatorCoin));
181
181
 
@@ -201,7 +201,7 @@ contract CreatorCoinTest is BaseTest {
201
201
 
202
202
  // First claim after 1 year
203
203
  vm.warp(creatorCoin.vestingStartTime() + oneYear);
204
- uint256 expectedClaim1 = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
204
+ uint256 expectedClaim1 = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
205
205
  uint256 claimed1 = creatorCoin.claimVesting();
206
206
 
207
207
  assertEq(claimed1, expectedClaim1);
@@ -210,7 +210,7 @@ contract CreatorCoinTest is BaseTest {
210
210
 
211
211
  // Second claim after another year (2 years total)
212
212
  vm.warp(creatorCoin.vestingStartTime() + 2 * oneYear);
213
- uint256 totalVestedAfter2Years = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * 2 * oneYear) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
213
+ uint256 totalVestedAfter2Years = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 2 * oneYear) / CoinConstants.CREATOR_VESTING_DURATION;
214
214
  uint256 expectedClaim2 = totalVestedAfter2Years - expectedClaim1;
215
215
 
216
216
  uint256 claimed2 = creatorCoin.claimVesting();
@@ -241,9 +241,9 @@ contract CreatorCoinTest is BaseTest {
241
241
 
242
242
  uint256 claimedAmount = creatorCoin.claimVesting();
243
243
 
244
- assertEq(claimedAmount, CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
245
- assertEq(creatorCoin.totalClaimed(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
246
- assertEq(creatorCoin.balanceOf(users.creator), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
244
+ assertEq(claimedAmount, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
245
+ assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
246
+ assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
247
247
 
248
248
  // Subsequent claims should return 0
249
249
  uint256 secondClaim = creatorCoin.claimVesting();
@@ -251,21 +251,21 @@ contract CreatorCoinTest is BaseTest {
251
251
  }
252
252
 
253
253
  function test_claimVesting_partial_then_full() public {
254
- uint256 halfVesting = CreatorCoinConstants.CREATOR_VESTING_DURATION / 2;
254
+ uint256 halfVesting = CoinConstants.CREATOR_VESTING_DURATION / 2;
255
255
 
256
256
  // Claim half way through vesting
257
257
  vm.warp(creatorCoin.vestingStartTime() + halfVesting);
258
258
  uint256 partialClaim = creatorCoin.claimVesting();
259
- assertEq(partialClaim, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2);
259
+ assertEq(partialClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
260
260
 
261
261
  // Claim the rest after full vesting
262
262
  vm.warp(creatorCoin.vestingEndTime());
263
263
  uint256 remainingClaim = creatorCoin.claimVesting();
264
- assertEq(remainingClaim, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 2);
264
+ assertEq(remainingClaim, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 2);
265
265
 
266
266
  // Total should equal full vesting supply
267
- assertEq(creatorCoin.totalClaimed(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
268
- assertEq(creatorCoin.balanceOf(users.creator), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
267
+ assertEq(creatorCoin.totalClaimed(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
268
+ assertEq(creatorCoin.balanceOf(users.creator), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
269
269
  }
270
270
 
271
271
  function test_vesting_calculation_edge_cases() public {
@@ -277,17 +277,17 @@ contract CreatorCoinTest is BaseTest {
277
277
  vm.warp(creatorCoin.vestingStartTime() + 1);
278
278
  uint256 claimableAfterOneSecond = creatorCoin.getClaimableAmount();
279
279
  assertGt(claimableAfterOneSecond, 0);
280
- assertLt(claimableAfterOneSecond, CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000000); // Very small amount
280
+ assertLt(claimableAfterOneSecond, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000); // Very small amount
281
281
 
282
282
  // Test one second before vesting ends
283
283
  vm.warp(creatorCoin.vestingEndTime() - 1);
284
284
  uint256 claimableBeforeEnd = creatorCoin.getClaimableAmount();
285
- assertLt(claimableBeforeEnd, CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
286
- assertGt(claimableBeforeEnd, CreatorCoinConstants.CREATOR_VESTING_SUPPLY - (CreatorCoinConstants.CREATOR_VESTING_SUPPLY / 1000000));
285
+ assertLt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
286
+ assertGt(claimableBeforeEnd, CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY - (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY / 1000000));
287
287
 
288
288
  // Test at exact vesting end time
289
289
  vm.warp(creatorCoin.vestingEndTime());
290
- assertEq(creatorCoin.getClaimableAmount(), CreatorCoinConstants.CREATOR_VESTING_SUPPLY);
290
+ assertEq(creatorCoin.getClaimableAmount(), CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY);
291
291
  }
292
292
 
293
293
  function test_vesting_frequent_small_claims() public {
@@ -302,11 +302,35 @@ contract CreatorCoinTest is BaseTest {
302
302
  }
303
303
 
304
304
  // Verify total claimed matches expected amount for 7 days
305
- uint256 expectedTotal = (CreatorCoinConstants.CREATOR_VESTING_SUPPLY * 7 days) / CreatorCoinConstants.CREATOR_VESTING_DURATION;
305
+ uint256 expectedTotal = (CoinConstants.CREATOR_COIN_CREATOR_VESTING_SUPPLY * 7 days) / CoinConstants.CREATOR_VESTING_DURATION;
306
306
  assertEq(totalClaimed, expectedTotal);
307
307
  assertEq(creatorCoin.totalClaimed(), expectedTotal);
308
308
  }
309
309
 
310
+ function test_vesting_duration_accounts_for_leap_years() public pure {
311
+ // Verify the vesting duration is exactly 5 years accounting for leap years
312
+ // 365.25 days per year * 5 years = 1826.25 days = 157,788,000 seconds
313
+ uint256 expectedDuration = 5 * 365.25 days;
314
+ uint256 expectedSeconds = 157_788_000; // 5 * 365.25 * 24 * 60 * 60
315
+
316
+ assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedDuration);
317
+ assertEq(CoinConstants.CREATOR_VESTING_DURATION, expectedSeconds);
318
+
319
+ // Verify it's longer than 5 * 365 days (which would be the old incorrect duration)
320
+ uint256 oldIncorrectDuration = 5 * 365 days;
321
+ uint256 differenceInSeconds = expectedDuration - oldIncorrectDuration;
322
+ uint256 expectedDifferenceInDays = 1.25 days; // 1.25 days = 108,000 seconds
323
+
324
+ assertEq(differenceInSeconds, expectedDifferenceInDays);
325
+ assertEq(differenceInSeconds, 108_000); // 1.25 * 24 * 60 * 60
326
+
327
+ // Verify this matches exactly 5 years with leap year correction
328
+ // Over 5 years, there's typically 1 leap day (Feb 29), plus 0.25 day per year
329
+ // for the quarter-day that accumulates: 1 + (5 * 0.25) = 2.25 days total
330
+ // But we use 365.25 average, so: 5 * 0.25 = 1.25 additional days
331
+ assertTrue(CoinConstants.CREATOR_VESTING_DURATION > oldIncorrectDuration);
332
+ }
333
+
310
334
  function test_buy(uint128 amountIn) public {
311
335
  vm.assume(amountIn > 0.00001e18);
312
336
  vm.assume(amountIn < 500_000e18);
@@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
4
4
  import {BaseTest} from "./utils/BaseTest.sol";
5
5
  import {BuySupplyWithSwapRouterHook} from "../src/hooks/deployment/BuySupplyWithSwapRouterHook.sol";
6
6
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
+ import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
7
8
  import {IUniswapV3Pool} from "../src/interfaces/IUniswapV3Pool.sol";
8
9
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
9
10
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
@@ -64,7 +65,7 @@ contract DeploymentsHooksTest is BaseTest {
64
65
  }
65
66
 
66
67
  function test_buySupplyWithEthUsingV4Hook_withExactInputMultiHop(uint256 initialOrderSize) public {
67
- vm.assume(initialOrderSize > CoinConstants.MIN_ORDER_SIZE);
68
+ vm.assume(initialOrderSize > 0.0001 ether);
68
69
  vm.assume(initialOrderSize < 1 ether);
69
70
 
70
71
  vm.deal(users.creator, initialOrderSize);
@@ -105,7 +106,7 @@ contract DeploymentsHooksTest is BaseTest {
105
106
  assertEq(coinV4.currency(), zora, "currency");
106
107
  assertGt(amountCurrency, 0, "amountCurrency > 0");
107
108
  assertGt(coinsPurchased, 0, "coinsPurchased > 0");
108
- assertEq(coinV4.balanceOf(users.creator), CoinConstants.CREATOR_LAUNCH_REWARD + coinsPurchased, "balanceOf creator");
109
+ assertEq(coinV4.balanceOf(users.creator), CoinConstants.CONTENT_COIN_INITIAL_CREATOR_SUPPLY + coinsPurchased, "balanceOf creator");
109
110
  // assertGt(IERC20(zora).balanceOf(address(pool)), 0, "Pool ZORA balance");
110
111
  }
111
112
 
@@ -211,4 +212,55 @@ contract DeploymentsHooksTest is BaseTest {
211
212
  vm.prank(users.creator);
212
213
  _deployWithHook(address(0), bytes(""), zora);
213
214
  }
215
+
216
+ function test_creatorCoin_deployWithHook_buySupplyWithEth() public {
217
+ uint256 initialOrderSize = 0.0001 ether;
218
+ vm.deal(users.creator, initialOrderSize);
219
+
220
+ uint24 poolFee = 3000;
221
+
222
+ bytes memory hookData = _encodeExactInputSingle(
223
+ users.creator,
224
+ ISwapRouter.ExactInputSingleParams({
225
+ tokenIn: address(weth),
226
+ tokenOut: zora,
227
+ fee: poolFee,
228
+ recipient: address(buySupplyWithSwapRouterHook),
229
+ amountIn: initialOrderSize,
230
+ amountOutMinimum: 0,
231
+ sqrtPriceLimitX96: 0
232
+ })
233
+ );
234
+
235
+ bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(zora);
236
+
237
+ vm.prank(users.creator);
238
+ (address creatorCoinAddress, bytes memory hookDataOut) = factory.deployCreatorCoin{value: initialOrderSize}(
239
+ users.creator,
240
+ _getDefaultOwners(),
241
+ "https://test.com",
242
+ "Creator Coin Test",
243
+ "CCT",
244
+ poolConfig,
245
+ users.platformReferrer,
246
+ address(buySupplyWithSwapRouterHook),
247
+ hookData,
248
+ bytes32(uint256(123)) // coinSalt
249
+ );
250
+
251
+ (uint256 amountCurrency, uint256 coinsPurchased) = abi.decode(hookDataOut, (uint256, uint256));
252
+
253
+ // Verify the creator coin was deployed successfully
254
+ ICoin creatorCoin = ICoin(creatorCoinAddress);
255
+ assertEq(creatorCoin.currency(), zora, "currency should be ZORA");
256
+ assertEq(IERC20Metadata(creatorCoinAddress).name(), "Creator Coin Test", "name should match");
257
+ assertEq(IERC20Metadata(creatorCoinAddress).symbol(), "CCT", "symbol should match");
258
+
259
+ // Verify the hook executed and purchased coins
260
+ assertGt(amountCurrency, 0, "amountCurrency should be > 0");
261
+ assertGt(coinsPurchased, 0, "coinsPurchased should be > 0");
262
+
263
+ // Verify creator received the purchased coins (launch reward vests over time for creator coins)
264
+ assertEq(IERC20(creatorCoinAddress).balanceOf(users.creator), coinsPurchased, "creator should have purchased coins");
265
+ }
214
266
  }