@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
@@ -23,6 +23,10 @@ contract ZoraHookRegistry is IZoraHookRegistry, MultiOwnable {
23
23
  /// @dev The tag for each hook
24
24
  mapping(address hook => string tag) internal hookTags;
25
25
 
26
+ /// @notice Constructor for deployment pathway
27
+ /// @dev This contract needs to be atomically initialized, otherwise it is unsafe to deploy.
28
+ /// @dev Initial owners need to be automatically set by the deployer contract.
29
+ /// @dev The initial owners is not a part of the constructor() to allow for multichain deterministic addresses.
26
30
  constructor() {}
27
31
 
28
32
  /// @notice Initializes the registry with initial owners
@@ -31,13 +31,14 @@ import {IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
31
31
  import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
32
32
  import {MultiOwnable} from "../utils/MultiOwnable.sol";
33
33
  import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
34
- import {IUpgradeableDestinationV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
34
+ import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
35
35
  import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
36
36
  import {BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
37
37
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
38
38
  import {TickMath} from "../utils/uniswap/TickMath.sol";
39
39
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
40
40
  import {IHasCoinType} from "../interfaces/ICoin.sol";
41
+ import {CoinConstants} from "../libs/CoinConstants.sol";
41
42
 
42
43
  /// @title ZoraV4CoinHook
43
44
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -49,7 +50,14 @@ import {IHasCoinType} from "../interfaces/ICoin.sol";
49
50
  /// 2. Swaps collected fees to the backing currency through multi-hop paths
50
51
  /// 3. Distributes converted fees as rewards
51
52
  /// @author oveddan
52
- contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC165, IUpgradeableDestinationV4Hook {
53
+ contract ZoraV4CoinHook is
54
+ BaseHook,
55
+ ContractVersionBase,
56
+ IZoraV4CoinHook,
57
+ ERC165,
58
+ IUpgradeableDestinationV4Hook,
59
+ IUpgradeableDestinationV4HookWithUpdateableFee
60
+ {
53
61
  using BalanceDeltaLibrary for BalanceDelta;
54
62
 
55
63
  /// @notice Mapping of trusted message senders - these are addresses that are trusted to provide a
@@ -130,6 +138,7 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
130
138
  return
131
139
  super.supportsInterface(interfaceId) ||
132
140
  interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
141
+ interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
133
142
  interfaceId == type(IVersionedContract).interfaceId;
134
143
  }
135
144
 
@@ -137,10 +146,18 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
137
146
  /// @param coin The coin address.
138
147
  /// @param key The pool key for the coin.
139
148
  /// @return positions The contract-created liquidity positions the positions for the coin's pool.
140
- function _generatePositions(ICoin coin, PoolKey memory key) internal view returns (LpPosition[] memory positions) {
149
+ function _generatePositions(ICoin coin, PoolKey memory key) internal view returns (LpPosition[] memory) {
141
150
  bool isCoinToken0 = Currency.unwrap(key.currency0) == address(coin);
142
151
 
143
- positions = CoinDopplerMultiCurve.calculatePositions(isCoinToken0, coin.getPoolConfiguration(), coin.totalSupplyForPositions());
152
+ LpPosition[] memory calculatedPositions = CoinDopplerMultiCurve.calculatePositions(
153
+ isCoinToken0,
154
+ coin.getPoolConfiguration(),
155
+ coin.totalSupplyForPositions()
156
+ );
157
+
158
+ // sometimes the calculated positions have liquidity added in duplicated positions. So here we dedupe them
159
+ // to save on gas in future swaps.
160
+ return V4Liquidity.dedupePositions(calculatedPositions);
144
161
  }
145
162
 
146
163
  /// @notice Internal fn called when a pool is initialized.
@@ -168,17 +185,50 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
168
185
  }
169
186
 
170
187
  /// @inheritdoc IUpgradeableDestinationV4Hook
188
+ /// @dev left for backward compatibility for migrating from hooks that dont support
189
+ /// updating the fee
171
190
  function initializeFromMigration(
172
191
  PoolKey calldata poolKey,
173
192
  address coin,
174
193
  uint160 sqrtPriceX96,
175
194
  BurnedPosition[] calldata migratedLiquidity,
176
- bytes calldata
195
+ bytes calldata additionalData
177
196
  ) external {
197
+ // keep the existing fee and tick spacing.
198
+ uint24 fee = poolKey.fee;
199
+ int24 tickSpacing = poolKey.tickSpacing;
200
+
201
+ _initializeFromMigration(poolKey, coin, sqrtPriceX96, migratedLiquidity, additionalData, fee, tickSpacing);
202
+ }
203
+
204
+ /// @inheritdoc IUpgradeableDestinationV4HookWithUpdateableFee
205
+ function initializeFromMigrationWithUpdateableFee(
206
+ PoolKey calldata poolKey,
207
+ address coin,
208
+ uint160 sqrtPriceX96,
209
+ BurnedPosition[] calldata migratedLiquidity,
210
+ bytes calldata additionalData
211
+ ) external returns (uint24 fee, int24 tickSpacing) {
212
+ // update the fee to the current one.
213
+ fee = CoinConstants.LP_FEE_V4;
214
+ tickSpacing = CoinConstants.TICK_SPACING;
215
+
216
+ _initializeFromMigration(poolKey, coin, sqrtPriceX96, migratedLiquidity, additionalData, fee, tickSpacing);
217
+ }
218
+
219
+ function _initializeFromMigration(
220
+ PoolKey calldata poolKey,
221
+ address coin,
222
+ uint160 sqrtPriceX96,
223
+ BurnedPosition[] calldata migratedLiquidity,
224
+ bytes calldata,
225
+ uint24 fee,
226
+ int24 tickSpacing
227
+ ) internal {
178
228
  address oldHook = msg.sender;
179
229
  address newHook = address(this);
180
230
 
181
- // Verify that the caller (new hook) is authorized to perform this migration
231
+ // Verify that the caller (old hook) is authorized to perform this migration
182
232
  // Only registered upgrade paths in the upgrade gate are allowed to migrate liquidity
183
233
  if (!upgradeGate.isRegisteredUpgradePath(oldHook, newHook)) {
184
234
  revert IUpgradeableV4Hook.UpgradePathNotRegistered(oldHook, newHook);
@@ -189,8 +239,8 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
189
239
  PoolKey memory newKey = PoolKey({
190
240
  currency0: poolKey.currency0,
191
241
  currency1: poolKey.currency1,
192
- fee: poolKey.fee,
193
- tickSpacing: poolKey.tickSpacing,
242
+ fee: fee,
243
+ tickSpacing: tickSpacing,
194
244
  hooks: IHooks(newHook)
195
245
  });
196
246
 
@@ -343,7 +393,7 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
343
393
  return CoinRewardsV4.getCoinType(coin);
344
394
  }
345
395
 
346
- /// @notice Internal fn called when the PoolManager is unlocked. Used to mint initial liquidity positions.
396
+ /// @notice Internal fn called when the PoolManager is unlocked. Used to mint initial liquidity positions and burn positions during migration.
347
397
  function unlockCallback(bytes calldata data) external onlyPoolManager returns (bytes memory) {
348
398
  return V4Liquidity.handleCallback(poolManager, data);
349
399
  }
@@ -376,7 +426,14 @@ contract ZoraV4CoinHook is BaseHook, ContractVersionBase, IZoraV4CoinHook, ERC16
376
426
  }
377
427
 
378
428
  newPoolKey = V4Liquidity.lockAndMigrate(poolManager, poolKey, poolCoin.positions, poolCoin.coin, newHook, additionalData);
429
+
430
+ // Delete the old pool key mapping to prevent future operations on the migrated pool
431
+ delete poolCoins[poolKeyHash];
379
432
  }
380
433
 
434
+ /// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
435
+ /// @dev Only required for coins using ETH as backing currency (currency = address(0)).
436
+ /// Restricted to onlyPoolManager to prevent ETH from getting stuck in the contract.
437
+ /// Unused for ERC20-backed coins.
381
438
  receive() external payable onlyPoolManager {}
382
439
  }
@@ -50,3 +50,21 @@ interface IUpgradeableDestinationV4Hook {
50
50
  bytes calldata additionalData
51
51
  ) external;
52
52
  }
53
+
54
+ interface IUpgradeableDestinationV4HookWithUpdateableFee {
55
+ /// @notice Initialize after migration from old hook
56
+ /// @param poolKey The pool key being migrated
57
+ /// @param coin The coin address
58
+ /// @param sqrtPriceX96 The current sqrt price
59
+ /// @param migratedLiquidity The migrated liquidity
60
+ /// @param additionalData Additional data for initialization
61
+ /// @return fee The new fee for the migrated liquidity
62
+ /// @return tickSpacing The new tick spacing for the migrated liquidity
63
+ function initializeFromMigrationWithUpdateableFee(
64
+ PoolKey calldata poolKey,
65
+ address coin,
66
+ uint160 sqrtPriceX96,
67
+ BurnedPosition[] calldata migratedLiquidity,
68
+ bytes calldata additionalData
69
+ ) external returns (uint24 fee, int24 tickSpacing);
70
+ }
@@ -97,7 +97,20 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
97
97
  /// @notice Thrwon when an invalid config version is provided
98
98
  error InvalidConfig();
99
99
 
100
- /// @notice Creates a new creator coin contract
100
+ /// @dev Deprecated: use `deployCreatorCoin` instead that has a salt and post-deploy hook specified
101
+ function deployCreatorCoin(
102
+ address payoutRecipient,
103
+ address[] memory owners,
104
+ string memory uri,
105
+ string memory name,
106
+ string memory symbol,
107
+ bytes memory poolConfig,
108
+ address platformReferrer,
109
+ bytes32 coinSalt
110
+ ) external returns (address);
111
+
112
+ /// @notice Creates a new creator coin contract with an optional hook that runs after the coin is deployed.
113
+ /// Enables buying initial supply by supporting ETH transfers to the post-deploy hook.
101
114
  /// @param payoutRecipient The recipient of creator reward payouts; this can be updated by an owner
102
115
  /// @param owners The list of addresses that will be able to manage the coin's payout address and metadata uri
103
116
  /// @param uri The coin metadata uri
@@ -105,7 +118,11 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
105
118
  /// @param symbol The symbol of the coin
106
119
  /// @param poolConfig The config parameters for the coin's pool
107
120
  /// @param platformReferrer The address of the platform referrer
121
+ /// @param postDeployHook The address of the hook to run after the coin is deployed
122
+ /// @param postDeployHookData The data to pass to the hook
108
123
  /// @param coinSalt The salt used to deploy the coin
124
+ /// @return coin The address of the deployed creator coin
125
+ /// @return postDeployHookDataOut The data returned by the hook
109
126
  function deployCreatorCoin(
110
127
  address payoutRecipient,
111
128
  address[] memory owners,
@@ -114,8 +131,10 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
114
131
  string memory symbol,
115
132
  bytes memory poolConfig,
116
133
  address platformReferrer,
134
+ address postDeployHook,
135
+ bytes calldata postDeployHookData,
117
136
  bytes32 coinSalt
118
- ) external returns (address);
137
+ ) external payable returns (address coin, bytes memory postDeployHookDataOut);
119
138
 
120
139
  /// @notice Creates a new coin contract with an optional hook that runs after the coin is deployed.
121
140
  /// Requires a salt to be specified, which enabled the coin to be deployed deterministically, and at
@@ -8,21 +8,64 @@
8
8
  pragma solidity ^0.8.23;
9
9
 
10
10
  library CoinConstants {
11
+ /// @dev Constant used to increase precision during calculations
12
+ uint256 internal constant WAD = 1e18;
13
+
11
14
  /// @notice The maximum total supply
12
15
  /// @dev Set to 1 billion coins with 18 decimals
13
- uint256 public constant MAX_TOTAL_SUPPLY = 1_000_000_000e18;
16
+ uint256 internal constant MAX_TOTAL_SUPPLY = 1_000_000_000e18;
17
+
18
+ /// @notice The total supply for creator coins (same as MAX_TOTAL_SUPPLY)
19
+ /// @dev 1 billion coins
20
+ uint256 internal constant TOTAL_SUPPLY = 1_000_000_000e18;
14
21
 
15
- /// @notice The number of coins allocated to the liquidity pool
22
+ /// @notice The number of coins allocated to the liquidity pool for content coins
16
23
  /// @dev 990 million coins
17
- uint256 public constant POOL_LAUNCH_SUPPLY = 990_000_000e18;
24
+ uint256 internal constant CONTENT_COIN_MARKET_SUPPLY = 990_000_000e18;
25
+
26
+ /// @notice The number of coins allocated to the liquidity pool for creator coins
27
+ /// @dev 500 million coins
28
+ uint256 internal constant CREATOR_COIN_MARKET_SUPPLY = 500_000_000e18;
18
29
 
19
- /// @notice The number of coins rewarded to the creator
30
+ /// @notice The number of coins rewarded to the creator for content coins on launch
20
31
  /// @dev 10 million coins
21
- uint256 public constant CREATOR_LAUNCH_REWARD = 10_000_000e18;
32
+ uint256 internal constant CONTENT_COIN_INITIAL_CREATOR_SUPPLY = TOTAL_SUPPLY - CONTENT_COIN_MARKET_SUPPLY;
33
+
34
+ /// @notice Creator coin vesting supply for creator
35
+ /// @dev 500 million coins
36
+ uint256 internal constant CREATOR_COIN_CREATOR_VESTING_SUPPLY = TOTAL_SUPPLY - CREATOR_COIN_MARKET_SUPPLY;
37
+
38
+ /// @notice Creator coin vesting duration
39
+ /// @dev 5 years with leap years accounted for
40
+ uint256 internal constant CREATOR_VESTING_DURATION = (5 * 365.25 days);
41
+
42
+ /// @notice The backing currency for creator coins
43
+ /// @dev ETH backing currency address
44
+ address internal constant CREATOR_COIN_CURRENCY = 0x1111111111166b7FE7bd91427724B487980aFc69;
45
+
46
+ /// @notice The LP fee
47
+ /// @dev 10000 basis points = 1%
48
+ uint24 internal constant LP_FEE_V4 = 10_000;
49
+
50
+ /// @notice The spacing for 1% pools
51
+ /// @dev 200 ticks
52
+ int24 internal constant TICK_SPACING = 200;
53
+
54
+ // Creator gets 62.5% of market rewards (0.50% of total 1% fee)
55
+ // Market rewards = 80% of total fee (0.80% of 1%)
56
+ uint256 internal constant CREATOR_REWARD_BPS = 6250;
57
+
58
+ // Platform referrer gets 25% of market rewards (0.20% of total 1% fee)
59
+ uint256 internal constant CREATE_REFERRAL_REWARD_BPS = 2500;
60
+
61
+ // Trade referrer gets 5% of market rewards (0.04% of total 1% fee)
62
+ uint256 internal constant TRADE_REFERRAL_REWARD_BPS = 500;
63
+
64
+ // Doppler gets 1.25% of market rewards (0.01% of total 1% fee)
65
+ uint256 internal constant DOPPLER_REWARD_BPS = 125;
22
66
 
23
- /// @notice The minimum order size allowed for trades
24
- /// @dev Set to 0.0000001 ETH to prevent dust transactions
25
- uint256 public constant MIN_ORDER_SIZE = 0.0000001 ether;
67
+ // LPs get 20% of total fee (0.20% of 1%)
68
+ uint256 internal constant LP_REWARD_BPS = 2000;
26
69
 
27
70
  int24 internal constant DEFAULT_DISCOVERY_TICK_LOWER = -777000;
28
71
  int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
@@ -10,7 +10,7 @@ pragma solidity ^0.8.23;
10
10
  import {PoolConfiguration} from "../interfaces/ICoin.sol";
11
11
  import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
12
12
  import {LpPosition} from "../types/LpPosition.sol";
13
- import {MarketConstants} from "./MarketConstants.sol";
13
+ import {CoinConstants} from "./CoinConstants.sol";
14
14
  import {FullMath} from "../utils/uniswap/FullMath.sol";
15
15
  import {TickMath} from "../utils/uniswap/TickMath.sol";
16
16
  import {IDopplerErrors} from "../interfaces/IDopplerErrors.sol";
@@ -56,8 +56,8 @@ library CoinDopplerMultiCurve {
56
56
  uint256 totalDiscoverySupplyShare;
57
57
  uint256 totalDiscoveryPositions;
58
58
 
59
- int24 boundryTickLower = DopplerMath.alignTickToTickSpacing(isCoinToken0, TickMath.MAX_TICK, MarketConstants.TICK_SPACING);
60
- int24 boundryTickUpper = DopplerMath.alignTickToTickSpacing(isCoinToken0, TickMath.MIN_TICK, MarketConstants.TICK_SPACING);
59
+ int24 boundryTickLower = DopplerMath.alignTickToTickSpacing(isCoinToken0, TickMath.MAX_TICK, CoinConstants.TICK_SPACING);
60
+ int24 boundryTickUpper = DopplerMath.alignTickToTickSpacing(isCoinToken0, TickMath.MIN_TICK, CoinConstants.TICK_SPACING);
61
61
 
62
62
  // For each curve:
63
63
  for (uint256 i; i < numCurves; i++) {
@@ -69,8 +69,8 @@ library CoinDopplerMultiCurve {
69
69
  totalDiscoveryPositions += numDiscoveryPositions_[i];
70
70
  totalDiscoverySupplyShare += maxDiscoverySupplyShare_[i];
71
71
 
72
- int24 currentTickLower = DopplerMath.alignTickToTickSpacing(isCoinToken0, tickLower_[i], MarketConstants.TICK_SPACING);
73
- int24 currentTickUpper = DopplerMath.alignTickToTickSpacing(isCoinToken0, tickUpper_[i], MarketConstants.TICK_SPACING);
72
+ int24 currentTickLower = DopplerMath.alignTickToTickSpacing(isCoinToken0, tickLower_[i], CoinConstants.TICK_SPACING);
73
+ int24 currentTickUpper = DopplerMath.alignTickToTickSpacing(isCoinToken0, tickUpper_[i], CoinConstants.TICK_SPACING);
74
74
 
75
75
  require(currentTickLower < currentTickUpper, ConfigTickLowerMustBeLessThanTickUpper());
76
76
 
@@ -84,15 +84,15 @@ library CoinDopplerMultiCurve {
84
84
 
85
85
  require(boundryTickLower < boundryTickUpper, InvalidTickRangeMisordered(boundryTickLower, boundryTickUpper));
86
86
  require(totalDiscoveryPositions > 1 && totalDiscoveryPositions <= 200, IDopplerErrors.NumDiscoveryPositionsOutOfRange());
87
- require(totalDiscoverySupplyShare < MarketConstants.WAD, IDopplerErrors.MaxShareToBeSoldExceeded(totalDiscoverySupplyShare, MarketConstants.WAD));
87
+ require(totalDiscoverySupplyShare < CoinConstants.WAD, IDopplerErrors.MaxShareToBeSoldExceeded(totalDiscoverySupplyShare, CoinConstants.WAD));
88
88
 
89
89
  sqrtPriceX96 = TickMath.getSqrtPriceAtTick(isCoinToken0 ? boundryTickLower : boundryTickUpper);
90
90
 
91
91
  poolConfiguration = PoolConfiguration({
92
92
  version: CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION,
93
93
  numPositions: uint16(totalDiscoveryPositions + 1), // Add one for the final tail position
94
- fee: MarketConstants.LP_FEE_V4,
95
- tickSpacing: MarketConstants.TICK_SPACING,
94
+ fee: CoinConstants.LP_FEE_V4,
95
+ tickSpacing: CoinConstants.TICK_SPACING,
96
96
  numDiscoveryPositions: numDiscoveryPositions_,
97
97
  tickLower: tickLower_,
98
98
  tickUpper: tickUpper_,
@@ -113,12 +113,12 @@ library CoinDopplerMultiCurve {
113
113
  uint256 numCurves = poolConfiguration.tickLower.length;
114
114
 
115
115
  for (uint256 i; i < numCurves; i++) {
116
- uint256 curveSupply = FullMath.mulDiv(totalSupply, poolConfiguration.maxDiscoverySupplyShare[i], MarketConstants.WAD);
116
+ uint256 curveSupply = FullMath.mulDiv(totalSupply, poolConfiguration.maxDiscoverySupplyShare[i], CoinConstants.WAD);
117
117
 
118
118
  (positions, curveSupply) = DopplerMath.calculateLogNormalDistribution(
119
119
  poolConfiguration.tickLower[i],
120
120
  poolConfiguration.tickUpper[i],
121
- MarketConstants.TICK_SPACING,
121
+ CoinConstants.TICK_SPACING,
122
122
  isCoinToken0,
123
123
  curveSupply,
124
124
  poolConfiguration.numDiscoveryPositions[i],
@@ -138,7 +138,7 @@ library CoinDopplerMultiCurve {
138
138
  poolConfiguration.tickUpper[numCurves - 1],
139
139
  isCoinToken0,
140
140
  tailSupply,
141
- MarketConstants.TICK_SPACING
141
+ CoinConstants.TICK_SPACING
142
142
  );
143
143
  }
144
144
  }
@@ -33,26 +33,11 @@ import {ICreatorCoinHook} from "../interfaces/ICreatorCoinHook.sol";
33
33
  import {IHasCoinType} from "../interfaces/ICoin.sol";
34
34
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
35
35
  import {ICreatorCoin} from "../interfaces/ICreatorCoin.sol";
36
+ import {CoinConstants} from "./CoinConstants.sol";
36
37
 
37
38
  library CoinRewardsV4 {
38
39
  using SafeERC20 for IERC20;
39
40
 
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;
43
-
44
- // Platform referrer gets 25% of market rewards (0.20% of total 1% fee)
45
- uint256 public constant CREATE_REFERRAL_REWARD_BPS = 2500;
46
-
47
- // Trade referrer gets 5% of market rewards (0.04% of total 1% fee)
48
- uint256 public constant TRADE_REFERRAL_REWARD_BPS = 500;
49
-
50
- // Doppler gets 1.25% of market rewards (0.01% of total 1% fee)
51
- uint256 public constant DOPPLER_REWARD_BPS = 125;
52
-
53
- // LPs get 20% of total fee (0.20% of 1%)
54
- uint256 public constant LP_REWARD_BPS = 2000;
55
-
56
41
  function getTradeReferral(bytes calldata hookData) internal pure returns (address) {
57
42
  return hookData.length >= 20 ? abi.decode(hookData, (address)) : address(0);
58
43
  }
@@ -83,13 +68,16 @@ library CoinRewardsV4 {
83
68
 
84
69
  /// @dev Computes the LP reward and remaining amount for market rewards from the total amount
85
70
  function computeLpReward(uint128 totalBackingAmount) internal pure returns (uint128 lpRewardAmount) {
86
- lpRewardAmount = uint128(calculateReward(uint256(totalBackingAmount), LP_REWARD_BPS));
71
+ lpRewardAmount = uint128(calculateReward(uint256(totalBackingAmount), CoinConstants.LP_REWARD_BPS));
87
72
  }
88
73
 
89
74
  function convertDeltaToPositiveUint128(int256 delta) internal pure returns (uint128) {
90
75
  if (delta < 0) {
91
76
  revert SafeCast.SafeCastOverflow();
92
77
  }
78
+ if (delta > int256(uint256(type(uint128).max))) {
79
+ revert SafeCast.SafeCastOverflow();
80
+ }
93
81
  return uint128(uint256(delta));
94
82
  }
95
83
 
@@ -181,6 +169,7 @@ library CoinRewardsV4 {
181
169
  /// @param fees The total amount of fees collected to be distributed
182
170
  /// @param coin The coin contract instance that implements IHasRewardsRecipients to get recipient addresses
183
171
  /// @param tradeReferrer The address of the trade referrer who should receive trade referral rewards (can be zero address)
172
+ /// @param coinType The type of coin (Creator or Content) which affects reward distribution percentages
184
173
  function distributeMarketRewards(
185
174
  Currency currency,
186
175
  uint128 fees,
@@ -258,26 +247,30 @@ library CoinRewardsV4 {
258
247
  ) internal returns (MarketRewards memory rewards) {
259
248
  rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0));
260
249
 
261
- if (platformReferrer != address(0)) {
262
- _transferCurrency(currency, rewards.platformReferrerAmount, platformReferrer);
263
- }
264
- if (tradeReferral != address(0)) {
265
- _transferCurrency(currency, rewards.tradeReferrerAmount, tradeReferral);
266
- }
267
- _transferCurrency(currency, rewards.creatorAmount, payoutRecipient);
268
- _transferCurrency(currency, rewards.dopplerAmount, doppler);
269
- _transferCurrency(currency, rewards.protocolAmount, protocolRewardRecipient);
250
+ // Notes on ETH transfer fallback behavior:
251
+ // - If the platform referrer is immutable; if it is set to an address that cannot receive ETH, it can brick swaps on the coin, as they would revert.
252
+ // - Both the creator recipient and trade referral are changeable, the former via updating the payout recipient on the coin, and the latter via updating the trade referrer argument when doing a swap;
253
+ // therefore we do not need to worry about permanently bricking swaps, and don't need a backup recipient.
254
+ _transferCurrency(currency, rewards.platformReferrerAmount, platformReferrer, protocolRewardRecipient);
255
+ _transferCurrency(currency, rewards.tradeReferrerAmount, tradeReferral, address(0));
256
+ _transferCurrency(currency, rewards.creatorAmount, payoutRecipient, address(0));
257
+ _transferCurrency(currency, rewards.dopplerAmount, doppler, protocolRewardRecipient);
258
+ _transferCurrency(currency, rewards.protocolAmount, protocolRewardRecipient, address(0));
270
259
  }
271
260
 
272
- function _transferCurrency(Currency currency, uint256 amount, address to) internal {
273
- if (amount == 0) {
261
+ function _transferCurrency(Currency currency, uint256 amount, address to, address backupRecipient) internal {
262
+ if (amount == 0 || to == address(0)) {
274
263
  return;
275
264
  }
276
265
 
277
266
  if (currency.isAddressZero()) {
278
267
  (bool success, ) = payable(to).call{value: amount}("");
279
268
  if (!success) {
280
- revert ICoin.EthTransferFailed();
269
+ if (backupRecipient == address(0)) {
270
+ revert ICoin.EthTransferFailed();
271
+ } else {
272
+ _transferCurrency(currency, amount, backupRecipient, address(0));
273
+ }
281
274
  }
282
275
  } else {
283
276
  IERC20(Currency.unwrap(currency)).safeTransfer(to, amount);
@@ -290,10 +283,10 @@ library CoinRewardsV4 {
290
283
  }
291
284
 
292
285
  uint256 totalAmount = uint256(fee);
293
- rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CREATE_REFERRAL_REWARD_BPS) : 0;
294
- rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, TRADE_REFERRAL_REWARD_BPS) : 0;
295
- rewards.creatorAmount = calculateReward(totalAmount, CREATOR_REWARD_BPS);
296
- rewards.dopplerAmount = calculateReward(totalAmount, DOPPLER_REWARD_BPS);
286
+ rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CoinConstants.CREATE_REFERRAL_REWARD_BPS) : 0;
287
+ rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, CoinConstants.TRADE_REFERRAL_REWARD_BPS) : 0;
288
+ rewards.creatorAmount = calculateReward(totalAmount, CoinConstants.CREATOR_REWARD_BPS);
289
+ rewards.dopplerAmount = calculateReward(totalAmount, CoinConstants.DOPPLER_REWARD_BPS);
297
290
  rewards.protocolAmount = totalAmount - rewards.platformReferrerAmount - rewards.tradeReferrerAmount - rewards.creatorAmount - rewards.dopplerAmount;
298
291
  }
299
292
 
@@ -11,12 +11,11 @@ import {PoolConfigurationV4} from "../interfaces/ICoin.sol";
11
11
  import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
12
12
  import {ICoin} from "../interfaces/ICoin.sol";
13
13
  import {CoinCommon} from "./CoinCommon.sol";
14
- import {MarketConstants} from "./MarketConstants.sol";
14
+ import {CoinConstants} from "./CoinConstants.sol";
15
15
  import {TickMath} from "../utils/uniswap/TickMath.sol";
16
16
  import {IPoolManager, PoolKey, Currency, IHooks} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
17
17
  import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
18
18
  import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
19
- import {MarketConstants} from "./MarketConstants.sol";
20
19
  import {LpPosition} from "../types/LpPosition.sol";
21
20
  import {CoinDopplerMultiCurve, PoolConfiguration} from "./CoinDopplerMultiCurve.sol";
22
21
 
@@ -37,13 +36,7 @@ library CoinSetup {
37
36
  Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
38
37
  Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
39
38
 
40
- poolKey = PoolKey({
41
- currency0: currency0,
42
- currency1: currency1,
43
- fee: MarketConstants.LP_FEE_V4,
44
- tickSpacing: MarketConstants.TICK_SPACING,
45
- hooks: hooks
46
- });
39
+ poolKey = PoolKey({currency0: currency0, currency1: currency1, fee: CoinConstants.LP_FEE_V4, tickSpacing: CoinConstants.TICK_SPACING, hooks: hooks});
47
40
  }
48
41
 
49
42
  function setupPoolWithVersion(
@@ -13,7 +13,7 @@ import {FullMath} from "../utils/uniswap/FullMath.sol";
13
13
  import {SqrtPriceMath} from "../utils/uniswap/SqrtPriceMath.sol";
14
14
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
15
15
  import {LpPosition} from "../types/LpPosition.sol";
16
- import {MarketConstants} from "./MarketConstants.sol";
16
+ import {CoinConstants} from "./CoinConstants.sol";
17
17
 
18
18
  /// @author Whetstone Research
19
19
  /// @notice Calculates liquidity provisioning with Uniswap v3
@@ -54,7 +54,7 @@ library DopplerMath {
54
54
  int24 spread = tickUpper - tickLower;
55
55
 
56
56
  uint160 farSqrtPriceX96 = TickMath.getSqrtPriceAtTick(farTick);
57
- uint256 amountPerPosition = FullMath.mulDiv(discoverySupply, MarketConstants.WAD, totalPositions * MarketConstants.WAD);
57
+ uint256 amountPerPosition = FullMath.mulDiv(discoverySupply, CoinConstants.WAD, totalPositions * CoinConstants.WAD);
58
58
  uint256 totalAssetsSold;
59
59
 
60
60
  for (uint256 i; i < totalPositions; i++) {