@zoralabs/coins 2.1.1 → 2.2.1

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 (97) hide show
  1. package/.turbo/turbo-build$colon$js.log +152 -0
  2. package/CHANGELOG.md +60 -0
  3. package/abis/BaseCoin.json +26 -0
  4. package/abis/BaseTest.json +2 -7
  5. package/abis/CoinConstants.json +0 -104
  6. package/abis/ContentCoin.json +26 -0
  7. package/abis/CreatorCoin.json +30 -4
  8. package/abis/FeeEstimatorHook.json +0 -5
  9. package/abis/ICoin.json +26 -0
  10. package/abis/ICoinV3.json +26 -0
  11. package/abis/ICreatorCoin.json +39 -0
  12. package/abis/IERC721.json +36 -36
  13. package/abis/IHasCoinType.json +15 -0
  14. package/abis/IHasTotalSupplyForPositions.json +15 -0
  15. package/abis/IZoraFactory.json +52 -0
  16. package/abis/IZoraHookRegistry.json +188 -0
  17. package/abis/VmContractHelper227.json +233 -0
  18. package/abis/ZoraFactoryImpl.json +32 -6
  19. package/abis/ZoraHookRegistry.json +375 -0
  20. package/abis/{CreatorCoinHook.json → ZoraV4CoinHook.json} +1 -1
  21. package/addresses/8453.json +2 -1
  22. package/dist/index.cjs +72 -10
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.js +72 -10
  25. package/dist/index.js.map +1 -1
  26. package/dist/wagmiGenerated.d.ts +90 -10
  27. package/dist/wagmiGenerated.d.ts.map +1 -1
  28. package/foundry.toml +4 -1
  29. package/package/wagmiGenerated.ts +72 -10
  30. package/package.json +7 -5
  31. package/script/PrintRegisterUpgradePath.s.sol +0 -7
  32. package/script/TestBackingCoinSwap.s.sol +0 -1
  33. package/script/TestV4Swap.s.sol +0 -1
  34. package/script/UpgradeFactoryImpl.s.sol +1 -1
  35. package/src/BaseCoin.sol +15 -12
  36. package/src/ContentCoin.sol +10 -0
  37. package/src/CreatorCoin.sol +28 -7
  38. package/src/ZoraFactoryImpl.sol +62 -23
  39. package/src/deployment/CoinsDeployerBase.sol +24 -58
  40. package/src/hook-registry/ZoraHookRegistry.sol +93 -0
  41. package/src/hooks/{BaseZoraV4CoinHook.sol → ZoraV4CoinHook.sol} +13 -8
  42. package/src/interfaces/ICoin.sol +19 -1
  43. package/src/interfaces/ICreatorCoin.sol +4 -0
  44. package/src/interfaces/IZoraFactory.sol +32 -10
  45. package/src/interfaces/IZoraHookRegistry.sol +47 -0
  46. package/src/libs/CoinConstants.sol +0 -32
  47. package/src/libs/CoinRewardsV4.sol +53 -15
  48. package/src/libs/CreatorCoinConstants.sol +0 -1
  49. package/src/libs/HooksDeployment.sol +13 -65
  50. package/src/libs/MarketConstants.sol +10 -12
  51. package/src/libs/V4Liquidity.sol +30 -0
  52. package/src/version/ContractVersionBase.sol +1 -1
  53. package/test/CoinUniV4.t.sol +33 -30
  54. package/test/ContentCoinRewards.t.sol +320 -0
  55. package/test/CreatorCoin.t.sol +1 -1
  56. package/test/CreatorCoinRewards.t.sol +375 -0
  57. package/test/DeploymentHooks.t.sol +10 -10
  58. package/test/Factory.t.sol +24 -7
  59. package/test/HooksDeployment.t.sol +4 -4
  60. package/test/LiquidityMigration.t.sol +4 -9
  61. package/test/Upgrades.t.sol +44 -48
  62. package/test/ZoraHookRegistry.t.sol +266 -0
  63. package/test/utils/BaseTest.sol +25 -42
  64. package/test/utils/FeeEstimatorHook.sol +4 -6
  65. package/test/utils/RewardTestHelpers.sol +106 -0
  66. package/.turbo/turbo-build.log +0 -199
  67. package/abis/AutoSwapperTest.json +0 -618
  68. package/abis/BadImpl.json +0 -15
  69. package/abis/BaseZoraV4CoinHook.json +0 -1664
  70. package/abis/CoinTest.json +0 -819
  71. package/abis/CoinUniV4Test.json +0 -1128
  72. package/abis/ContentCoinHook.json +0 -1733
  73. package/abis/CreatorCoinTest.json +0 -887
  74. package/abis/Deploy.json +0 -9
  75. package/abis/DeployHooks.json +0 -9
  76. package/abis/DeployScript.json +0 -35
  77. package/abis/DeployedCoinVersionLookupTest.json +0 -740
  78. package/abis/DifferentNamespaceVersionLookup.json +0 -39
  79. package/abis/FactoryTest.json +0 -748
  80. package/abis/FakeHookNoInterface.json +0 -21
  81. package/abis/GenerateDeterministicParams.json +0 -9
  82. package/abis/HooksDeploymentTest.json +0 -645
  83. package/abis/HooksTest.json +0 -709
  84. package/abis/InvalidLiquidityMigrationReceiver.json +0 -21
  85. package/abis/LiquidityMigrationReceiver.json +0 -103
  86. package/abis/LiquidityMigrationTest.json +0 -889
  87. package/abis/MockBadFactory.json +0 -15
  88. package/abis/MultiOwnableTest.json +0 -766
  89. package/abis/PrintUpgradeCommand.json +0 -9
  90. package/abis/TestDeployedCoinVersionLookupImplementation.json +0 -39
  91. package/abis/TestV4Swap.json +0 -9
  92. package/abis/UpgradeFactoryImpl.json +0 -9
  93. package/abis/UpgradeHooks.json +0 -35
  94. package/abis/UpgradesTest.json +0 -723
  95. package/src/hooks/ContentCoinHook.sol +0 -27
  96. package/src/hooks/CreatorCoinHook.sol +0 -27
  97. package/src/libs/CreatorCoinRewards.sol +0 -34
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
2
+ // This software is licensed under the Zora Delayed Open Source License.
3
+ // Under this license, you may use, copy, modify, and distribute this software for
4
+ // non-commercial purposes only. Commercial use and competitive products are prohibited
5
+ // until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6
+ // at which point this software automatically becomes available under the MIT License.
7
+ // Full license terms available at: https://docs.zora.co/coins/license
8
+ pragma solidity ^0.8.23;
9
+
10
+ interface IZoraHookRegistry {
11
+ /// @notice Zora hook data
12
+ struct ZoraHook {
13
+ address hook;
14
+ string tag;
15
+ string version;
16
+ }
17
+
18
+ /// @notice Emitted when a hook is added to the registry
19
+ event ZoraHookRegistered(address indexed hook, string tag, string version);
20
+
21
+ /// @notice Emitted when a hook is removed from the registry
22
+ event ZoraHookRemoved(address indexed hook, string tag, string version);
23
+
24
+ /// @dev Reverts when the length of the hooks and tags arrays do not match
25
+ error ArrayLengthMismatch();
26
+
27
+ /// @notice Returns whether a hook is currently registered
28
+ function isRegisteredHook(address hook) external view returns (bool);
29
+
30
+ /// @notice Returns all registered hooks
31
+ function getHooks() external view returns (ZoraHook[] memory);
32
+
33
+ /// @notice Returns all registered hook addresses
34
+ function getHookAddresses() external view returns (address[] memory);
35
+
36
+ /// @notice Returns the tag for a hook
37
+ function getHookTag(address hook) external view returns (string memory);
38
+
39
+ /// @notice Returns the contract version for a hook if it exists
40
+ function getHookVersion(address hook) external view returns (string memory);
41
+
42
+ /// @notice Adds hooks to the registry
43
+ function registerHooks(address[] calldata hooks, string[] calldata tags) external;
44
+
45
+ /// @notice Removes hooks from the registry
46
+ function removeHooks(address[] calldata hooks) external;
47
+ }
@@ -24,38 +24,6 @@ library CoinConstants {
24
24
  /// @dev Set to 0.0000001 ETH to prevent dust transactions
25
25
  uint256 public constant MIN_ORDER_SIZE = 0.0000001 ether;
26
26
 
27
- /// @notice The total fee percentage in basis points
28
- /// @dev 100 basis points = 1%
29
- uint256 public constant TOTAL_FEE_BPS = 100;
30
-
31
- /// @notice The percentage of the total fee allocated to creators
32
- /// @dev 5000 basis points = 50% of TOTAL_FEE_BPS
33
- uint256 public constant TOKEN_CREATOR_FEE_BPS = 5000;
34
-
35
- /// @notice The percentage of the total fee allocated to the protocol
36
- /// @dev 2000 basis points = 20% of TOTAL_FEE_BPS
37
- uint256 public constant PROTOCOL_FEE_BPS = 2000;
38
-
39
- /// @notice The percentage of the total fee allocated to platform referrers
40
- /// @dev 1500 basis points = 15% of TOTAL_FEE_BPS
41
- uint256 public constant PLATFORM_REFERRER_FEE_BPS = 1500;
42
-
43
- /// @notice The percentage of the total fee allocated to trade referrers
44
- /// @dev 1500 basis points = 15% of TOTAL_FEE_BPS
45
- uint256 public constant TRADE_REFERRER_FEE_BPS = 1500;
46
-
47
- /// @notice The percentage of the LP fee allocated to creators
48
- /// @dev 5000 basis points = 50% of the 1% LP FEE
49
- uint256 public constant CREATOR_MARKET_REWARD_BPS = 5000;
50
-
51
- /// @notice The percentage of the LP fee allocated to platform referrers
52
- /// @dev 2500 basis points = 25% of the 1% LP FEE
53
- uint256 public constant PLATFORM_REFERRER_MARKET_REWARD_BPS = 2500;
54
-
55
- /// @notice The percentage of the LP fee allocated to the Doppler protocol
56
- /// @dev 500 basis points = 5% of the 1% LP FEE
57
- uint256 public constant DOPPLER_MARKET_REWARD_BPS = 500;
58
-
59
27
  int24 internal constant DEFAULT_DISCOVERY_TICK_LOWER = -777000;
60
28
  int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
61
29
  uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
@@ -29,28 +29,29 @@ import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
29
29
  import {IHasSwapPath} from "../interfaces/ICoin.sol";
30
30
  import {V4Liquidity} from "./V4Liquidity.sol";
31
31
  import {UniV4SwapToCurrency} from "./UniV4SwapToCurrency.sol";
32
+ import {ICreatorCoinHook} from "../interfaces/ICreatorCoinHook.sol";
33
+ import {IHasCoinType} from "../interfaces/ICoin.sol";
34
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
35
+ import {ICreatorCoin} from "../interfaces/ICreatorCoin.sol";
32
36
 
33
37
  library CoinRewardsV4 {
34
38
  using SafeERC20 for IERC20;
35
39
 
36
- // creator gets 50% of the market rewards
37
- // market rewards are 2/3 of the total fee
38
- uint256 public constant CREATOR_REWARD_BPS = 5000;
40
+ // Creator gets 62.5% of market rewards (0.50% of total 1% fee)
41
+ // Market rewards = 80% of total fee (0.80% of 1%)
42
+ uint256 public constant CREATOR_REWARD_BPS = 6250;
39
43
 
40
- // create referrer gets 15% of the market rewards
41
- // market rewards are 2/3 of the total fee
42
- uint256 public constant CREATE_REFERRAL_REWARD_BPS = 1500;
44
+ // Platform referrer gets 25% of market rewards (0.20% of total 1% fee)
45
+ uint256 public constant CREATE_REFERRAL_REWARD_BPS = 2500;
43
46
 
44
- // trade referrer gets 10% of the market rewards
45
- // market rewards are 2/3 of the total fee
46
- uint256 public constant TRADE_REFERRAL_REWARD_BPS = 1500;
47
+ // Trade referrer gets 5% of market rewards (0.04% of total 1% fee)
48
+ uint256 public constant TRADE_REFERRAL_REWARD_BPS = 500;
47
49
 
48
- // doppler gets 5% of the market rewards
49
- // market rewards are 2/3 of the total fee
50
- uint256 public constant DOPPLER_REWARD_BPS = 500;
50
+ // Doppler gets 1.25% of market rewards (0.01% of total 1% fee)
51
+ uint256 public constant DOPPLER_REWARD_BPS = 125;
51
52
 
52
- // LPs get 1/3 of the total fee
53
- uint256 public constant LP_REWARD_BPS = 3333;
53
+ // LPs get 20% of total fee (0.20% of 1%)
54
+ uint256 public constant LP_REWARD_BPS = 2000;
54
55
 
55
56
  function getTradeReferral(bytes calldata hookData) internal pure returns (address) {
56
57
  return hookData.length >= 20 ? abi.decode(hookData, (address)) : address(0);
@@ -180,7 +181,13 @@ library CoinRewardsV4 {
180
181
  /// @param fees The total amount of fees collected to be distributed
181
182
  /// @param coin The coin contract instance that implements IHasRewardsRecipients to get recipient addresses
182
183
  /// @param tradeReferrer The address of the trade referrer who should receive trade referral rewards (can be zero address)
183
- function distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal {
184
+ function distributeMarketRewards(
185
+ Currency currency,
186
+ uint128 fees,
187
+ IHasRewardsRecipients coin,
188
+ address tradeReferrer,
189
+ IHasCoinType.CoinType coinType
190
+ ) internal {
184
191
  address payoutRecipient = coin.payoutRecipient();
185
192
  address platformReferrer = coin.platformReferrer();
186
193
  address protocolRewardRecipient = coin.protocolRewardRecipient();
@@ -219,6 +226,17 @@ library CoinRewardsV4 {
219
226
  doppler,
220
227
  marketRewards
221
228
  );
229
+
230
+ if (coinType == IHasCoinType.CoinType.Creator) {
231
+ emit ICreatorCoinHook.CreatorCoinRewards(
232
+ address(coin),
233
+ Currency.unwrap(currency),
234
+ payoutRecipient,
235
+ protocolRewardRecipient,
236
+ rewards.creatorAmount,
237
+ rewards.protocolAmount
238
+ );
239
+ }
222
240
  }
223
241
 
224
242
  struct MarketRewards {
@@ -282,4 +300,24 @@ library CoinRewardsV4 {
282
300
  function calculateReward(uint256 amount, uint256 bps) internal pure returns (uint256) {
283
301
  return (amount * bps) / 10_000;
284
302
  }
303
+
304
+ function getCoinType(IHasRewardsRecipients coin) internal view returns (IHasCoinType.CoinType) {
305
+ // first check if the coin supports the IHasCoinType interface - if it does, we can use that
306
+ if (IERC165(address(coin)).supportsInterface(type(IHasCoinType).interfaceId)) {
307
+ return IHasCoinType(address(coin)).coinType();
308
+ }
309
+
310
+ // see if its a legacy creator coin
311
+ return isLegacyCreatorCoin(coin) ? IHasCoinType.CoinType.Creator : IHasCoinType.CoinType.Content;
312
+ }
313
+
314
+ function isLegacyCreatorCoin(IHasRewardsRecipients coin) internal view returns (bool) {
315
+ // try to call the method `getClaimableAmount` on the legacy creator coin, if it succeeds, then it is a legacy creator coin,
316
+ // otherwise we can assume it is a content coin
317
+ try ICreatorCoin(address(coin)).getClaimableAmount() returns (uint256) {
318
+ return true;
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
285
323
  }
@@ -9,7 +9,6 @@ pragma solidity ^0.8.23;
9
9
 
10
10
  library CreatorCoinConstants {
11
11
  uint256 internal constant TOTAL_SUPPLY = 1_000_000_000e18; // 1b coins
12
- uint256 internal constant MARKET_SUPPLY = 500_000_000e18; // 500m coins
13
12
  uint256 internal constant CREATOR_VESTING_SUPPLY = 500_000_000e18; // 500m coins
14
13
  uint256 internal constant CREATOR_VESTING_DURATION = 5 * 365 days; // 5 years
15
14
  address internal constant CURRENCY = 0x1111111111166b7FE7bd91427724B487980aFc69;
@@ -9,8 +9,7 @@ pragma solidity ^0.8.23;
9
9
 
10
10
  import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
11
11
  import {Vm} from "forge-std/Vm.sol";
12
- import {ContentCoinHook} from "../hooks/ContentCoinHook.sol";
13
- import {CreatorCoinHook} from "../hooks/CreatorCoinHook.sol";
12
+ import {ZoraV4CoinHook} from "../hooks/ZoraV4CoinHook.sol";
14
13
  import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";
15
14
  import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
16
15
  import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
@@ -83,26 +82,14 @@ library HooksDeployment {
83
82
  }
84
83
  }
85
84
 
86
- function mineForCreatorCoinSalt(
85
+ function mineForCoinSalt(
87
86
  address deployer,
88
87
  address poolManager,
89
88
  address coinVersionLookup,
90
89
  address[] memory trustedMessageSenders,
91
90
  address upgradeGate
92
91
  ) internal returns (address hookAddress, bytes32 salt) {
93
- bytes memory hookCreationCode = creatorCoinHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
94
- (salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
95
- hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
96
- }
97
-
98
- function mineForContentCoinSalt(
99
- address deployer,
100
- address poolManager,
101
- address coinVersionLookup,
102
- address[] memory trustedMessageSenders,
103
- address upgradeGate
104
- ) internal returns (address hookAddress, bytes32 salt) {
105
- bytes memory hookCreationCode = contentCoinCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
92
+ bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
106
93
  (salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
107
94
  hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
108
95
  }
@@ -141,7 +128,7 @@ library HooksDeployment {
141
128
  isDeployed = hookAddress.code.length > 0;
142
129
  }
143
130
 
144
- function contentCoinConstructorArgs(
131
+ function hookConstructorArgs(
145
132
  address poolManager,
146
133
  address coinVersionLookup,
147
134
  address[] memory trustedMessageSenders,
@@ -150,82 +137,43 @@ library HooksDeployment {
150
137
  return abi.encode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
151
138
  }
152
139
 
153
- function contentCoinCreationCode(
154
- address poolManager,
155
- address coinVersionLookup,
156
- address[] memory trustedMessageSenders,
157
- address upgradeGate
158
- ) internal pure returns (bytes memory) {
159
- return
160
- abi.encodePacked(
161
- type(ContentCoinHook).creationCode,
162
- contentCoinConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate)
163
- );
164
- }
165
-
166
- function creatorCoinCreationCode(
140
+ function makeHookCreationCode(
167
141
  address poolManager,
168
142
  address coinVersionLookup,
169
143
  address[] memory trustedMessageSenders,
170
144
  address upgradeGate
171
145
  ) internal pure returns (bytes memory) {
172
- return
173
- abi.encodePacked(
174
- type(CreatorCoinHook).creationCode,
175
- creatorCoinConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate)
176
- );
146
+ return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate));
177
147
  }
178
148
 
179
149
  /// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
180
150
  /// deployed with the provided salt, it will be returned.
181
- function deployContentCoinHook(
151
+ function deployZoraV4CoinHook(
182
152
  address poolManager,
183
153
  address coinVersionLookup,
184
154
  address[] memory trustedMessageSenders,
185
155
  address upgradeGate,
186
156
  bytes32 salt
187
157
  ) internal returns (IHooks hook) {
188
- bytes memory hookCreationCode = contentCoinCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
189
- return deployHookWithSalt(hookCreationCode, salt);
190
- }
191
-
192
- function creatorCoinConstructorArgs(
193
- address poolManager,
194
- address coinVersionLookup,
195
- address[] memory trustedMessageSenders,
196
- address upgradeGate
197
- ) internal pure returns (bytes memory) {
198
- return abi.encode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
199
- }
200
-
201
- function creatorCoinHookCreationCode(
202
- address poolManager,
203
- address coinVersionLookup,
204
- address[] memory trustedMessageSenders,
205
- address upgradeGate
206
- ) internal pure returns (bytes memory) {
207
- return
208
- abi.encodePacked(
209
- type(CreatorCoinHook).creationCode,
210
- creatorCoinConstructorArgs(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate)
211
- );
158
+ bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMessageSenders, upgradeGate);
159
+ return deployHookWithSalt(creationCode, salt);
212
160
  }
213
161
 
214
162
  address constant FOUNDRY_SCRIPT_ADDRESS = 0x4e59b44847b379578588920cA78FbF26c0B4956C;
215
163
 
216
164
  function deployHookWithExistingOrNewSalt(
217
165
  address deployer,
218
- bytes memory hookCreationCode,
166
+ bytes memory _hookCreationCode,
219
167
  bytes32 salt
220
168
  ) internal returns (IHooks hook, bytes32 resultingSalt) {
221
- (bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, hookCreationCode, salt);
169
+ (bool isDeployed, address existingHookAddress) = hooksIsDeployed(deployer, _hookCreationCode, salt);
222
170
 
223
171
  if (isDeployed) {
224
172
  hook = IHooks(existingHookAddress);
225
173
  resultingSalt = salt;
226
174
  } else {
227
- (, resultingSalt) = mineForSalt(deployer, hookCreationCode);
228
- hook = IHooks(Create2.deploy(0, resultingSalt, hookCreationCode));
175
+ (, resultingSalt) = mineForSalt(deployer, _hookCreationCode);
176
+ hook = IHooks(Create2.deploy(0, resultingSalt, _hookCreationCode));
229
177
  }
230
178
  }
231
179
  }
@@ -3,23 +3,21 @@ pragma solidity ^0.8.23;
3
3
 
4
4
  library MarketConstants {
5
5
  /// @dev Constant used to increase precision during calculations
6
- uint256 constant WAD = 1e18;
6
+ uint256 internal constant WAD = 1e18;
7
7
 
8
- /// @notice The LP fee
9
- /// @dev 10000 basis points = 1%
10
- uint24 internal constant LP_FEE = 10000;
8
+ /// @notice The number of coins allocated to the liquidity pool for content coins
9
+ /// @dev 990 million coins
10
+ uint256 internal constant CONTENT_COIN_MARKET_SUPPLY = 990_000_000 * WAD;
11
+
12
+ /// @notice The number of coins allocated to the liquidity pool for creator coins
13
+ /// @dev 500 million coins
14
+ uint256 internal constant CREATOR_COIN_MARKET_SUPPLY = 500_000_000 * WAD;
11
15
 
12
16
  /// @notice The LP fee
13
- /// @dev 30000 basis points = 3%
14
- uint24 internal constant LP_FEE_V4 = 30000;
17
+ /// @dev 10000 basis points = 1%
18
+ uint24 internal constant LP_FEE_V4 = 10_000;
15
19
 
16
20
  /// @notice The spacing for 1% pools
17
21
  /// @dev 200 ticks
18
22
  int24 internal constant TICK_SPACING = 200;
19
-
20
- /// @notice The minimum lower tick for legacy single LP WETH pools
21
- int24 internal constant LP_TICK_LOWER_WETH = -208200;
22
-
23
- /// @notice The upper tick for legacy single LP WETH pools
24
- int24 internal constant LP_TICK_UPPER = 887200;
25
23
  }
@@ -167,6 +167,12 @@ library V4Liquidity {
167
167
  continue;
168
168
  }
169
169
 
170
+ // skip lps with no fees to collect
171
+ (uint256 feeGrowthInside0DeltaX128, uint256 feeGrowthInside1DeltaX128) = getFeeGrowth(poolManager, poolKey, positions[i]);
172
+ if (feeGrowthInside0DeltaX128 == 0 && feeGrowthInside1DeltaX128 == 0) {
173
+ continue;
174
+ }
175
+
170
176
  params = ModifyLiquidityParams({
171
177
  tickLower: positions[i].tickLower,
172
178
  tickUpper: positions[i].tickUpper,
@@ -221,6 +227,30 @@ library V4Liquidity {
221
227
  liquidity = StateLibrary.getPositionLiquidity(poolManager, poolKey.toId(), positionId);
222
228
  }
223
229
 
230
+ function getFeeGrowth(
231
+ IPoolManager poolManager,
232
+ PoolKey memory poolKey,
233
+ LpPosition memory position
234
+ ) private view returns (uint256 feeGrowthInside0DeltaX128, uint256 feeGrowthInside1DeltaX128) {
235
+ (, uint256 feeGrowthInside0LastX128, uint256 feeGrowthInside1LastX128) = StateLibrary.getPositionInfo(
236
+ poolManager,
237
+ poolKey.toId(),
238
+ address(this),
239
+ position.tickLower,
240
+ position.tickUpper,
241
+ bytes32(0)
242
+ );
243
+ (uint256 feeGrowthInside0X128, uint256 feeGrowthInside1X128) = StateLibrary.getFeeGrowthInside(
244
+ poolManager,
245
+ poolKey.toId(),
246
+ position.tickLower,
247
+ position.tickUpper
248
+ );
249
+
250
+ feeGrowthInside0DeltaX128 = feeGrowthInside0X128 - feeGrowthInside0LastX128;
251
+ feeGrowthInside1DeltaX128 = feeGrowthInside1X128 - feeGrowthInside1LastX128;
252
+ }
253
+
224
254
  function mintPositions(IPoolManager poolManager, PoolKey memory poolKey, LpPosition[] memory positions) internal returns (int128 amount0, int128 amount1) {
225
255
  ModifyLiquidityParams memory params;
226
256
  uint256 numPositions = positions.length;
@@ -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.1.1";
12
+ return "2.2.1";
13
13
  }
14
14
  }
@@ -16,6 +16,8 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
16
16
  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
+ import {CoinConstants} from "../src/libs/CoinConstants.sol";
20
+ import {MarketConstants} from "../src/libs/MarketConstants.sol";
19
21
  import {IMsgSender} from "../src/interfaces/IMsgSender.sol";
20
22
  import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
21
23
  import {toBalanceDelta, BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
@@ -57,13 +59,13 @@ contract CoinUniV4Test is BaseTest {
57
59
  /// and then reverting the state after the swap
58
60
  function _estimateLpFees(bytes memory commands, bytes[] memory inputs) internal returns (FeeEstimatorHook.FeeEstimatorState memory feeState) {
59
61
  uint256 snapshot = vm.snapshot();
60
- _deployFeeEstimatorHook(CoinConstants.POOL_LAUNCH_SUPPLY, address(contentCoinHook));
62
+ _deployFeeEstimatorHook(address(hook));
61
63
 
62
64
  // Execute the swap
63
65
  uint256 deadline = block.timestamp + 20;
64
66
  router.execute(commands, inputs, deadline);
65
67
 
66
- feeState = FeeEstimatorHook(payable(address(contentCoinHook))).getFeeState();
68
+ feeState = FeeEstimatorHook(payable(address(hook))).getFeeState();
67
69
 
68
70
  vm.revertToState(snapshot);
69
71
  }
@@ -74,14 +76,14 @@ contract CoinUniV4Test is BaseTest {
74
76
  bytes[] memory inputs
75
77
  ) internal returns (BalanceDelta delta, SwapParams memory swapParams, uint160 sqrtPriceX96) {
76
78
  uint256 snapshot = vm.snapshot();
77
- _deployFeeEstimatorHook(CoinConstants.POOL_LAUNCH_SUPPLY, address(contentCoinHook));
79
+ _deployFeeEstimatorHook(address(hook));
78
80
 
79
81
  // Execute the swap
80
82
  uint256 deadline = block.timestamp + 20;
81
83
  router.execute(commands, inputs, deadline);
82
84
 
83
- delta = FeeEstimatorHook(payable(address(contentCoinHook))).getFeeState().lastDelta;
84
- swapParams = FeeEstimatorHook(payable(address(contentCoinHook))).getFeeState().lastSwapParams;
85
+ delta = FeeEstimatorHook(payable(address(hook))).getFeeState().lastDelta;
86
+ swapParams = FeeEstimatorHook(payable(address(hook))).getFeeState().lastSwapParams;
85
87
 
86
88
  sqrtPriceX96 = PoolStateReader.getSqrtPriceX96(coinV4.getPoolKey(), poolManager);
87
89
 
@@ -98,15 +100,15 @@ contract CoinUniV4Test is BaseTest {
98
100
  });
99
101
  }
100
102
 
101
- // function test_setupZeroAddressForHooks() public {
102
- // vm.expectRevert(ICoin.AddressZero.selector);
103
- // new ContentCoin({
104
- // protocolRewardRecipient_: address(0x1234),
105
- // protocolRewards_: address(0x1234),
106
- // poolManager_: IPoolManager(address(0x1234)),
107
- // airlock_: address(0x1234)
108
- // });
109
- // }
103
+ function test_deployContentCoin_verifyTotalSupplyAllocation() public {
104
+ address currency = address(mockERC20A);
105
+ _deployV4Coin(currency);
106
+
107
+ // Verify total supply equals maximum allowed
108
+ 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");
111
+ }
110
112
 
111
113
  function test_estimateLpFees() public {
112
114
  address currency = address(mockERC20A);
@@ -161,17 +163,18 @@ contract CoinUniV4Test is BaseTest {
161
163
  FeeEstimatorHook.FeeEstimatorState memory newFeeState = _estimateLpFees(commands, inputs);
162
164
 
163
165
  uint128 newFeeCoin = isCoinToken0 ? newFeeState.fees0 : newFeeState.fees1;
164
- uint128 newFeeCurrency = isCoinToken0 ? newFeeState.fees1 : newFeeState.fees0;
166
+ // uint128 newFeeCurrency = isCoinToken0 ? newFeeState.fees1 : newFeeState.fees0;
165
167
 
166
168
  assertGt(newFeeCoin, 0, "fee coin on second swap should be greater than 0");
167
169
  // assertGt(newFeeCurrency, 0, "fee currency on second swap should be greater than 0"); // TODO confirm what this should be -- prev was assertEq(0) and test passed but error message was asserting greater than 0
168
170
  assertGt(newFeeState.afterSwapCurrencyAmount, 0, "after swap fee currency on second swap should be greater than 0");
169
171
  }
170
172
 
171
- uint256 public constant CREATOR_REWARD_BPS = 5000;
172
- uint256 public constant CREATE_REFERRAL_REWARD_BPS = 1500;
173
- uint256 public constant TRADE_REFERRAL_REWARD_BPS = 1500;
174
- uint256 public constant DOPPLER_REWARD_BPS = 500;
173
+ // Use the same constants as CoinRewardsV4.sol for consistency
174
+ uint256 public constant CREATOR_REWARD_BPS = 6250; // 62.5% of market rewards (0.50% of total 1% fee)
175
+ uint256 public constant CREATE_REFERRAL_REWARD_BPS = 2500; // 25% of market rewards (0.20% of total 1% fee)
176
+ uint256 public constant TRADE_REFERRAL_REWARD_BPS = 500; // 5% of market rewards (0.04% of total 1% fee)
177
+ uint256 public constant DOPPLER_REWARD_BPS = 125; // 1.25% of market rewards (0.01% of total 1% fee)
175
178
 
176
179
  struct Rewards {
177
180
  uint256 backing;
@@ -228,8 +231,8 @@ contract CoinUniV4Test is BaseTest {
228
231
  return rewards;
229
232
  }
230
233
 
231
- function test_distributesMarketRewards(uint64 amountIn, bool hasCreateReferral, bool hasTradeReferral) public {
232
- vm.assume(amountIn > 0.00001 ether);
234
+ function test_distributesMarketRewards(bool hasCreateReferral, bool hasTradeReferral) public {
235
+ uint64 amountIn = 2 ether;
233
236
  address currency = address(mockERC20A);
234
237
  address createReferral = hasCreateReferral ? makeAddr("createReferral") : address(0);
235
238
  address tradeReferral = hasTradeReferral ? makeAddr("tradeReferral") : address(0);
@@ -282,15 +285,15 @@ contract CoinUniV4Test is BaseTest {
282
285
  }
283
286
  assertEq(coinV4.balanceOf(coinV4.protocolRewardRecipient()), 0, "protocol reward coin");
284
287
 
285
- assertApproxEqAbs(mockERC20A.balanceOf(coinV4.payoutRecipient()), totalRewards.backing, 10, "backing reward currency");
286
- assertApproxEqAbs(mockERC20A.balanceOf(coinV4.dopplerFeeRecipient()), totalRewards.doppler, 10, "doppler reward currency");
288
+ assertApproxEqAbs(mockERC20A.balanceOf(coinV4.payoutRecipient()), totalRewards.backing, 5000, "backing reward currency");
289
+ assertApproxEqAbs(mockERC20A.balanceOf(coinV4.dopplerFeeRecipient()), totalRewards.doppler, 5000, "doppler reward currency");
287
290
  if (hasCreateReferral) {
288
- assertApproxEqAbs(mockERC20A.balanceOf(createReferral), totalRewards.createReferral, 10, "create referral reward currency");
291
+ assertApproxEqAbs(mockERC20A.balanceOf(createReferral), totalRewards.createReferral, 5000, "create referral reward currency");
289
292
  }
290
293
  if (hasTradeReferral) {
291
- assertApproxEqAbs(mockERC20A.balanceOf(tradeReferral), totalRewards.tradeReferral, 10, "trade referral reward currency");
294
+ assertApproxEqAbs(mockERC20A.balanceOf(tradeReferral), totalRewards.tradeReferral, 5000, "trade referral reward currency");
292
295
  }
293
- assertApproxEqAbs(mockERC20A.balanceOf(coinV4.protocolRewardRecipient()), totalRewards.protocol, 10, "protocol reward currency");
296
+ assertApproxEqAbs(mockERC20A.balanceOf(coinV4.protocolRewardRecipient()), totalRewards.protocol, 5000, "protocol reward currency");
294
297
  }
295
298
 
296
299
  function test_distributesMarketRewardsInEth() public {
@@ -349,8 +352,8 @@ contract CoinUniV4Test is BaseTest {
349
352
  assertGt(trader.balance, 0, "trader should have received ETH back");
350
353
  }
351
354
 
352
- function test_swap_emitsCoinMarketRewardsV4(uint64 amountIn) public {
353
- vm.assume(amountIn > 0.00001 ether);
355
+ function test_swap_emitsCoinMarketRewardsV4() public {
356
+ uint64 amountIn = 1 ether;
354
357
  address currency = address(mockERC20A);
355
358
  address createReferral = makeAddr("createReferral");
356
359
  address tradeReferral = makeAddr("tradeReferral");
@@ -588,7 +591,7 @@ contract CoinUniV4Test is BaseTest {
588
591
  currency1: isCoinToken0 ? Currency.wrap(address(notACoin)) : Currency.wrap(address(coinV4)),
589
592
  fee: 3000,
590
593
  tickSpacing: 60,
591
- hooks: IHooks(address(contentCoinHook))
594
+ hooks: IHooks(address(hook))
592
595
  });
593
596
 
594
597
  // We need to prank the call to come from the non-coin contract
@@ -598,7 +601,7 @@ contract CoinUniV4Test is BaseTest {
598
601
  vm.expectRevert(
599
602
  abi.encodeWithSelector(
600
603
  CustomRevert.WrappedError.selector,
601
- address(contentCoinHook),
604
+ address(hook),
602
605
  IHooks.afterInitialize.selector,
603
606
  abi.encodeWithSelector(IZoraV4CoinHook.NotACoin.selector, address(notACoin)),
604
607
  abi.encodeWithSelector(Hooks.HookCallFailed.selector)