@zoralabs/coins 0.7.1 → 1.0.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 (167) hide show
  1. package/.turbo/turbo-build.log +106 -84
  2. package/CHANGELOG.md +68 -0
  3. package/abis/BadImpl.json +15 -0
  4. package/abis/BalanceDeltaLibrary.json +15 -0
  5. package/abis/BaseCoin.json +1350 -0
  6. package/abis/BaseCoinDeployHook.json +78 -0
  7. package/abis/BaseHook.json +897 -0
  8. package/abis/BaseTest.json +60 -91
  9. package/abis/BeforeSwapDeltaLibrary.json +15 -0
  10. package/abis/BuySupplyWithSwapRouterHook.json +126 -0
  11. package/abis/Coin.json +214 -150
  12. package/abis/CoinConstants.json +65 -0
  13. package/abis/CoinDopplerMultiCurve.json +38 -0
  14. package/abis/CoinRewardsV4.json +54 -0
  15. package/abis/CoinTest.json +66 -111
  16. package/abis/CoinUniV4Test.json +1053 -0
  17. package/abis/CoinV4.json +1687 -0
  18. package/abis/CurrencyLibrary.json +25 -0
  19. package/abis/DeployHooks.json +9 -0
  20. package/abis/DeployScript.json +47 -0
  21. package/abis/DeployedCoinVersionLookup.json +21 -0
  22. package/abis/DeployedCoinVersionLookupTest.json +716 -0
  23. package/abis/DifferentNamespaceVersionLookup.json +39 -0
  24. package/abis/DopplerUniswapV3Test.json +62 -184
  25. package/abis/ERC20.json +310 -0
  26. package/abis/FactoryTest.json +98 -98
  27. package/abis/FakeHookNoInterface.json +21 -0
  28. package/abis/FeeEstimatorHook.json +1528 -0
  29. package/abis/Hooks.json +28 -0
  30. package/abis/HooksDeployment.json +23 -0
  31. package/abis/HooksTest.json +698 -0
  32. package/abis/IAllowanceTransfer.json +486 -0
  33. package/abis/ICoin.json +62 -69
  34. package/abis/ICoinDeployHook.json +31 -0
  35. package/abis/ICoinV3.json +879 -0
  36. package/abis/ICoinV4.json +915 -0
  37. package/abis/IContractMetadata.json +28 -0
  38. package/abis/IDeployedCoinVersionLookup.json +21 -0
  39. package/abis/IEIP712.json +15 -0
  40. package/abis/IEIP712_v4.json +15 -0
  41. package/abis/IERC20Minimal.json +172 -0
  42. package/abis/IERC6909Claims.json +288 -0
  43. package/abis/IERC721.json +36 -36
  44. package/abis/IERC721Permit_v4.json +88 -0
  45. package/abis/IExtsload.json +64 -0
  46. package/abis/IExttload.json +40 -0
  47. package/abis/IHasAfterCoinDeploy.json +31 -0
  48. package/abis/IHasContractName.json +15 -0
  49. package/abis/IHasPoolKey.json +42 -0
  50. package/abis/IHasRewardsRecipients.json +54 -0
  51. package/abis/IHasSwapPath.json +60 -0
  52. package/abis/IHooks.json +789 -0
  53. package/abis/IImmutableState.json +15 -0
  54. package/abis/IMsgSender.json +15 -0
  55. package/abis/IMulticall_v4.json +21 -0
  56. package/abis/INotifier.json +187 -0
  57. package/abis/IPermit2.json +865 -0
  58. package/abis/IPermit2Forwarder.json +138 -0
  59. package/abis/IPoolConfigEncoding.json +46 -0
  60. package/abis/IPoolInitializer_v4.json +53 -0
  61. package/abis/IPoolManager.json +1286 -0
  62. package/abis/IPositionManager.json +712 -0
  63. package/abis/IProtocolFees.json +174 -0
  64. package/abis/ISignatureTransfer.json +394 -0
  65. package/abis/ISubscriber.json +89 -0
  66. package/abis/ISwapPathRouter.json +92 -0
  67. package/abis/ISwapRouter.json +82 -0
  68. package/abis/IUniversalRouter.json +61 -0
  69. package/abis/IUnlockCallback.json +21 -0
  70. package/abis/IUnorderedNonce.json +44 -0
  71. package/abis/IV4Quoter.json +310 -0
  72. package/abis/IV4Router.json +47 -0
  73. package/abis/IZoraFactory.json +328 -4
  74. package/abis/IZoraV4CoinHook.json +427 -0
  75. package/abis/ImmutableState.json +36 -0
  76. package/abis/LPFeeLibrary.json +65 -0
  77. package/abis/MockERC20.json +21 -0
  78. package/abis/MultiOwnableTest.json +60 -91
  79. package/abis/{CoinConfigurationVersions.json → Position.json} +1 -1
  80. package/abis/PrintUpgradeCommand.json +9 -0
  81. package/abis/ProxyShim.json +24 -0
  82. package/abis/Simulate.json +0 -91
  83. package/abis/StateLibrary.json +80 -0
  84. package/abis/TestDeployedCoinVersionLookupImplementation.json +39 -0
  85. package/abis/TestV4Swap.json +9 -0
  86. package/abis/{CoinSetup.json → UniV3BuySell.json} +5 -0
  87. package/abis/UniV3Errors.json +32 -0
  88. package/abis/UpgradeCoinImpl.json +47 -0
  89. package/abis/UpgradeFactoryImpl.json +9 -0
  90. package/abis/UpgradesTest.json +671 -0
  91. package/abis/Vm.json +1482 -111
  92. package/abis/VmSafe.json +856 -32
  93. package/abis/ZoraFactoryImpl.json +450 -1
  94. package/abis/ZoraV4CoinHook.json +1439 -0
  95. package/addresses/8453.json +8 -3
  96. package/addresses/84532.json +8 -3
  97. package/dist/index.cjs +1998 -184
  98. package/dist/index.cjs.map +1 -1
  99. package/dist/index.js +1989 -178
  100. package/dist/index.js.map +1 -1
  101. package/dist/wagmiGenerated.d.ts +2852 -688
  102. package/dist/wagmiGenerated.d.ts.map +1 -1
  103. package/package/wagmiGenerated.ts +1992 -173
  104. package/package.json +7 -2
  105. package/remappings.txt +6 -1
  106. package/script/CoinsDeployerBase.sol +105 -10
  107. package/script/DeployDevFactory.s.sol +21 -0
  108. package/script/DeployHooks.s.sol +22 -0
  109. package/script/PrintUpgradeCommand.s.sol +13 -0
  110. package/script/Simulate.s.sol +4 -12
  111. package/script/TestBackingCoinSwap.s.sol +146 -0
  112. package/script/TestV4Swap.s.sol +136 -0
  113. package/script/UpgradeCoinImpl.sol +2 -2
  114. package/script/UpgradeFactoryImpl.s.sol +23 -0
  115. package/src/BaseCoin.sol +176 -0
  116. package/src/Coin.sol +93 -515
  117. package/src/CoinV4.sol +121 -0
  118. package/src/ZoraFactoryImpl.sol +257 -57
  119. package/src/hooks/ZoraV4CoinHook.sol +195 -0
  120. package/src/hooks/deployment/BaseCoinDeployHook.sol +62 -0
  121. package/src/hooks/deployment/BuySupplyWithSwapRouterHook.sol +80 -0
  122. package/src/interfaces/ICoin.sol +35 -39
  123. package/src/interfaces/ICoinDeployHook.sol +8 -0
  124. package/src/interfaces/ICoinV3.sol +71 -0
  125. package/src/interfaces/ICoinV4.sol +69 -0
  126. package/src/interfaces/IDeployedCoinVersionLookup.sol +11 -0
  127. package/src/interfaces/IMsgSender.sol +9 -0
  128. package/src/interfaces/IPoolConfigEncoding.sol +14 -0
  129. package/src/interfaces/ISwapPathRouter.sol +14 -0
  130. package/src/interfaces/ISwapRouter.sol +1 -35
  131. package/src/interfaces/IZoraFactory.sol +97 -7
  132. package/src/interfaces/IZoraV4CoinHook.sol +116 -0
  133. package/src/libs/CoinCommon.sol +15 -0
  134. package/src/libs/CoinConfigurationVersions.sol +116 -1
  135. package/src/{utils → libs}/CoinConstants.sol +11 -6
  136. package/src/libs/CoinDopplerMultiCurve.sol +134 -0
  137. package/src/libs/CoinDopplerUniV3.sol +19 -171
  138. package/src/libs/CoinRewards.sol +195 -0
  139. package/src/libs/CoinRewardsV4.sol +180 -0
  140. package/src/libs/CoinSetup.sol +40 -20
  141. package/src/libs/CoinSetupV3.sol +50 -0
  142. package/src/libs/DopplerMath.sol +156 -0
  143. package/src/libs/HooksDeployment.sol +84 -0
  144. package/src/libs/MarketConstants.sol +4 -0
  145. package/src/libs/PoolStateReader.sol +22 -0
  146. package/src/libs/UniV3BuySell.sol +231 -0
  147. package/src/libs/UniV3Errors.sol +11 -0
  148. package/src/libs/UniV4SwapHelper.sol +65 -0
  149. package/src/libs/UniV4SwapToCurrency.sol +109 -0
  150. package/src/libs/V4Liquidity.sol +129 -0
  151. package/src/types/PoolConfiguration.sol +15 -0
  152. package/src/utils/DeployedCoinVersionLookup.sol +52 -0
  153. package/src/version/ContractVersionBase.sol +1 -1
  154. package/test/Coin.t.sol +94 -101
  155. package/test/CoinDopplerUniV3.t.sol +35 -184
  156. package/test/CoinUniV4.t.sol +752 -0
  157. package/test/DeploymentHooks.t.sol +270 -0
  158. package/test/Factory.t.sol +84 -50
  159. package/test/MultiOwnable.t.sol +6 -3
  160. package/test/Upgrades.t.sol +68 -0
  161. package/test/mocks/MockERC20.sol +12 -0
  162. package/test/utils/BaseTest.sol +124 -59
  163. package/test/utils/DeployedCoinVersionLookup.t.sol +127 -0
  164. package/test/utils/FeeEstimatorHook.sol +84 -0
  165. package/test/utils/ProxyShim.sol +17 -0
  166. package/wagmi.config.ts +10 -9
  167. package/src/libs/CoinLegacy.sol +0 -48
@@ -1,7 +1,6 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.23;
3
3
 
4
- import {PoolConfiguration} from "../interfaces/ICoin.sol";
5
4
  import {TickMath} from "../utils/uniswap/TickMath.sol";
6
5
  import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
7
6
  import {ICoin} from "../interfaces/ICoin.sol";
@@ -11,13 +10,13 @@ import {FullMath} from "../utils/uniswap/FullMath.sol";
11
10
  import {SqrtPriceMath} from "../utils/uniswap/SqrtPriceMath.sol";
12
11
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
13
12
  import {IDopplerErrors} from "../interfaces/IDopplerErrors.sol";
13
+ import {DopplerMath} from "./DopplerMath.sol";
14
+ import {PoolConfiguration} from "../types/PoolConfiguration.sol";
14
15
 
15
16
  library CoinDopplerUniV3 {
16
17
  function setupPool(bool isCoinToken0, bytes memory poolConfig_) internal pure returns (uint160 sqrtPriceX96, PoolConfiguration memory poolConfiguration) {
17
- (, , int24 tickLower_, int24 tickUpper_, uint16 numDiscoveryPositions_, uint256 maxDiscoverySupplyShare_) = abi.decode(
18
- poolConfig_,
19
- (uint8, address, int24, int24, uint16, uint256)
20
- );
18
+ (, , int24 tickLower_, int24 tickUpper_, uint16 numDiscoveryPositions_, uint256 maxDiscoverySupplyShare_) = CoinConfigurationVersions
19
+ .decodeDopplerUniV3(poolConfig_);
21
20
 
22
21
  require(numDiscoveryPositions_ > 1 && numDiscoveryPositions_ <= 200, IDopplerErrors.NumDiscoveryPositionsOutOfRange());
23
22
 
@@ -25,178 +24,27 @@ library CoinDopplerUniV3 {
25
24
  revert IDopplerErrors.MaxShareToBeSoldExceeded(maxDiscoverySupplyShare_, MarketConstants.WAD);
26
25
  }
27
26
 
28
- int24 savedTickLower = isCoinToken0 ? tickLower_ : -tickUpper_;
29
- int24 savedTickUpper = isCoinToken0 ? tickUpper_ : -tickLower_;
27
+ uint256[] memory maxDiscoverySupplyShare = new uint256[](1);
28
+ uint16[] memory numDiscoveryPositions = new uint16[](1);
29
+ int24[] memory savedTickLower = new int24[](1);
30
+ int24[] memory savedTickUpper = new int24[](1);
30
31
 
31
- sqrtPriceX96 = TickMath.getSqrtPriceAtTick(isCoinToken0 ? savedTickLower : savedTickUpper);
32
+ maxDiscoverySupplyShare[0] = maxDiscoverySupplyShare_;
33
+ numDiscoveryPositions[0] = numDiscoveryPositions_;
34
+ savedTickLower[0] = isCoinToken0 ? tickLower_ : -tickUpper_;
35
+ savedTickUpper[0] = isCoinToken0 ? tickUpper_ : -tickLower_;
36
+
37
+ sqrtPriceX96 = TickMath.getSqrtPriceAtTick(isCoinToken0 ? savedTickLower[0] : savedTickUpper[0]);
32
38
 
33
39
  poolConfiguration = PoolConfiguration({
34
40
  version: CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION,
41
+ fee: MarketConstants.LP_FEE,
42
+ tickSpacing: MarketConstants.TICK_SPACING,
35
43
  tickLower: savedTickLower,
36
44
  tickUpper: savedTickUpper,
37
- numPositions: numDiscoveryPositions_,
38
- maxDiscoverySupplyShare: maxDiscoverySupplyShare_
45
+ numPositions: numDiscoveryPositions_ + 1, // Add one for the final tail position
46
+ maxDiscoverySupplyShare: maxDiscoverySupplyShare,
47
+ numDiscoveryPositions: numDiscoveryPositions
39
48
  });
40
49
  }
41
-
42
- function calculatePositions(bool isCoinToken0, PoolConfiguration memory poolConfiguration) internal pure returns (LpPosition[] memory positions) {
43
- positions = new LpPosition[](poolConfiguration.numPositions);
44
-
45
- uint256 discoverySupply = FullMath.mulDiv(MarketConstants.POOL_LAUNCH_SUPPLY, poolConfiguration.maxDiscoverySupplyShare, MarketConstants.WAD);
46
-
47
- (positions, discoverySupply) = calculateLogNormalDistribution(
48
- poolConfiguration.tickLower,
49
- poolConfiguration.tickUpper,
50
- MarketConstants.TICK_SPACING,
51
- isCoinToken0,
52
- discoverySupply,
53
- // Populate all positions before the last position (the tail position)
54
- poolConfiguration.numPositions - 1, // Only discovery positions
55
- positions
56
- );
57
-
58
- uint256 tailSupply = MarketConstants.POOL_LAUNCH_SUPPLY - discoverySupply;
59
-
60
- // Calculate the tail position (the last position in the array)
61
- positions[poolConfiguration.numPositions - 1] = calculateLpTail(
62
- poolConfiguration.tickLower,
63
- poolConfiguration.tickUpper,
64
- isCoinToken0,
65
- tailSupply,
66
- MarketConstants.TICK_SPACING
67
- );
68
- }
69
-
70
- /// @notice Calculates the distribution of liquidity positions across tick ranges.
71
- /// @dev For example, with 1000 tokens and 10 bins starting at tick 0:
72
- /// - Creates positions: [0,10], [1,10], [2,10], ..., [9,10]
73
- /// - Each position gets an equal share of tokens (100 tokens each)
74
- /// This creates a linear distribution of liquidity across the tick range
75
- /// @dev Changed in DopplerUniswapV3:
76
- /// - Added `LpPosition[] memory newPositions` as an input parameter, removing the internal allocation (`new LpPosition[](totalPositions + 1)`).
77
- /// - Removed the calculation and accumulation of the `reserves` variable entirely.
78
- /// - Return value changed from `(LpPosition[] memory, uint256)` (positions, reserves) to `(LpPosition[] memory, uint256)` (positions, totalAssetsSold).
79
- /// @param tickLower The lower tick of the LP range set
80
- /// @param tickUpper The upper tick of the LP range set
81
- /// @param tickSpacing The tick spacing of the LP range set
82
- /// @param isToken0 Whether the base asset is the token0 of the pair
83
- /// @param discoverySupply The total supply of the base asset to be sold
84
- /// @param totalPositions The total number of positions in the LP range set
85
- /// @param newPositions The array of new positions to be created
86
- /// @return newPositions The array of new positions to be created
87
- /// @return totalAssetsSold The total assets used in the LP range set
88
- function calculateLogNormalDistribution(
89
- int24 tickLower,
90
- int24 tickUpper,
91
- int24 tickSpacing,
92
- bool isToken0,
93
- uint256 discoverySupply,
94
- uint16 totalPositions,
95
- LpPosition[] memory newPositions
96
- ) internal pure returns (LpPosition[] memory, uint256) {
97
- int24 farTick = isToken0 ? tickUpper : tickLower;
98
- int24 closeTick = isToken0 ? tickLower : tickUpper;
99
-
100
- int24 spread = tickUpper - tickLower;
101
-
102
- uint160 farSqrtPriceX96 = TickMath.getSqrtPriceAtTick(farTick);
103
- uint256 amountPerPosition = FullMath.mulDiv(discoverySupply, MarketConstants.WAD, totalPositions * MarketConstants.WAD);
104
- uint256 totalAssetsSold;
105
-
106
- for (uint256 i; i < totalPositions; i++) {
107
- // calculate the ticks position * 1/n to optimize the division
108
- int24 startingTick = isToken0
109
- ? closeTick + int24(uint24(FullMath.mulDiv(i, uint256(uint24(spread)), totalPositions)))
110
- : closeTick - int24(uint24(FullMath.mulDiv(i, uint256(uint24(spread)), totalPositions)));
111
-
112
- // round the tick to the nearest bin
113
- startingTick = alignTickToTickSpacing(isToken0, startingTick, tickSpacing);
114
-
115
- if (startingTick != farTick) {
116
- uint160 startingSqrtPriceX96 = TickMath.getSqrtPriceAtTick(startingTick);
117
-
118
- // if discoverySupply is 0, we skip the liquidity calculation as we are burning max liquidity
119
- // in each position
120
- uint128 liquidity;
121
- if (discoverySupply != 0) {
122
- liquidity = isToken0
123
- ? LiquidityAmounts.getLiquidityForAmount0(startingSqrtPriceX96, farSqrtPriceX96, amountPerPosition)
124
- : LiquidityAmounts.getLiquidityForAmount1(farSqrtPriceX96, startingSqrtPriceX96, amountPerPosition);
125
-
126
- totalAssetsSold += (
127
- isToken0
128
- ? SqrtPriceMath.getAmount0Delta(startingSqrtPriceX96, farSqrtPriceX96, liquidity, true)
129
- : SqrtPriceMath.getAmount1Delta(farSqrtPriceX96, startingSqrtPriceX96, liquidity, true)
130
- );
131
- }
132
-
133
- newPositions[i] = LpPosition({
134
- tickLower: farSqrtPriceX96 < startingSqrtPriceX96 ? farTick : startingTick,
135
- tickUpper: farSqrtPriceX96 < startingSqrtPriceX96 ? startingTick : farTick,
136
- liquidity: liquidity
137
- });
138
- }
139
- }
140
-
141
- require(totalAssetsSold <= discoverySupply, IDopplerErrors.CannotMintZeroLiquidity());
142
-
143
- return (newPositions, totalAssetsSold);
144
- }
145
-
146
- /// @notice Calculates the final LP position that extends from the far tick to the pool's min/max tick
147
- /// @dev This position ensures price equivalence between Uniswap v2 and v3 pools beyond the LBP range
148
- /// @dev Changed in DopplerUniswapV3:
149
- /// - Removed parameters: `id`, `reserves`
150
- /// - Liquidity calculation is based *solely* on the provided `tailSupply` within the calculated tail tick range using `LiquidityAmounts.getLiquidityForAmount0` or `getLiquidityForAmount1`.
151
- function calculateLpTail(
152
- int24 tickLower,
153
- int24 tickUpper,
154
- bool isToken0,
155
- uint256 tailSupply,
156
- int24 tickSpacing
157
- ) internal pure returns (LpPosition memory lpTail) {
158
- int24 posTickLower = isToken0 ? tickUpper : alignTickToTickSpacing(false, TickMath.MIN_TICK, tickSpacing);
159
- int24 posTickUpper = isToken0 ? alignTickToTickSpacing(true, TickMath.MAX_TICK, tickSpacing) : tickLower;
160
-
161
- require(posTickLower < posTickUpper, IDopplerErrors.InvalidTickRangeMisordered(posTickLower, posTickUpper));
162
-
163
- // Calculate the sqrtPrices for the tail range boundaries
164
- uint160 sqrtPriceA = TickMath.getSqrtPriceAtTick(posTickLower);
165
- uint160 sqrtPriceB = TickMath.getSqrtPriceAtTick(posTickUpper);
166
-
167
- // Calculate liquidity only based on the tail range supply
168
- uint128 lpTailLiquidity = isToken0
169
- ? LiquidityAmounts.getLiquidityForAmount0(sqrtPriceA, sqrtPriceB, tailSupply)
170
- : LiquidityAmounts.getLiquidityForAmount1(sqrtPriceA, sqrtPriceB, tailSupply);
171
-
172
- lpTail = LpPosition({tickLower: posTickLower, tickUpper: posTickUpper, liquidity: lpTailLiquidity});
173
- }
174
-
175
- /// @notice Aligns a tick to the nearest tick spacing
176
- /// @dev The tickSpacing parameter cannot be zero
177
- /// @param isToken0 Whether the base asset is the token0 of the pair
178
- /// @param tick The tick to align
179
- /// @param tickSpacing The tick spacing of the pair
180
- /// @return alignedTick The aligned tick
181
- function alignTickToTickSpacing(bool isToken0, int24 tick, int24 tickSpacing) internal pure returns (int24) {
182
- if (isToken0) {
183
- // Round down if isToken0
184
- if (tick < 0) {
185
- // If the tick is negative, we round up (negatively) the negative result to round down
186
- return ((tick - tickSpacing + 1) / tickSpacing) * tickSpacing;
187
- } else {
188
- // Else if positive, we simply round down
189
- return (tick / tickSpacing) * tickSpacing;
190
- }
191
- } else {
192
- // Round up if isToken1
193
- if (tick < 0) {
194
- // If the tick is negative, we round down the negative result to round up
195
- return (tick / tickSpacing) * tickSpacing;
196
- } else {
197
- // Else if positive, we simply round up
198
- return ((tick + tickSpacing - 1) / tickSpacing) * tickSpacing;
199
- }
200
- }
201
- }
202
50
  }
@@ -0,0 +1,195 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {Address} from "@openzeppelin/contracts/utils/Address.sol";
6
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
7
+ import {IProtocolRewards} from "../interfaces/IProtocolRewards.sol";
8
+ import {ICoin} from "../interfaces/ICoin.sol";
9
+ import {CoinConstants} from "./CoinConstants.sol";
10
+ import {IWETH} from "../interfaces/IWETH.sol";
11
+
12
+ struct CoinConfig {
13
+ address protocolRewardRecipient;
14
+ address platformReferrer;
15
+ address payoutRecipient;
16
+ address protocolRewards;
17
+ }
18
+
19
+ library CoinRewards {
20
+ using SafeERC20 for IERC20;
21
+
22
+ /// @dev Handles sending ETH and ERC20 payouts and refunds to recipients
23
+ /// @param orderPayout The amount of currency to pay out
24
+ /// @param recipient The address to receive the payout
25
+ function handlePayout(uint256 orderPayout, address recipient, address currency, address weth) internal {
26
+ if (currency == weth) {
27
+ Address.sendValue(payable(recipient), orderPayout);
28
+ } else {
29
+ IERC20(currency).safeTransfer(recipient, orderPayout);
30
+ }
31
+ }
32
+
33
+ /// @dev Handles calculating and depositing fees to an escrow protocol rewards contract
34
+ function handleTradeRewards(uint256 totalValue, address _tradeReferrer, CoinConfig memory coinConfig, address currency, IWETH weth) internal {
35
+ address protocolRewardRecipient = coinConfig.protocolRewardRecipient;
36
+ address platformReferrer = coinConfig.platformReferrer;
37
+ address payoutRecipient = coinConfig.payoutRecipient;
38
+ IProtocolRewards protocolRewards = IProtocolRewards(coinConfig.protocolRewards);
39
+
40
+ if (_tradeReferrer == address(0)) {
41
+ _tradeReferrer = protocolRewardRecipient;
42
+ }
43
+
44
+ uint256 tokenCreatorFee = calculateReward(totalValue, CoinConstants.TOKEN_CREATOR_FEE_BPS);
45
+ uint256 platformReferrerFee = calculateReward(totalValue, CoinConstants.PLATFORM_REFERRER_FEE_BPS);
46
+ uint256 tradeReferrerFee = calculateReward(totalValue, CoinConstants.TRADE_REFERRER_FEE_BPS);
47
+ uint256 protocolFee = totalValue - tokenCreatorFee - platformReferrerFee - tradeReferrerFee;
48
+
49
+ if (currency == address(weth)) {
50
+ address[] memory recipients = new address[](4);
51
+ uint256[] memory amounts = new uint256[](4);
52
+ bytes4[] memory reasons = new bytes4[](4);
53
+
54
+ recipients[0] = payoutRecipient;
55
+ amounts[0] = tokenCreatorFee;
56
+ reasons[0] = bytes4(keccak256("COIN_CREATOR_REWARD"));
57
+
58
+ recipients[1] = platformReferrer;
59
+ amounts[1] = platformReferrerFee;
60
+ reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_REWARD"));
61
+
62
+ recipients[2] = _tradeReferrer;
63
+ amounts[2] = tradeReferrerFee;
64
+ reasons[2] = bytes4(keccak256("COIN_TRADE_REFERRER_REWARD"));
65
+
66
+ recipients[3] = protocolRewardRecipient;
67
+ amounts[3] = protocolFee;
68
+ reasons[3] = bytes4(keccak256("COIN_PROTOCOL_REWARD"));
69
+
70
+ IProtocolRewards(protocolRewards).depositBatch{value: totalValue}(recipients, amounts, reasons, "");
71
+ }
72
+
73
+ if (currency != address(weth)) {
74
+ IERC20(currency).safeTransfer(payoutRecipient, tokenCreatorFee);
75
+ IERC20(currency).safeTransfer(platformReferrer, platformReferrerFee);
76
+ IERC20(currency).safeTransfer(_tradeReferrer, tradeReferrerFee);
77
+ IERC20(currency).safeTransfer(protocolRewardRecipient, protocolFee);
78
+ }
79
+
80
+ emit ICoin.CoinTradeRewards(
81
+ payoutRecipient,
82
+ platformReferrer,
83
+ _tradeReferrer,
84
+ protocolRewardRecipient,
85
+ tokenCreatorFee,
86
+ platformReferrerFee,
87
+ tradeReferrerFee,
88
+ protocolFee,
89
+ currency
90
+ );
91
+ }
92
+
93
+ function calculateReward(uint256 amount, uint256 bps) internal pure returns (uint256) {
94
+ return (amount * bps) / 10_000;
95
+ }
96
+
97
+ function transferBothRewards(
98
+ address token0,
99
+ uint256 totalAmountToken0,
100
+ address token1,
101
+ uint256 totalAmountToken1,
102
+ address coin,
103
+ CoinConfig memory coinConfig,
104
+ address currency,
105
+ IWETH weth,
106
+ address doppler
107
+ ) internal returns (ICoin.MarketRewards memory rewards) {
108
+ rewards = transferMarketRewards(token0, currency, totalAmountToken0, rewards, coin, coinConfig, weth, doppler);
109
+ rewards = transferMarketRewards(token1, currency, totalAmountToken1, rewards, coin, coinConfig, weth, doppler);
110
+ }
111
+
112
+ struct Distribution {
113
+ bool isCurrency;
114
+ uint256 totalAmount;
115
+ uint256 creatorPayout;
116
+ uint256 platformReferrerPayout;
117
+ uint256 protocolPayout;
118
+ }
119
+
120
+ function transferMarketRewards(
121
+ address token,
122
+ address currency,
123
+ uint256 totalAmount,
124
+ ICoin.MarketRewards memory rewards,
125
+ address coin,
126
+ CoinConfig memory coinConfig,
127
+ IWETH weth,
128
+ address dopplerRecipient
129
+ ) internal returns (ICoin.MarketRewards memory) {
130
+ address payoutRecipient = coinConfig.payoutRecipient;
131
+ address platformReferrer = coinConfig.platformReferrer;
132
+ address protocolRewardRecipient = coinConfig.protocolRewardRecipient;
133
+ address protocolRewards = coinConfig.protocolRewards;
134
+
135
+ if (totalAmount > 0) {
136
+ uint256 dopplerPayout = calculateReward(totalAmount, CoinConstants.DOPPLER_MARKET_REWARD_BPS);
137
+ uint256 creatorPayout = calculateReward(totalAmount, CoinConstants.CREATOR_MARKET_REWARD_BPS);
138
+ uint256 platformReferrerPayout = calculateReward(totalAmount, CoinConstants.PLATFORM_REFERRER_MARKET_REWARD_BPS);
139
+ uint256 protocolPayout = totalAmount - creatorPayout - platformReferrerPayout - dopplerPayout;
140
+
141
+ bool isCurrency = token == currency;
142
+
143
+ if (token == address(weth)) {
144
+ IWETH(weth).withdraw(totalAmount);
145
+
146
+ address[] memory recipients = new address[](4);
147
+ recipients[0] = payoutRecipient;
148
+ recipients[1] = platformReferrer;
149
+ recipients[2] = protocolRewardRecipient;
150
+ recipients[3] = dopplerRecipient;
151
+
152
+ uint256[] memory amounts = new uint256[](4);
153
+ amounts[0] = creatorPayout;
154
+ amounts[1] = platformReferrerPayout;
155
+ amounts[2] = protocolPayout;
156
+ amounts[3] = dopplerPayout;
157
+
158
+ bytes4[] memory reasons = new bytes4[](4);
159
+ reasons[0] = bytes4(keccak256("COIN_CREATOR_MARKET_REWARD"));
160
+ reasons[1] = bytes4(keccak256("COIN_PLATFORM_REFERRER_MARKET_REWARD"));
161
+ reasons[2] = bytes4(keccak256("COIN_PROTOCOL_MARKET_REWARD"));
162
+ reasons[3] = bytes4(keccak256("COIN_DOPPLER_MARKET_REWARD"));
163
+
164
+ IProtocolRewards(protocolRewards).depositBatch{value: totalAmount}(recipients, amounts, reasons, "");
165
+ IProtocolRewards(protocolRewards).withdrawFor(dopplerRecipient, dopplerPayout);
166
+ } else {
167
+ if (!isCurrency) {
168
+ IERC20(coin).safeTransfer(payoutRecipient, creatorPayout);
169
+ IERC20(coin).safeTransfer(platformReferrer, platformReferrerPayout);
170
+ IERC20(coin).safeTransfer(protocolRewardRecipient, protocolPayout);
171
+ IERC20(coin).safeTransfer(dopplerRecipient, dopplerPayout);
172
+ } else {
173
+ IERC20(currency).safeTransfer(payoutRecipient, creatorPayout);
174
+ IERC20(currency).safeTransfer(platformReferrer, platformReferrerPayout);
175
+ IERC20(currency).safeTransfer(protocolRewardRecipient, protocolPayout);
176
+ IERC20(currency).safeTransfer(dopplerRecipient, dopplerPayout);
177
+ }
178
+ }
179
+
180
+ if (isCurrency) {
181
+ rewards.totalAmountCurrency = totalAmount;
182
+ rewards.creatorPayoutAmountCurrency = creatorPayout;
183
+ rewards.platformReferrerAmountCurrency = platformReferrerPayout;
184
+ rewards.protocolAmountCurrency = protocolPayout;
185
+ } else {
186
+ rewards.totalAmountCoin = totalAmount;
187
+ rewards.creatorPayoutAmountCoin = creatorPayout;
188
+ rewards.platformReferrerAmountCoin = platformReferrerPayout;
189
+ rewards.protocolAmountCoin = protocolPayout;
190
+ }
191
+ }
192
+
193
+ return rewards;
194
+ }
195
+ }
@@ -0,0 +1,180 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
5
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
6
+ import {LpPosition} from "../types/LpPosition.sol";
7
+ import {V4Liquidity} from "./V4Liquidity.sol";
8
+ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
9
+ import {IHasRewardsRecipients} from "../interfaces/ICoin.sol";
10
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
12
+ import {ICoin} from "../interfaces/ICoin.sol";
13
+ import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
14
+ import {UniV4SwapToCurrency} from "./UniV4SwapToCurrency.sol";
15
+ import {IHasSwapPath} from "../interfaces/ICoinV4.sol";
16
+
17
+ library CoinRewardsV4 {
18
+ using SafeERC20 for IERC20;
19
+
20
+ // creator gets 50% of the total fee
21
+ uint256 public constant CREATOR_REWARD_BPS = 5000;
22
+
23
+ // create referrer gets 15% of the total fee
24
+ uint256 public constant CREATE_REFERRAL_REWARD_BPS = 1500;
25
+
26
+ // trade referrer gets 10% of the total fee
27
+ uint256 public constant TRADE_REFERRAL_REWARD_BPS = 1500;
28
+
29
+ // doppler gets 5% of the total fee
30
+ uint256 public constant DOPPLER_REWARD_BPS = 500;
31
+
32
+ function getTradeReferral(bytes calldata hookData) internal pure returns (address) {
33
+ return hookData.length > 0 ? abi.decode(hookData, (address)) : address(0);
34
+ }
35
+
36
+ /// @notice Collects fees from LP positions, swaps them to target payout currency, and transfers to hook contract, so
37
+ /// that they can later be distributed as rewards.
38
+ /// @param poolManager The pool manager instance
39
+ /// @param key The pool key
40
+ /// @param positions The LP positions to collect fees from
41
+ /// @param payoutSwapPath The swap path to convert fees to target currency
42
+ /// @return fees0 The amount of fees collected in currency0
43
+ /// @return fees1 The amount of fees collected in currency1
44
+ /// @return receivedCurrency The final currency after swapping
45
+ /// @return receivedAmount The final amount after swapping
46
+ function collectFeesAndConvertToPayout(
47
+ IPoolManager poolManager,
48
+ PoolKey memory key,
49
+ LpPosition[] storage positions,
50
+ IHasSwapPath.PayoutSwapPath memory payoutSwapPath
51
+ ) internal returns (int128 fees0, int128 fees1, Currency receivedCurrency, uint128 receivedAmount) {
52
+ // Step 1: Collect accrued fees from all LP positions in both token0 and token1
53
+ (fees0, fees1) = V4Liquidity.collectFees(poolManager, key, positions);
54
+
55
+ // Step 2: Swap the collected fees through the specified path to convert them to the target payout currency
56
+ // This handles multi-hop swaps if needed (e.g. coin -> backingCoin -> backingCoin's currency)
57
+ (receivedCurrency, receivedAmount) = UniV4SwapToCurrency.swapToPath(
58
+ poolManager,
59
+ uint128(fees0),
60
+ uint128(fees1),
61
+ payoutSwapPath.currencyIn,
62
+ payoutSwapPath.path
63
+ );
64
+
65
+ // Step 3: Transfer the final converted currency amount to this contract for distribution
66
+ // This makes the tokens available for the subsequent reward distribution
67
+ if (receivedAmount > 0) {
68
+ poolManager.take(receivedCurrency, address(this), receivedAmount);
69
+ }
70
+ }
71
+
72
+ /// @notice Distributes collected market fees as rewards to various recipients including creator, referrers, protocol, and doppler
73
+ /// @dev Calculates reward amounts based on predefined basis points and transfers the specified currency to each recipient
74
+ /// @param currency The currency token to distribute as rewards (can be native ETH if address is zero)
75
+ /// @param fees The total amount of fees collected to be distributed
76
+ /// @param coin The coin contract instance that implements IHasRewardsRecipients to get recipient addresses
77
+ /// @param tradeReferrer The address of the trade referrer who should receive trade referral rewards (can be zero address)
78
+ function distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal {
79
+ address payoutRecipient = coin.payoutRecipient();
80
+ address platformReferrer = coin.platformReferrer();
81
+ address protocolRewardRecipient = coin.protocolRewardRecipient();
82
+ address doppler = coin.dopplerFeeRecipient();
83
+
84
+ MarketRewards memory rewards = _distributeCurrencyRewards(
85
+ currency,
86
+ fees,
87
+ payoutRecipient,
88
+ platformReferrer,
89
+ protocolRewardRecipient,
90
+ doppler,
91
+ tradeReferrer
92
+ );
93
+
94
+ IZoraV4CoinHook.MarketRewardsV4 memory marketRewards = IZoraV4CoinHook.MarketRewardsV4({
95
+ creatorPayoutAmountCurrency: rewards.creatorAmount,
96
+ creatorPayoutAmountCoin: 0,
97
+ platformReferrerAmountCurrency: rewards.platformReferrerAmount,
98
+ platformReferrerAmountCoin: 0,
99
+ tradeReferrerAmountCurrency: rewards.tradeReferrerAmount,
100
+ tradeReferrerAmountCoin: 0,
101
+ protocolAmountCurrency: rewards.protocolAmount,
102
+ protocolAmountCoin: 0,
103
+ dopplerAmountCurrency: rewards.dopplerAmount,
104
+ dopplerAmountCoin: 0
105
+ });
106
+
107
+ emit IZoraV4CoinHook.CoinMarketRewardsV4(
108
+ address(coin),
109
+ Currency.unwrap(currency),
110
+ payoutRecipient,
111
+ platformReferrer,
112
+ tradeReferrer,
113
+ protocolRewardRecipient,
114
+ doppler,
115
+ marketRewards
116
+ );
117
+ }
118
+
119
+ struct MarketRewards {
120
+ uint256 platformReferrerAmount;
121
+ uint256 tradeReferrerAmount;
122
+ uint256 protocolAmount;
123
+ uint256 creatorAmount;
124
+ uint256 dopplerAmount;
125
+ }
126
+
127
+ function _distributeCurrencyRewards(
128
+ Currency currency,
129
+ uint128 fee,
130
+ address payoutRecipient,
131
+ address platformReferrer,
132
+ address protocolRewardRecipient,
133
+ address doppler,
134
+ address tradeReferral
135
+ ) internal returns (MarketRewards memory rewards) {
136
+ rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0));
137
+
138
+ if (platformReferrer != address(0)) {
139
+ _transferCurrency(currency, rewards.platformReferrerAmount, platformReferrer);
140
+ }
141
+ if (tradeReferral != address(0)) {
142
+ _transferCurrency(currency, rewards.tradeReferrerAmount, tradeReferral);
143
+ }
144
+ _transferCurrency(currency, rewards.creatorAmount, payoutRecipient);
145
+ _transferCurrency(currency, rewards.dopplerAmount, doppler);
146
+ _transferCurrency(currency, rewards.protocolAmount, protocolRewardRecipient);
147
+ }
148
+
149
+ function _transferCurrency(Currency currency, uint256 amount, address to) internal {
150
+ if (amount == 0) {
151
+ return;
152
+ }
153
+
154
+ if (currency.isAddressZero()) {
155
+ (bool success, ) = payable(to).call{value: amount}("");
156
+ if (!success) {
157
+ revert ICoin.EthTransferFailed();
158
+ }
159
+ } else {
160
+ IERC20(Currency.unwrap(currency)).safeTransfer(to, amount);
161
+ }
162
+ }
163
+
164
+ function _computeMarketRewards(uint128 fee, bool hasTradeReferral, bool hasCreateReferral) internal pure returns (MarketRewards memory rewards) {
165
+ if (fee == 0) {
166
+ return rewards;
167
+ }
168
+
169
+ uint256 totalAmount = uint256(fee);
170
+ rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CREATE_REFERRAL_REWARD_BPS) : 0;
171
+ rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, TRADE_REFERRAL_REWARD_BPS) : 0;
172
+ rewards.creatorAmount = calculateReward(totalAmount, CREATOR_REWARD_BPS);
173
+ rewards.dopplerAmount = calculateReward(totalAmount, DOPPLER_REWARD_BPS);
174
+ rewards.protocolAmount = totalAmount - rewards.platformReferrerAmount - rewards.tradeReferrerAmount - rewards.creatorAmount - rewards.dopplerAmount;
175
+ }
176
+
177
+ function calculateReward(uint256 amount, uint256 bps) internal pure returns (uint256) {
178
+ return (amount * bps) / 10_000;
179
+ }
180
+ }
@@ -1,37 +1,57 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.23;
3
3
 
4
- import {PoolConfiguration} from "../interfaces/ICoin.sol";
5
- import {CoinLegacy} from "./CoinLegacy.sol";
6
- import {CoinDopplerUniV3} from "./CoinDopplerUniV3.sol";
4
+ import {PoolConfigurationV4} from "../interfaces/ICoin.sol";
7
5
  import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
6
+ import {ICoin} from "../interfaces/ICoin.sol";
7
+ import {CoinCommon} from "./CoinCommon.sol";
8
+ import {MarketConstants} from "./MarketConstants.sol";
9
+ import {TickMath} from "../utils/uniswap/TickMath.sol";
10
+ import {IPoolManager, PoolKey, Currency, IHooks} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
+ import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
12
+ import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
13
+ import {MarketConstants} from "./MarketConstants.sol";
8
14
  import {LpPosition} from "../types/LpPosition.sol";
15
+ import {CoinDopplerMultiCurve, PoolConfiguration} from "./CoinDopplerMultiCurve.sol";
16
+ import {CoinDopplerUniV3} from "./CoinDopplerUniV3.sol";
9
17
 
10
18
  library CoinSetup {
11
- error InvalidPoolVersion();
19
+ function generatePoolConfig(
20
+ address coin,
21
+ bytes memory poolConfig_
22
+ ) internal pure returns (uint8 version, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) {
23
+ // Extract version and currency from pool config
24
+ (version, currency) = CoinConfigurationVersions.decodeVersionAndCurrency(poolConfig_);
25
+
26
+ isCoinToken0 = CoinCommon.sortTokens(coin, currency);
27
+
28
+ (sqrtPriceX96, poolConfiguration) = setupPoolWithVersion(version, poolConfig_, isCoinToken0);
29
+ }
30
+
31
+ function buildPoolKey(address coin, address currency, bool isCoinToken0, IHooks hooks) internal pure returns (PoolKey memory poolKey) {
32
+ Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
33
+ Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
34
+
35
+ poolKey = PoolKey({
36
+ currency0: currency0,
37
+ currency1: currency1,
38
+ fee: MarketConstants.LP_FEE_V4,
39
+ tickSpacing: MarketConstants.TICK_SPACING,
40
+ hooks: hooks
41
+ });
42
+ }
12
43
 
13
44
  function setupPoolWithVersion(
14
45
  uint8 version,
15
46
  bytes memory poolConfig_,
16
- bool isCoinToken0,
17
- address weth
47
+ bool isCoinToken0
18
48
  ) internal pure returns (uint160 sqrtPriceX96, PoolConfiguration memory poolConfiguration) {
19
- if (version == CoinConfigurationVersions.LEGACY_POOL_VERSION) {
20
- (sqrtPriceX96, poolConfiguration) = CoinLegacy.setupPool(isCoinToken0, poolConfig_, weth);
21
- } else if (version == CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION) {
49
+ if (version == CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION) {
22
50
  (sqrtPriceX96, poolConfiguration) = CoinDopplerUniV3.setupPool(isCoinToken0, poolConfig_);
51
+ } else if (version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION) {
52
+ (sqrtPriceX96, poolConfiguration) = CoinDopplerMultiCurve.setupPool(isCoinToken0, poolConfig_);
23
53
  } else {
24
- revert InvalidPoolVersion();
25
- }
26
- }
27
-
28
- function calculatePositions(bool isCoinToken0, PoolConfiguration memory poolConfiguration) internal pure returns (LpPosition[] memory positions) {
29
- if (poolConfiguration.version == CoinConfigurationVersions.LEGACY_POOL_VERSION) {
30
- positions = CoinLegacy.calculatePositions(isCoinToken0, poolConfiguration);
31
- } else if (poolConfiguration.version == CoinConfigurationVersions.DOPPLER_UNI_V3_POOL_VERSION) {
32
- positions = CoinDopplerUniV3.calculatePositions(isCoinToken0, poolConfiguration);
33
- } else {
34
- revert InvalidPoolVersion();
54
+ revert ICoin.InvalidPoolVersion();
35
55
  }
36
56
  }
37
57
  }