@zoralabs/coins 2.4.0 → 2.5.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 (129) hide show
  1. package/.abi-stability +923 -0
  2. package/.turbo/turbo-build$colon$js.log +110 -116
  3. package/CHANGELOG.md +25 -0
  4. package/abis/Address.json +0 -16
  5. package/abis/BaseCoin.json +18 -0
  6. package/abis/BuySupplyWithSwapRouterHook.json +0 -27
  7. package/abis/BuySupplyWithV4SwapHook.json +0 -32
  8. package/abis/Clones.json +1 -1
  9. package/abis/CoinDopplerMultiCurve.json +109 -0
  10. package/abis/ContentCoin.json +18 -0
  11. package/abis/Create2.json +0 -21
  12. package/abis/CreatorCoin.json +18 -0
  13. package/abis/ERC1967Proxy.json +1 -1
  14. package/abis/ERC1967Utils.json +0 -45
  15. package/abis/{UpgradeCoinImpl.json → Errors.json} +14 -10
  16. package/abis/{MockERC20.json → IERC1363.json} +134 -104
  17. package/abis/IERC1967.json +47 -0
  18. package/abis/IERC20.json +0 -36
  19. package/abis/IHasCreationInfo.json +20 -0
  20. package/abis/IProtocolRewards.json +0 -258
  21. package/abis/{Script.json → ISupportsLimitOrderFill.json} +2 -2
  22. package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
  23. package/abis/IZoraV4CoinHook.json +10 -0
  24. package/abis/ProxyShim.json +15 -16
  25. package/abis/SafeCast.json +51 -0
  26. package/abis/{AddressConstants.json → SafeCast160.json} +1 -1
  27. package/abis/Strings.json +10 -0
  28. package/abis/UUPSUpgradeable.json +1 -1
  29. package/abis/V3ToV4SwapLib.json +28 -0
  30. package/abis/ZoraFactory.json +1 -1
  31. package/abis/ZoraFactoryImpl.json +22 -6
  32. package/abis/ZoraV4CoinHook.json +20 -48
  33. package/dist/index.cjs +980 -43
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +978 -41
  36. package/dist/index.js.map +1 -1
  37. package/dist/wagmiGenerated.d.ts +1501 -76
  38. package/dist/wagmiGenerated.d.ts.map +1 -1
  39. package/package/wagmiGenerated.ts +981 -44
  40. package/package.json +11 -9
  41. package/remappings.txt +2 -1
  42. package/src/BaseCoin.sol +32 -2
  43. package/src/ZoraFactoryImpl.sol +8 -0
  44. package/src/deployment/ForkedCoinsAddresses.sol +54 -0
  45. package/src/hooks/ZoraV4CoinHook.sol +131 -20
  46. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +20 -142
  47. package/src/interfaces/IHasCreationInfo.sol +12 -0
  48. package/src/interfaces/ISupportsLimitOrderFill.sol +11 -0
  49. package/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol +21 -0
  50. package/src/interfaces/IZoraV4CoinHook.sol +6 -0
  51. package/src/libs/CoinConstants.sol +22 -0
  52. package/src/libs/CoinDopplerMultiCurve.sol +1 -1
  53. package/src/libs/CoinRewardsV4.sol +0 -1
  54. package/src/libs/CoinSetup.sol +7 -1
  55. package/src/libs/HooksDeployment.sol +20 -8
  56. package/src/libs/UniV4SwapHelper.sol +35 -0
  57. package/src/libs/V3ToV4SwapLib.sol +265 -0
  58. package/src/version/ContractVersionBase.sol +1 -1
  59. package/test/BuySupplyWithV4SwapHook.t.sol +4 -3
  60. package/test/Coin.t.sol +7 -1
  61. package/test/CoinUniV4.t.sol +6 -1
  62. package/test/ContentCoinRewards.t.sol +6 -1
  63. package/test/CreatorCoin.t.sol +3 -1
  64. package/test/CreatorCoinRewards.t.sol +4 -1
  65. package/test/Factory.t.sol +20 -7
  66. package/test/HooksDeployment.t.sol +16 -3
  67. package/test/LaunchFee.t.sol +286 -0
  68. package/test/LiquidityMigration.t.sol +52 -44
  69. package/test/MultiOwnable.t.sol +2 -1
  70. package/test/Upgrades.t.sol +110 -81
  71. package/test/V4Liquidity.t.sol +1 -1
  72. package/test/mocks/MockSwapRouter.sol +33 -0
  73. package/test/mocks/MockZoraLimitOrderBook.sol +14 -0
  74. package/test/utils/BaseTest.sol +14 -448
  75. package/test/utils/FeeEstimatorHook.sol +6 -2
  76. package/test/utils/V4TestSetup.sol +595 -0
  77. package/wagmi.config.ts +1 -1
  78. package/abis/BaseTest.json +0 -718
  79. package/abis/DeterministicDeployerAndCaller.json +0 -315
  80. package/abis/DeterministicUUPSProxyDeployer.json +0 -167
  81. package/abis/EIP712.json +0 -67
  82. package/abis/ERC20.json +0 -310
  83. package/abis/FeeEstimatorHook.json +0 -1938
  84. package/abis/IERC721.json +0 -287
  85. package/abis/IERC721Enumerable.json +0 -343
  86. package/abis/IERC721Metadata.json +0 -332
  87. package/abis/IERC721TokenReceiver.json +0 -36
  88. package/abis/IImmutableCreate2Factory.json +0 -93
  89. package/abis/IMulticall3.json +0 -440
  90. package/abis/ISafe.json +0 -15
  91. package/abis/ISymbol.json +0 -15
  92. package/abis/IUniswapV4Router04.json +0 -484
  93. package/abis/IUniversalRouter.json +0 -61
  94. package/abis/IV4Quoter.json +0 -310
  95. package/abis/ImmutableCreate2FactoryUtils.json +0 -15
  96. package/abis/LibString.json +0 -7
  97. package/abis/Math.json +0 -7
  98. package/abis/MockAirlock.json +0 -39
  99. package/abis/MockERC721.json +0 -350
  100. package/abis/ProtocolRewards.json +0 -494
  101. package/abis/ShortStrings.json +0 -18
  102. package/abis/SimpleERC20.json +0 -326
  103. package/abis/StdAssertions.json +0 -379
  104. package/abis/StdInvariant.json +0 -180
  105. package/abis/Test.json +0 -570
  106. package/abis/VmContractHelper235.json +0 -233
  107. package/abis/VmContractHelper242.json +0 -233
  108. package/abis/stdError.json +0 -119
  109. package/abis/stdStorageSafe.json +0 -52
  110. package/addresses/8453.json +0 -13
  111. package/addresses/84532.json +0 -10
  112. package/deterministicConfig/deployerAndCaller.json +0 -5
  113. package/deterministicConfig/zoraFactory.json +0 -8
  114. package/script/Deploy.s.sol +0 -23
  115. package/script/DeployAutoSwapper.s.sol +0 -30
  116. package/script/DeployDevFactory.s.sol +0 -21
  117. package/script/DeployPostDeploymentHooks.s.sol +0 -20
  118. package/script/DeployTrustedMsgSenderLookup.s.sol +0 -20
  119. package/script/DeployUpgradeGate.s.sol +0 -21
  120. package/script/GenerateDeterministicParams.s.sol +0 -43
  121. package/script/PrintRegisterUpgradePath.s.sol +0 -28
  122. package/script/PrintUpgradeCommand.s.sol +0 -13
  123. package/script/TestBackingCoinSwap.s.sol +0 -144
  124. package/script/TestV4Swap.s.sol +0 -133
  125. package/script/UpgradeCoinImpl.sol +0 -23
  126. package/script/UpgradeFactoryImpl.s.sol +0 -28
  127. package/script/UpgradeHooks.s.sol +0 -23
  128. package/src/deployment/CoinsDeployerBase.sol +0 -297
  129. /package/{test → src}/utils/ProxyShim.sol +0 -0
@@ -0,0 +1,12 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ /// @title IHasCreationInfo
5
+ /// @notice Interface for coins that support launch fee functionality
6
+ /// @dev Legacy coins that don't implement this interface will use the normal LP fee
7
+ interface IHasCreationInfo {
8
+ /// @notice Returns creation info for the coin used by the launch fee calculation
9
+ /// @return creationTimestamp The block.timestamp when the coin was initialized
10
+ /// @return isDeploying True if the coin is being deployed (transient), false otherwise
11
+ function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying);
12
+ }
@@ -0,0 +1,11 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ /// @title ISupportsLimitOrderFill
5
+ /// @notice Marker interface for hooks that handle limit order filling in their afterSwap
6
+ /// @dev Hooks implementing this interface should handle zoraLimitOrderBook.fill() in afterSwap.
7
+ /// Use ERC165's supportsInterface to declare support: supportsInterface(type(ISupportsLimitOrderFill).interfaceId)
8
+
9
+ interface ISupportsLimitOrderFill {
10
+ function supportsLimitOrderFill() external pure returns (bool);
11
+ }
@@ -0,0 +1,21 @@
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
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11
+
12
+ interface IZoraLimitOrderBookCoinsInterface {
13
+ /// @notice Fills limit orders within a tick window.
14
+ /// @param key Pool key whose orders should be processed.
15
+ /// @param isCurrency0 Whether currency0 orders are targeted; otherwise currency1.
16
+ /// @param startTick Inclusive starting tick. Use `-type(int24).max` for the default lower bound.
17
+ /// @param endTick Inclusive ending tick. Use `type(int24).max` for the default upper bound.
18
+ /// @param maxFillCount Maximum orders to fill in this pass.
19
+ /// @param fillReferral Address to receive accrued LP fees; use address(0) to give fees to maker.
20
+ function fill(PoolKey calldata key, bool isCurrency0, int24 startTick, int24 endTick, uint256 maxFillCount, address fillReferral) external;
21
+ }
@@ -57,6 +57,12 @@ interface IZoraV4CoinHook is IUpgradeableV4Hook {
57
57
  /// @notice Thrown when a non-coin is used to access the functionality of a coin.
58
58
  error OnlyCoin(address caller, address expectedCoin);
59
59
 
60
+ /// @notice Thrown when the Zora limit order book is the zero address.
61
+ error ZoraLimitOrderBookCannotBeZeroAddress();
62
+
63
+ /// @notice Thrown when the Zora hook registry is the zero address.
64
+ error ZoraHookRegistryCannotBeZeroAddress();
65
+
60
66
  /// @notice The pool coin struct. Lists all the contract-created positions for the coin.
61
67
  struct PoolCoin {
62
68
  /// @notice The address of the coin.
@@ -8,6 +8,12 @@
8
8
  pragma solidity ^0.8.23;
9
9
 
10
10
  library CoinConstants {
11
+ /// @dev Slot for transiently storing the tick before a swap.
12
+ bytes32 internal constant _BEFORE_SWAP_TICK_SLOT = keccak256("ZoraV4CoinHook.beforeSwap.tick");
13
+
14
+ /// @dev Constant used to indicate the max fill count for limit orders
15
+ uint256 internal constant SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT = 0;
16
+
11
17
  /// @dev Constant used to increase precision during calculations
12
18
  uint256 internal constant WAD = 1e18;
13
19
 
@@ -47,6 +53,22 @@ library CoinConstants {
47
53
  /// @dev 10000 basis points = 1%
48
54
  uint24 internal constant LP_FEE_V4 = 10_000;
49
55
 
56
+ /// @notice Flag to enable dynamic fees for the pool
57
+ /// @dev When set in pool key fee, enables hook to override fee per-swap
58
+ uint24 internal constant DYNAMIC_FEE_FLAG = 0x800000;
59
+
60
+ /// @notice Flag to override the fee in beforeSwap return value
61
+ /// @dev Combined with fee value to signal V4 to use the returned fee
62
+ uint24 internal constant OVERRIDE_FEE_FLAG = 0x400000;
63
+
64
+ /// @notice Starting fee for launch fee (99%)
65
+ /// @dev 990,000 pips = 99% (1,000,000 pips = 100%)
66
+ uint24 internal constant LAUNCH_FEE_START = 990_000;
67
+
68
+ /// @notice Duration over which launch fee decays from start to end fee
69
+ /// @dev 10 seconds
70
+ uint256 internal constant LAUNCH_FEE_DURATION = 10 seconds;
71
+
50
72
  /// @notice The spacing for 1% pools
51
73
  /// @dev 200 ticks
52
74
  int24 internal constant TICK_SPACING = 200;
@@ -105,7 +105,7 @@ library CoinDopplerMultiCurve {
105
105
  bool isCoinToken0,
106
106
  PoolConfiguration memory poolConfiguration,
107
107
  uint256 totalSupply
108
- ) internal pure returns (LpPosition[] memory positions) {
108
+ ) external pure returns (LpPosition[] memory positions) {
109
109
  positions = new LpPosition[](poolConfiguration.numPositions);
110
110
 
111
111
  uint256 discoverySupply;
@@ -27,7 +27,6 @@ import {IHasRewardsRecipients} from "../interfaces/IHasRewardsRecipients.sol";
27
27
  import {ICoin} from "../interfaces/ICoin.sol";
28
28
  import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
29
29
  import {IHasSwapPath} from "../interfaces/ICoin.sol";
30
- import {V4Liquidity} from "./V4Liquidity.sol";
31
30
  import {UniV4SwapToCurrency} from "./UniV4SwapToCurrency.sol";
32
31
  import {ICreatorCoinHook} from "../interfaces/ICreatorCoinHook.sol";
33
32
  import {IHasCoinType} from "../interfaces/ICoin.sol";
@@ -36,7 +36,13 @@ library CoinSetup {
36
36
  Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
37
37
  Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
38
38
 
39
- poolKey = PoolKey({currency0: currency0, currency1: currency1, fee: CoinConstants.LP_FEE_V4, tickSpacing: CoinConstants.TICK_SPACING, hooks: hooks});
39
+ poolKey = PoolKey({
40
+ currency0: currency0,
41
+ currency1: currency1,
42
+ fee: CoinConstants.DYNAMIC_FEE_FLAG,
43
+ tickSpacing: CoinConstants.TICK_SPACING,
44
+ hooks: hooks
45
+ });
40
46
  }
41
47
 
42
48
  function setupPoolWithVersion(
@@ -63,7 +63,7 @@ library HooksDeployment {
63
63
  bytes32 constant VALID_CREATOR_COIN_SALT = 0x00000000000000000000000000000000000000000000000000000000000031af;
64
64
 
65
65
  function mineForSalt(address deployer, bytes memory hookCreationCode) internal view returns (address hookAddress, bytes32 salt) {
66
- uint160 flags = uint160(Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
66
+ uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG) ^ (0x4444 << 144);
67
67
  return HookMinerWithCreationCodeArgs.find(deployer, flags, hookCreationCode);
68
68
  }
69
69
 
@@ -88,9 +88,11 @@ library HooksDeployment {
88
88
  address poolManager,
89
89
  address coinVersionLookup,
90
90
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
91
- address upgradeGate
91
+ address upgradeGate,
92
+ address orderFiller,
93
+ address hookRegistry
92
94
  ) internal returns (address hookAddress, bytes32 salt) {
93
- bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
95
+ bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
94
96
  (salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
95
97
  hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
96
98
  }
@@ -133,18 +135,26 @@ library HooksDeployment {
133
135
  address poolManager,
134
136
  address coinVersionLookup,
135
137
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
136
- address upgradeGate
138
+ address upgradeGate,
139
+ address orderFiller,
140
+ address hookRegistry
137
141
  ) internal pure returns (bytes memory) {
138
- return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
142
+ return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
139
143
  }
140
144
 
141
145
  function makeHookCreationCode(
142
146
  address poolManager,
143
147
  address coinVersionLookup,
144
148
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
145
- address upgradeGate
149
+ address upgradeGate,
150
+ address orderFiller,
151
+ address hookRegistry
146
152
  ) internal pure returns (bytes memory) {
147
- return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate));
153
+ return
154
+ abi.encodePacked(
155
+ type(ZoraV4CoinHook).creationCode,
156
+ hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry)
157
+ );
148
158
  }
149
159
 
150
160
  /// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
@@ -154,9 +164,11 @@ library HooksDeployment {
154
164
  address coinVersionLookup,
155
165
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
156
166
  address upgradeGate,
167
+ address orderFiller,
168
+ address hookRegistry,
157
169
  bytes32 salt
158
170
  ) internal returns (IHooks hook) {
159
- bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
171
+ bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate, orderFiller, hookRegistry);
160
172
  return deployHookWithSalt(creationCode, salt);
161
173
  }
162
174
 
@@ -22,6 +22,41 @@ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
22
22
  import {PathKey} from "@uniswap/v4-periphery/src/libraries/PathKey.sol";
23
23
 
24
24
  library UniV4SwapHelper {
25
+ function buildExactInputMultiSwapCommand(
26
+ address currencyIn,
27
+ uint128 amountIn,
28
+ PoolKey[] memory keys,
29
+ uint128 minAmountOut,
30
+ bytes[] memory hopHookData
31
+ ) internal pure returns (bytes memory commands, bytes[] memory inputs) {
32
+ require(keys.length > 0 && hopHookData.length == keys.length, "invalid lengths");
33
+
34
+ PathKey[] memory path = new PathKey[](keys.length);
35
+
36
+ Currency currency = Currency.wrap(currencyIn);
37
+ Currency finalCurrencyOut;
38
+
39
+ for (uint256 i; i < keys.length; ++i) {
40
+ Currency out = currency == keys[i].currency0 ? keys[i].currency1 : keys[i].currency0;
41
+ path[i] = PathKey({intermediateCurrency: out, fee: keys[i].fee, tickSpacing: keys[i].tickSpacing, hooks: keys[i].hooks, hookData: hopHookData[i]});
42
+ currency = out;
43
+ finalCurrencyOut = out;
44
+ }
45
+
46
+ bytes memory actions = abi.encodePacked(uint8(Actions.SWAP_EXACT_IN), uint8(Actions.SETTLE), uint8(Actions.TAKE_ALL));
47
+ bytes[] memory params = new bytes[](3);
48
+ params[0] = abi.encode( // 1) SWAP_EXACT_IN({ currencyIn, path, amountIn, amountOutMinimum })
49
+ IV4Router.ExactInputParams({currencyIn: Currency.wrap(currencyIn), path: path, amountIn: amountIn, amountOutMinimum: minAmountOut})
50
+ );
51
+ params[1] = abi.encode(currencyIn, amountIn, true); // 2) SETTLE(tokenIn, amountIn, payerIsUser=true) — pulls from user via Permit2
52
+ params[2] = abi.encode(Currency.unwrap(finalCurrencyOut), minAmountOut); // 3) TAKE_ALL(finalCurrencyOut, minOut)
53
+
54
+ commands = abi.encodePacked(uint8(Commands.V4_SWAP));
55
+
56
+ inputs = new bytes[](1);
57
+ inputs[0] = abi.encode(actions, params);
58
+ }
59
+
25
60
  function buildExactInputSingleSwapCommand(
26
61
  address currencyIn,
27
62
  uint128 amountIn,
@@ -0,0 +1,265 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.23;
3
+
4
+ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5
+ import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
6
+ import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
7
+ import {IPoolManager, SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
8
+ import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
9
+ import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
10
+ import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
11
+ import {ISwapRouter} from "../interfaces/ISwapRouter.sol";
12
+ import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
13
+ import {IAllowanceTransfer} from "permit2/src/interfaces/IAllowanceTransfer.sol";
14
+ import {SafeCast160} from "permit2/src/libraries/SafeCast160.sol";
15
+
16
+ /// @title V3ToV4SwapLib
17
+ /// @notice Shared library for executing V3-to-V4 swap routing
18
+ /// @dev Provides common functionality for:
19
+ /// - V3 route validation and connection to V4 routes
20
+ /// - Input currency validation and transfer (ETH vs ERC20)
21
+ /// - V3 swap execution via ISwapRouter.exactInput()
22
+ /// - V4 multi-hop swap execution
23
+ /// - Delta settlement with poolManager
24
+ /// - V3 route parsing utilities
25
+ library V3ToV4SwapLib {
26
+ using SafeERC20 for IERC20;
27
+ using BalanceDeltaLibrary for BalanceDelta;
28
+ using CurrencyLibrary for Currency;
29
+ using Path for bytes;
30
+ using SafeCast160 for uint256;
31
+
32
+ // ============ ERRORS ============
33
+
34
+ error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
35
+ error V3RouteCannotStartWithInputCurrency();
36
+ error V3RouteDoesNotConnectToV4RouteStart();
37
+
38
+ // ============ STRUCTS ============
39
+
40
+ /// @notice Parameters for V3 swap execution
41
+ struct V3SwapParams {
42
+ bytes v3Route; // V3 route path
43
+ address inputCurrency; // Input currency (address(0) for ETH)
44
+ uint256 inputAmount; // Amount of input currency
45
+ address recipient; // Recipient of swap output
46
+ }
47
+
48
+ /// @notice Parameters for V4 multi-hop swap execution
49
+ struct V4SwapParams {
50
+ PoolKey[] v4Route; // Array of pool keys to swap through
51
+ uint256 amountIn; // Starting amount
52
+ Currency startingCurrency; // Starting currency
53
+ }
54
+
55
+ /// @notice Result from V4 multi-hop swap
56
+ struct V4SwapResult {
57
+ uint128 outputAmount; // Final output amount
58
+ Currency outputCurrency; // Final output currency
59
+ BalanceDelta targetPoolDelta; // Delta from final (target) pool swap
60
+ }
61
+
62
+ // ============ VALIDATION ============
63
+
64
+ /// @notice Validates that V3 route output connects to V4 route start
65
+ /// @param v3Route The V3 route path (empty if no V3 swap)
66
+ /// @param inputCurrency The input currency for the swap
67
+ /// @param v4Route The V4 route (first pool must accept V3 output or input currency)
68
+ function validateRoutes(bytes memory v3Route, address inputCurrency, PoolKey[] memory v4Route) internal pure {
69
+ if (v4Route.length == 0) {
70
+ return; // No V4 route to validate
71
+ }
72
+
73
+ // Determine what currency should be the input to the V4 route
74
+ address v4InputCurrency;
75
+ if (v3Route.length == 0) {
76
+ // No V3 swap - input currency should directly match V4 route start
77
+ v4InputCurrency = inputCurrency;
78
+ } else {
79
+ // V3 swap exists - V3 output should match V4 route start
80
+ v4InputCurrency = getV3RouteOutputCurrency(v3Route);
81
+ }
82
+
83
+ PoolKey memory firstPool = v4Route[0];
84
+
85
+ require(
86
+ v4InputCurrency == Currency.unwrap(firstPool.currency0) || v4InputCurrency == Currency.unwrap(firstPool.currency1),
87
+ V3RouteDoesNotConnectToV4RouteStart()
88
+ );
89
+ }
90
+
91
+ /// @notice Validates and transfers input currency from sender to contract
92
+ /// @param inputCurrency The input currency address (address(0) for ETH)
93
+ /// @param inputAmount The amount to transfer
94
+ /// @param from The address to transfer from
95
+ /// @param msgValue The msg.value sent with the transaction
96
+ function validateAndTransferInputCurrency(address inputCurrency, uint256 inputAmount, address from, uint256 msgValue) internal {
97
+ if (inputCurrency == address(0)) {
98
+ // ETH payment
99
+ require(msgValue == inputAmount, InsufficientInputCurrency(inputAmount, msgValue));
100
+ } else {
101
+ // ERC20 payment
102
+ uint256 allowanceAmount = IERC20(inputCurrency).allowance(from, address(this));
103
+ require(allowanceAmount >= inputAmount, InsufficientInputCurrency(inputAmount, allowanceAmount));
104
+
105
+ uint256 balanceAmount = IERC20(inputCurrency).balanceOf(from);
106
+ require(balanceAmount >= inputAmount, InsufficientInputCurrency(inputAmount, balanceAmount));
107
+
108
+ IERC20(inputCurrency).safeTransferFrom(from, address(this), inputAmount);
109
+ }
110
+ }
111
+
112
+ /// @notice Validates and transfers input currency from sender using Permit2
113
+ /// @param permit2 The Permit2 contract
114
+ /// @param inputCurrency The input currency address (address(0) for ETH)
115
+ /// @param inputAmount The amount to transfer
116
+ /// @param from The address to transfer from
117
+ /// @param to The address to transfer to (recipient)
118
+ /// @param msgValue The msg.value sent with the transaction
119
+ function permit2TransferFrom(IAllowanceTransfer permit2, address inputCurrency, uint256 inputAmount, address from, address to, uint256 msgValue) internal {
120
+ if (inputCurrency == address(0)) {
121
+ // ETH payment - no Permit2 needed
122
+ require(msgValue == inputAmount, InsufficientInputCurrency(inputAmount, msgValue));
123
+ } else {
124
+ // ERC20 payment via Permit2
125
+ require(msgValue == 0, InsufficientInputCurrency(0, msgValue));
126
+ permit2.transferFrom(from, to, inputAmount.toUint160(), inputCurrency);
127
+ }
128
+ }
129
+
130
+ // ============ V3 SWAP LOGIC ============
131
+
132
+ /// @notice Executes a V3 swap if v3Route is provided, otherwise returns input
133
+ /// @param swapRouter The Uniswap V3 swap router
134
+ /// @param params The V3 swap parameters
135
+ /// @return amountCurrency The amount received from V3 swap (or input if no swap)
136
+ /// @return currencyReceived The currency received (output of V3 or input if no swap)
137
+ function executeV3Swap(ISwapRouter swapRouter, V3SwapParams memory params) internal returns (uint256 amountCurrency, address currencyReceived) {
138
+ if (params.v3Route.length == 0) {
139
+ // No V3 swap needed - return input directly
140
+ return (params.inputAmount, params.inputCurrency);
141
+ }
142
+
143
+ // Handle ERC20 input - approve swapRouter to spend tokens
144
+ if (params.inputCurrency != address(0)) {
145
+ IERC20(params.inputCurrency).safeIncreaseAllowance(address(swapRouter), params.inputAmount);
146
+ }
147
+
148
+ // Build swap router call for exactInput
149
+ ISwapRouter.ExactInputParams memory swapParams = ISwapRouter.ExactInputParams({
150
+ path: params.v3Route,
151
+ recipient: params.recipient,
152
+ amountIn: params.inputAmount,
153
+ amountOutMinimum: 0 // Slippage protection should be handled at higher level
154
+ });
155
+
156
+ // Conditional value passing - ETH if inputCurrency is address(0), otherwise 0
157
+ uint256 value = params.inputCurrency == address(0) ? params.inputAmount : 0;
158
+ amountCurrency = swapRouter.exactInput{value: value}(swapParams);
159
+ currencyReceived = getV3RouteOutputCurrency(params.v3Route);
160
+ }
161
+
162
+ // ============ V4 SWAP LOGIC ============
163
+
164
+ /// @notice Executes a multi-hop V4 swap through multiple pools
165
+ /// @param poolManager The Uniswap V4 pool manager
166
+ /// @param params The V4 swap parameters
167
+ /// @return result The swap result containing output amount and currency
168
+ function executeV4MultiHopSwap(IPoolManager poolManager, V4SwapParams memory params) internal returns (V4SwapResult memory result) {
169
+ Currency currentCurrency = params.startingCurrency;
170
+ uint128 currentAmount = uint128(params.amountIn);
171
+ BalanceDelta lastDelta;
172
+
173
+ // Execute swaps through the route
174
+ for (uint256 i = 0; i < params.v4Route.length; i++) {
175
+ PoolKey memory poolKey = params.v4Route[i];
176
+
177
+ // Determine swap direction based on current currency
178
+ bool zeroForOne = currentCurrency == poolKey.currency0;
179
+
180
+ lastDelta = poolManager.swap(
181
+ poolKey,
182
+ SwapParams(zeroForOne, -(int128(currentAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
183
+ ""
184
+ );
185
+
186
+ // Extract output amount from delta
187
+ uint128 outputAmount = zeroForOne ? uint128(lastDelta.amount1()) : uint128(lastDelta.amount0());
188
+
189
+ // Update for next iteration
190
+ currentAmount = outputAmount;
191
+ currentCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
192
+ }
193
+
194
+ result.outputAmount = currentAmount;
195
+ result.outputCurrency = currentCurrency;
196
+ result.targetPoolDelta = lastDelta;
197
+ }
198
+
199
+ // ============ DELTA SETTLEMENT ============
200
+
201
+ /// @notice Settles currency deltas with the pool manager
202
+ /// @param poolManager The Uniswap V4 pool manager
203
+ /// @param inputCurrency The input currency to settle
204
+ /// @param outputCurrency The output currency to take
205
+ /// @param to The recipient of the output currency
206
+ /// @param inputAmount The amount of input currency to settle
207
+ /// @param outputAmount The amount of output currency to take
208
+ function settleDeltas(
209
+ IPoolManager poolManager,
210
+ Currency inputCurrency,
211
+ Currency outputCurrency,
212
+ address to,
213
+ uint256 inputAmount,
214
+ uint128 outputAmount
215
+ ) internal {
216
+ // Pay the input amount
217
+ if (inputCurrency.isAddressZero()) {
218
+ // For ETH, sync and settle with msg.value
219
+ poolManager.sync(inputCurrency);
220
+ poolManager.settle{value: inputAmount}();
221
+ } else {
222
+ // For ERC20, sync and transfer
223
+ poolManager.sync(inputCurrency);
224
+ inputCurrency.transfer(address(poolManager), inputAmount);
225
+ poolManager.settle();
226
+ }
227
+
228
+ // Transfer the output amount to the recipient
229
+ poolManager.take(outputCurrency, to, outputAmount);
230
+ }
231
+
232
+ // ============ UTILITIES ============
233
+
234
+ /// @notice Gets the output currency from a V3 route path
235
+ /// @param path The V3 route path
236
+ /// @return tokenOut The output token address
237
+ function getV3RouteOutputCurrency(bytes memory path) internal pure returns (address tokenOut) {
238
+ if (path.length == 0) {
239
+ return address(0);
240
+ }
241
+
242
+ // Traverse to the end of the path to find the final token
243
+ bytes memory currentPath = path;
244
+
245
+ // Keep skipping tokens until we reach the final pool
246
+ while (currentPath.hasMultiplePools()) {
247
+ currentPath = currentPath.skipToken();
248
+ }
249
+
250
+ // The final segment contains the last pool, decode to get the output token
251
+ (, tokenOut, ) = currentPath.decodeFirstPool();
252
+ }
253
+
254
+ /// @notice Gets the input currency from a V3 route path
255
+ /// @param path The V3 route path
256
+ /// @return tokenIn The input token address
257
+ function getV3RouteInputCurrency(bytes memory path) internal pure returns (address tokenIn) {
258
+ if (path.length == 0) {
259
+ return address(0);
260
+ }
261
+
262
+ // Use Path library to get the input token (first token in the path)
263
+ (tokenIn, , ) = path.decodeFirstPool();
264
+ }
265
+ }
@@ -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.4.0";
12
+ return "2.5.0";
13
13
  }
14
14
  }
@@ -3,6 +3,7 @@ pragma solidity ^0.8.13;
3
3
 
4
4
  import {BaseTest} from "./utils/BaseTest.sol";
5
5
  import {BuySupplyWithV4SwapHook} from "../src/hooks/deployment/BuySupplyWithV4SwapHook.sol";
6
+ import {V3ToV4SwapLib} from "../src/libs/V3ToV4SwapLib.sol";
6
7
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7
8
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
8
9
  import {ICoin} from "../src/interfaces/ICoin.sol";
@@ -363,7 +364,7 @@ contract BuySupplyWithV4SwapHookTest is BaseTest {
363
364
  // Should revert with InsufficientInputCurrency
364
365
  vm.deal(users.creator, insufficientAmount);
365
366
  bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(ZORA);
366
- vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, insufficientAmount));
367
+ vm.expectRevert(abi.encodeWithSelector(V3ToV4SwapLib.InsufficientInputCurrency.selector, inputAmount, insufficientAmount));
367
368
 
368
369
  vm.prank(users.creator);
369
370
  factory.deployWithHook{value: insufficientAmount}(
@@ -414,7 +415,7 @@ contract BuySupplyWithV4SwapHookTest is BaseTest {
414
415
 
415
416
  bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
416
417
  // Should revert with InsufficientInputCurrency
417
- vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.InsufficientInputCurrency.selector, inputAmount, amountToApprove));
418
+ vm.expectRevert(abi.encodeWithSelector(V3ToV4SwapLib.InsufficientInputCurrency.selector, inputAmount, amountToApprove));
418
419
 
419
420
  vm.prank(users.creator);
420
421
  factory.deployWithHook(
@@ -454,7 +455,7 @@ contract BuySupplyWithV4SwapHookTest is BaseTest {
454
455
  bytes memory poolConfig = CoinConfigurationVersions.defaultDopplerMultiCurveUniV4(creatorCoinAddress);
455
456
 
456
457
  vm.prank(users.creator);
457
- vm.expectRevert(abi.encodeWithSelector(BuySupplyWithV4SwapHook.V3RouteDoesNotConnectToV4RouteStart.selector));
458
+ vm.expectRevert(abi.encodeWithSelector(V3ToV4SwapLib.V3RouteDoesNotConnectToV4RouteStart.selector));
458
459
  factory.deployWithHook{value: 1 ether}(
459
460
  users.creator, // payoutRecipient
460
461
  _getDefaultOwners(), // owners
package/test/Coin.t.sol CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.13;
3
3
 
4
- import "./utils/BaseTest.sol";
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
5
  import {ISwapRouter} from "../src/interfaces/ISwapRouter.sol";
6
6
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
7
7
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
@@ -11,6 +11,12 @@ import {PoolConfiguration} from "../src/interfaces/ICoin.sol";
11
11
  import {IERC165, IERC7572, ICoin, ICoinComments, IERC20} from "../src/BaseCoin.sol";
12
12
  import {IERC20Permit} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Permit.sol";
13
13
  import {BaseCoin} from "../src/BaseCoin.sol";
14
+ import {stdJson} from "forge-std/StdJson.sol";
15
+ import {ContentCoin} from "../src/ContentCoin.sol";
16
+ import {ZoraFactoryImpl} from "../src/ZoraFactoryImpl.sol";
17
+ import {MockERC20} from "./mocks/MockERC20.sol";
18
+ import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
19
+ import {MultiOwnable} from "../src/utils/MultiOwnable.sol";
14
20
 
15
21
  contract CoinTest is BaseTest {
16
22
  using stdJson for string;
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.13;
3
3
 
4
- import "./utils/BaseTest.sol";
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
5
  import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
6
6
  import {IV4Router} from "@uniswap/v4-periphery/src/interfaces/IV4Router.sol";
7
7
  import {IV4Quoter} from "@uniswap/v4-periphery/src/interfaces/IV4Quoter.sol";
@@ -18,6 +18,7 @@ import {CoinCommon} from "../src/libs/CoinCommon.sol";
18
18
  import {IZoraV4CoinHook} from "../src/interfaces/IZoraV4CoinHook.sol";
19
19
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
20
20
  import {IMsgSender} from "../src/interfaces/IMsgSender.sol";
21
+ import {ContentCoin} from "../src/ContentCoin.sol";
21
22
  import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
22
23
  import {toBalanceDelta, BalanceDelta} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
23
24
  import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
@@ -33,6 +34,7 @@ import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
33
34
  import {ICoin, IHasSwapPath, PathKey} from "../src/interfaces/ICoin.sol";
34
35
  import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
35
36
 
37
+ /// forge-config: default.isolate = true
36
38
  contract CoinUniV4Test is BaseTest {
37
39
  MockERC20 internal mockERC20A;
38
40
  MockERC20 internal mockERC20B;
@@ -112,6 +114,9 @@ contract CoinUniV4Test is BaseTest {
112
114
  address currency = address(mockERC20A);
113
115
  _deployV4Coin(currency);
114
116
 
117
+ // Skip past launch fee period to test normal LP fees
118
+ vm.warp(block.timestamp + 1 days);
119
+
115
120
  uint128 amountIn = uint128(0.00001 ether);
116
121
  uint128 minAmountOut = uint128(0);
117
122
 
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.13;
3
3
 
4
- import "./utils/BaseTest.sol";
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
5
  import {console} from "forge-std/console.sol";
6
6
 
7
7
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
@@ -12,7 +12,12 @@ import {RewardTestHelpers, RewardBalances} from "./utils/RewardTestHelpers.sol";
12
12
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
13
13
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
14
14
  import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
15
+ import {ContentCoin} from "../src/ContentCoin.sol";
16
+ import {CreatorCoin} from "../src/CreatorCoin.sol";
17
+ import {ICoin} from "../src/interfaces/ICoin.sol";
18
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
15
19
 
20
+ /// forge-config: default.isolate = true
16
21
  contract ContentCoinRewardsTest is BaseTest {
17
22
  ContentCoin internal contentCoin;
18
23
  CreatorCoin internal backingCreatorCoin;
@@ -1,13 +1,15 @@
1
1
  // SPDX-License-Identifier: MIT
2
2
  pragma solidity ^0.8.13;
3
3
 
4
- import "./utils/BaseTest.sol";
4
+ import {BaseTest} from "./utils/BaseTest.sol";
5
5
 
6
6
  import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
7
7
  import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
8
8
  import {CoinConstants} from "../src/libs/CoinConstants.sol";
9
9
  import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
10
10
  import {UniV4SwapHelper} from "../src/libs/UniV4SwapHelper.sol";
11
+ import {CreatorCoin} from "../src/CreatorCoin.sol";
12
+ import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
11
13
 
12
14
  contract CreatorCoinTest is BaseTest {
13
15
  CreatorCoin internal creatorCoin;