@zoralabs/coins 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.turbo/turbo-build$colon$js.log +116 -124
  2. package/CHANGELOG.md +6 -0
  3. package/abis/Address.json +0 -16
  4. package/abis/BuySupplyWithSwapRouterHook.json +0 -27
  5. package/abis/BuySupplyWithV4SwapHook.json +0 -32
  6. package/abis/Clones.json +1 -1
  7. package/abis/CoinDopplerMultiCurve.json +109 -0
  8. package/abis/Create2.json +0 -21
  9. package/abis/ERC1967Proxy.json +1 -1
  10. package/abis/ERC1967Utils.json +0 -45
  11. package/abis/{UpgradeCoinImpl.json → Errors.json} +14 -10
  12. package/abis/{MockERC20.json → IERC1363.json} +134 -104
  13. package/abis/IERC1967.json +47 -0
  14. package/abis/IERC20.json +0 -36
  15. package/abis/IProtocolRewards.json +0 -258
  16. package/abis/{Script.json → ISupportsLimitOrderFill.json} +2 -2
  17. package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
  18. package/abis/IZoraV4CoinHook.json +10 -0
  19. package/abis/ProxyShim.json +15 -16
  20. package/abis/SafeCast.json +51 -0
  21. package/abis/{AddressConstants.json → SafeCast160.json} +1 -1
  22. package/abis/Strings.json +10 -0
  23. package/abis/UUPSUpgradeable.json +1 -1
  24. package/abis/V3ToV4SwapLib.json +28 -0
  25. package/abis/ZoraFactory.json +1 -1
  26. package/abis/ZoraFactoryImpl.json +22 -6
  27. package/abis/ZoraV4CoinHook.json +20 -48
  28. package/dist/index.cjs +950 -43
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +948 -41
  31. package/dist/index.js.map +1 -1
  32. package/dist/wagmiGenerated.d.ts +1459 -76
  33. package/dist/wagmiGenerated.d.ts.map +1 -1
  34. package/package/wagmiGenerated.ts +951 -44
  35. package/package.json +9 -9
  36. package/remappings.txt +2 -1
  37. package/src/ZoraFactoryImpl.sol +8 -0
  38. package/src/deployment/ForkedCoinsAddresses.sol +54 -0
  39. package/src/hooks/ZoraV4CoinHook.sol +74 -20
  40. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +20 -142
  41. package/src/interfaces/ISupportsLimitOrderFill.sol +11 -0
  42. package/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol +21 -0
  43. package/src/interfaces/IZoraV4CoinHook.sol +6 -0
  44. package/src/libs/CoinConstants.sol +6 -0
  45. package/src/libs/CoinDopplerMultiCurve.sol +1 -1
  46. package/src/libs/CoinRewardsV4.sol +0 -1
  47. package/src/libs/HooksDeployment.sol +20 -8
  48. package/src/libs/UniV4SwapHelper.sol +35 -0
  49. package/src/libs/V3ToV4SwapLib.sol +261 -0
  50. package/src/version/ContractVersionBase.sol +1 -1
  51. package/test/BuySupplyWithV4SwapHook.t.sol +4 -3
  52. package/test/Coin.t.sol +7 -1
  53. package/test/CoinUniV4.t.sol +2 -1
  54. package/test/ContentCoinRewards.t.sol +5 -1
  55. package/test/CreatorCoin.t.sol +3 -1
  56. package/test/CreatorCoinRewards.t.sol +3 -1
  57. package/test/Factory.t.sol +20 -7
  58. package/test/HooksDeployment.t.sol +16 -3
  59. package/test/LiquidityMigration.t.sol +52 -44
  60. package/test/MultiOwnable.t.sol +2 -1
  61. package/test/Upgrades.t.sol +110 -81
  62. package/test/V4Liquidity.t.sol +1 -1
  63. package/test/mocks/MockSwapRouter.sol +33 -0
  64. package/test/mocks/MockZoraLimitOrderBook.sol +14 -0
  65. package/test/utils/BaseTest.sol +14 -448
  66. package/test/utils/FeeEstimatorHook.sol +6 -2
  67. package/test/utils/V4TestSetup.sol +595 -0
  68. package/wagmi.config.ts +1 -1
  69. package/abis/BaseTest.json +0 -718
  70. package/abis/DeterministicDeployerAndCaller.json +0 -315
  71. package/abis/DeterministicUUPSProxyDeployer.json +0 -167
  72. package/abis/EIP712.json +0 -67
  73. package/abis/ERC20.json +0 -310
  74. package/abis/FeeEstimatorHook.json +0 -1938
  75. package/abis/IERC721.json +0 -287
  76. package/abis/IERC721Enumerable.json +0 -343
  77. package/abis/IERC721Metadata.json +0 -332
  78. package/abis/IERC721TokenReceiver.json +0 -36
  79. package/abis/IImmutableCreate2Factory.json +0 -93
  80. package/abis/IMulticall3.json +0 -440
  81. package/abis/ISafe.json +0 -15
  82. package/abis/ISymbol.json +0 -15
  83. package/abis/IUniswapV4Router04.json +0 -484
  84. package/abis/IUniversalRouter.json +0 -61
  85. package/abis/IV4Quoter.json +0 -310
  86. package/abis/ImmutableCreate2FactoryUtils.json +0 -15
  87. package/abis/LibString.json +0 -7
  88. package/abis/Math.json +0 -7
  89. package/abis/MockAirlock.json +0 -39
  90. package/abis/MockERC721.json +0 -350
  91. package/abis/ProtocolRewards.json +0 -494
  92. package/abis/ShortStrings.json +0 -18
  93. package/abis/SimpleERC20.json +0 -326
  94. package/abis/StdAssertions.json +0 -379
  95. package/abis/StdInvariant.json +0 -180
  96. package/abis/Test.json +0 -570
  97. package/abis/VmContractHelper235.json +0 -233
  98. package/abis/VmContractHelper242.json +0 -233
  99. package/abis/stdError.json +0 -119
  100. package/abis/stdStorageSafe.json +0 -52
  101. package/addresses/8453.json +0 -13
  102. package/addresses/84532.json +0 -10
  103. package/deterministicConfig/deployerAndCaller.json +0 -5
  104. package/deterministicConfig/zoraFactory.json +0 -8
  105. package/script/Deploy.s.sol +0 -23
  106. package/script/DeployAutoSwapper.s.sol +0 -30
  107. package/script/DeployDevFactory.s.sol +0 -21
  108. package/script/DeployPostDeploymentHooks.s.sol +0 -20
  109. package/script/DeployTrustedMsgSenderLookup.s.sol +0 -20
  110. package/script/DeployUpgradeGate.s.sol +0 -21
  111. package/script/GenerateDeterministicParams.s.sol +0 -43
  112. package/script/PrintRegisterUpgradePath.s.sol +0 -28
  113. package/script/PrintUpgradeCommand.s.sol +0 -13
  114. package/script/TestBackingCoinSwap.s.sol +0 -144
  115. package/script/TestV4Swap.s.sol +0 -133
  116. package/script/UpgradeCoinImpl.sol +0 -23
  117. package/script/UpgradeFactoryImpl.s.sol +0 -28
  118. package/script/UpgradeHooks.s.sol +0 -23
  119. package/src/deployment/CoinsDeployerBase.sol +0 -297
  120. /package/{test → src}/utils/ProxyShim.sol +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/coins",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.js",
@@ -14,8 +14,8 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@openzeppelin/contracts": "^5.0.2",
18
- "@openzeppelin/contracts-upgradeable": "^5.0.2"
17
+ "@openzeppelin/contracts": "5.4.0",
18
+ "@openzeppelin/contracts-upgradeable": "5.4.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "^20.1.2",
@@ -35,19 +35,19 @@
35
35
  "tsx": "^3.13.0",
36
36
  "typescript": "^5.2.2",
37
37
  "viem": "^2.21.18",
38
+ "@zoralabs/shared-contracts": "^0.0.5",
38
39
  "@zoralabs/shared-scripts": "^0.0.0",
39
- "@zoralabs/tsconfig": "^0.0.1",
40
- "@zoralabs/shared-contracts": "^0.0.5"
40
+ "@zoralabs/tsconfig": "^0.0.1"
41
41
  },
42
42
  "scripts": {
43
43
  "build": "forge build",
44
- "build:contracts:minimal": "forge build --skip test --skip script --no-metadata",
44
+ "build:contracts:minimal": "forge build src/ --no-metadata",
45
45
  "build:js": "pnpm run wagmi:generate && pnpm run copy-abis && pnpm run prettier:write && tsup",
46
46
  "build:sizes": "forge build src/ --sizes",
47
47
  "copy-abis": "pnpm exec bundle-abis",
48
- "coverage": "forge coverage --report lcov --ir-minimum --no-match-coverage '(test/|src/utils/uniswap/|script/)'",
49
- "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'",
50
- "prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol' 'script/**/*.sol'",
48
+ "coverage": "forge coverage --report lcov --ir-minimum --no-match-coverage '(test/|src/utils/uniswap/|src/deployment/)'",
49
+ "prettier:check": "prettier --check 'src/**/*.sol' 'test/**/*.sol'",
50
+ "prettier:write": "prettier --write 'src/**/*.sol' 'test/**/*.sol'",
51
51
  "test": "forge test -vv",
52
52
  "test-gas": "forge test --gas-report",
53
53
  "update-contract-version": "pnpm exec update-contract-version",
package/remappings.txt CHANGED
@@ -2,7 +2,8 @@ ds-test/=node_modules/ds-test/src/
2
2
  forge-std/=node_modules/forge-std/src/
3
3
  @openzeppelin/=node_modules/@openzeppelin/
4
4
  @zoralabs/shared-contracts/=node_modules/@zoralabs/shared-contracts/src/
5
- solady/=node_modules/solady/src/
5
+ @zoralabs/limit-orders/=node_modules/@zoralabs/limit-orders/
6
+ solady/=node_modules/solady/src/
6
7
  @uniswap/v4-core/=node_modules/@uniswap/v4-core/
7
8
  @uniswap/v4-periphery/=node_modules/@uniswap/v4-periphery/
8
9
  permit2/src/=node_modules/@uniswap/permit2/src/
@@ -390,6 +390,14 @@ contract ZoraFactoryImpl is
390
390
  __UUPSUpgradeable_init();
391
391
  __ReentrancyGuard_init();
392
392
  __Ownable_init(initialOwner);
393
+
394
+ address[] memory hooks = new address[](1);
395
+ string[] memory tags = new string[](1);
396
+
397
+ hooks[0] = hook;
398
+ tags[0] = "CoinHook";
399
+
400
+ IZoraHookRegistry(zoraHookRegistry).registerHooks(hooks, tags);
393
401
  }
394
402
 
395
403
  /// @notice The implementation address of the factory contract
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: MIT
2
+ pragma solidity ^0.8.28;
3
+
4
+ /// @notice Minimal deployment base for coins tests
5
+ /// @dev Provides hardcoded deployment addresses for testing
6
+ contract ForkedCoinsAddresses {
7
+ struct CoinsDeployment {
8
+ // Factory
9
+ address zoraFactory;
10
+ address zoraFactoryImpl;
11
+ // Implementation
12
+ address coinV3Impl;
13
+ address coinV4Impl;
14
+ address creatorCoinImpl;
15
+ string coinVersion;
16
+ // hooks
17
+ address buySupplyWithSwapRouterHook;
18
+ address zoraV4CoinHook;
19
+ address hookUpgradeGate;
20
+ // trusted sender lookup
21
+ address trustedMsgSenderLookup;
22
+ // Hook deployment salt (for deterministic deployment)
23
+ bytes32 zoraV4CoinHookSalt;
24
+ bool isDev;
25
+ // Hook registry
26
+ address zoraHookRegistry;
27
+ // Limit order book
28
+ address zoraLimitOrderBook;
29
+ address swapWithLimitOrdersRouter;
30
+ address orderBookAuthority;
31
+ }
32
+
33
+ address internal constant ZORA = 0x1111111111166b7FE7bd91427724B487980aFc69;
34
+
35
+ function readDeployment() internal pure returns (CoinsDeployment memory deployment) {
36
+ return readDeployment(false);
37
+ }
38
+
39
+ function readDeployment(bool dev) internal pure returns (CoinsDeployment memory deployment) {
40
+ // Hardcoded Base mainnet deployment addresses
41
+ deployment.zoraFactory = 0x777777751622c0d3258f214F9DF38E35BF45baF3;
42
+ deployment.zoraFactoryImpl = 0x8Ec7f068A77fa5FC1925110f82381374BA054Ff2;
43
+ deployment.coinV3Impl = 0x45Bf86430af7CD071Ea23aE52325A78C8d12aD5a;
44
+ deployment.coinV4Impl = 0x7Cad62748DDf516CF85bC2C05C14786D84Cf861c;
45
+ deployment.creatorCoinImpl = 0x36853f9f48fAEe51Bd3db15db21EB4B9038bB795;
46
+ deployment.coinVersion = "2.3.0";
47
+ deployment.buySupplyWithSwapRouterHook = 0xd8CC7bCA1dE52eA788829B16E375e9B96C18D433;
48
+ deployment.zoraV4CoinHook = 0xC8d077444625eB300A427a6dfB2b1DBf9b159040;
49
+ deployment.hookUpgradeGate = 0xD88f6BdD765313CaFA5888C177c325E2C3AbF2D2;
50
+ deployment.zoraV4CoinHookSalt = 0x0000000000000000000000000000000000000000000000000000000000001624;
51
+ deployment.zoraHookRegistry = 0x777777C4c14b133858c3982D41Dbf02509fc18d7;
52
+ deployment.isDev = dev;
53
+ }
54
+ }
@@ -5,41 +5,42 @@
5
5
  // until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
6
6
  // at which point this software automatically becomes available under the MIT License.
7
7
  // Full license terms available at: https://docs.zora.co/coins/license
8
- pragma solidity ^0.8.23;
8
+ pragma solidity ^0.8.28;
9
9
 
10
+ import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
11
+ import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
10
12
  import {BaseHook} from "@uniswap/v4-periphery/src/utils/BaseHook.sol";
11
13
  import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
12
- import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
13
14
  import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
14
15
  import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
15
16
  import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
16
17
  import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
18
+ import {BeforeSwapDelta} from "@uniswap/v4-core/src/types/BeforeSwapDelta.sol";
19
+ import {StateLibrary} from "@uniswap/v4-core/src/libraries/StateLibrary.sol";
20
+ import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
21
+ import {TransientSlot} from "@openzeppelin/contracts/utils/TransientSlot.sol";
22
+
17
23
  import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
18
24
  import {IMsgSender} from "../interfaces/IMsgSender.sol";
19
25
  import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
20
- import {IHasSwapPath} from "../interfaces/ICoin.sol";
26
+ import {ICoin, IHasSwapPath, IHasRewardsRecipients, IHasCoinType} from "../interfaces/ICoin.sol";
27
+ import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
28
+ import {IUpgradeableV4Hook, IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee, BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
29
+ import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
30
+ import {IZoraHookRegistry} from "../interfaces/IZoraHookRegistry.sol";
31
+ import {IZoraLimitOrderBookCoinsInterface} from "../interfaces/IZoraLimitOrderBookCoinsInterface.sol";
21
32
  import {LpPosition} from "../types/LpPosition.sol";
22
33
  import {V4Liquidity} from "../libs/V4Liquidity.sol";
23
34
  import {CoinRewardsV4} from "../libs/CoinRewardsV4.sol";
24
- import {ICoin} from "../interfaces/ICoin.sol";
25
- import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
26
35
  import {CoinCommon} from "../libs/CoinCommon.sol";
27
36
  import {CoinDopplerMultiCurve} from "../libs/CoinDopplerMultiCurve.sol";
28
37
  import {PoolStateReader} from "../libs/PoolStateReader.sol";
29
- import {IHasRewardsRecipients} from "../interfaces/ICoin.sol";
30
38
  import {CoinConfigurationVersions} from "../libs/CoinConfigurationVersions.sol";
31
- import {IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
32
- import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
33
- import {MultiOwnable} from "../utils/MultiOwnable.sol";
34
- import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
35
- import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
36
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
37
- import {BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
39
+ import {CoinConstants} from "../libs/CoinConstants.sol";
38
40
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
39
41
  import {TickMath} from "../utils/uniswap/TickMath.sol";
40
42
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
41
- import {IHasCoinType} from "../interfaces/ICoin.sol";
42
- import {CoinConstants} from "../libs/CoinConstants.sol";
43
+ import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
43
44
 
44
45
  /// @title ZoraV4CoinHook
45
46
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -78,26 +79,38 @@ contract ZoraV4CoinHook is
78
79
  /// @notice The trusted message sender lookup contract - used to determine if an address is trusted
79
80
  ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
80
81
 
82
+ /// @notice The Zora limit order book contract - used to fill limit orders during swaps
83
+ IZoraLimitOrderBookCoinsInterface internal immutable zoraLimitOrderBook;
84
+
85
+ /// @notice The Zora hook registry
86
+ IZoraHookRegistry internal immutable zoraHookRegistry;
87
+
81
88
  /// @notice The constructor for the ZoraV4CoinHook.
82
89
  /// @param poolManager_ The Uniswap V4 pool manager
83
90
  /// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
84
91
  /// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
85
92
  /// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
93
+ /// @param zoraLimitOrderBook_ The Zora limit order book contract for filling orders during swaps
94
+ /// @param zoraHookRegistry_ The Zora hook registry contract for identifying registered hooks
86
95
  constructor(
87
96
  IPoolManager poolManager_,
88
97
  IDeployedCoinVersionLookup coinVersionLookup_,
89
98
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
90
- IHooksUpgradeGate upgradeGate_
99
+ IHooksUpgradeGate upgradeGate_,
100
+ IZoraLimitOrderBookCoinsInterface zoraLimitOrderBook_,
101
+ IZoraHookRegistry zoraHookRegistry_
91
102
  ) BaseHook(poolManager_) {
92
103
  require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
93
-
94
104
  require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
95
-
105
+ require(address(zoraLimitOrderBook_) != address(0), ZoraLimitOrderBookCannotBeZeroAddress());
106
+ require(address(zoraHookRegistry_) != address(0), ZoraHookRegistryCannotBeZeroAddress());
96
107
  require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
97
108
 
98
109
  coinVersionLookup = coinVersionLookup_;
99
110
  upgradeGate = upgradeGate_;
100
111
  trustedMsgSenderLookup = trustedMsgSenderLookup_;
112
+ zoraLimitOrderBook = zoraLimitOrderBook_;
113
+ zoraHookRegistry = zoraHookRegistry_;
101
114
  }
102
115
 
103
116
  /// @notice Returns the trusted message sender lookup contract
@@ -116,7 +129,7 @@ contract ZoraV4CoinHook is
116
129
  afterAddLiquidity: false,
117
130
  beforeRemoveLiquidity: false,
118
131
  afterRemoveLiquidity: false,
119
- beforeSwap: false,
132
+ beforeSwap: true,
120
133
  afterSwap: true,
121
134
  beforeDonate: false,
122
135
  afterDonate: false,
@@ -148,7 +161,8 @@ contract ZoraV4CoinHook is
148
161
  super.supportsInterface(interfaceId) ||
149
162
  interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
150
163
  interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
151
- interfaceId == type(IVersionedContract).interfaceId;
164
+ interfaceId == type(IVersionedContract).interfaceId ||
165
+ interfaceId == type(ISupportsLimitOrderFill).interfaceId;
152
166
  }
153
167
 
154
168
  /// @notice Internal fn generating the positions for a given pool key.
@@ -281,6 +295,27 @@ contract ZoraV4CoinHook is
281
295
  V4Liquidity.lockAndMint(poolManager, key, positions);
282
296
  }
283
297
 
298
+ /// @notice Transiently stores the tick before a swap.
299
+ /// @dev This is used in `_afterSwap` to determine the ticks crossed during the swap.
300
+ function _beforeSwap(
301
+ address sender,
302
+ PoolKey calldata key,
303
+ SwapParams calldata,
304
+ bytes calldata
305
+ ) internal virtual override returns (bytes4, BeforeSwapDelta, uint24) {
306
+ if (_isInternalSwap(sender)) {
307
+ return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
308
+ }
309
+
310
+ // Store tick for user-initiated swaps only
311
+ (, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
312
+
313
+ TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
314
+ TransientSlot.tstore(slot, int256(currentTick));
315
+
316
+ return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
317
+ }
318
+
284
319
  /// @notice Internal fn called when a swap is executed.
285
320
  /// @dev This hook is called from BaseHook library from uniswap v4.
286
321
  /// This hook:
@@ -301,6 +336,10 @@ contract ZoraV4CoinHook is
301
336
  BalanceDelta delta,
302
337
  bytes calldata hookData
303
338
  ) internal virtual override returns (bytes4, int128) {
339
+ if (_isInternalSwap(sender)) {
340
+ return (BaseHook.afterSwap.selector, 0);
341
+ }
342
+
304
343
  bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
305
344
 
306
345
  // get the coin address and positions for the pool key; they must have been set in the afterInitialize callback
@@ -343,9 +382,19 @@ contract ZoraV4CoinHook is
343
382
  );
344
383
  }
345
384
 
385
+ (int24 tickBeforeSwap, int24 tickAfterSwap) = _getSwapTickRange(key);
386
+ zoraLimitOrderBook.fill(key, !params.zeroForOne, tickBeforeSwap, tickAfterSwap, CoinConstants.SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT, address(0));
387
+
346
388
  return (BaseHook.afterSwap.selector, 0);
347
389
  }
348
390
 
391
+ /// @dev Get the tick range of a swap
392
+ function _getSwapTickRange(PoolKey calldata key) internal view returns (int24 tickBeforeSwap, int24 tickAfterSwap) {
393
+ TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
394
+ tickBeforeSwap = int24(int256(TransientSlot.tload(slot)));
395
+ (, tickAfterSwap, , ) = StateLibrary.getSlot0(poolManager, key.toId());
396
+ }
397
+
349
398
  /// @dev Internal fn to allow for overriding market reward distribution logic
350
399
  function _distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal virtual {
351
400
  // get rewards distribution methodology from the coin
@@ -395,6 +444,11 @@ contract ZoraV4CoinHook is
395
444
  delete poolCoins[poolKeyHash];
396
445
  }
397
446
 
447
+ /// @dev Checks if the swap is internal and should skip hook operations
448
+ function _isInternalSwap(address sender) internal view returns (bool) {
449
+ return sender == address(this) || sender == address(zoraLimitOrderBook) || zoraHookRegistry.isRegisteredHook(sender);
450
+ }
451
+
398
452
  /// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
399
453
  /// @dev Only required for coins using ETH as backing currency (currency = address(0)).
400
454
  /// Restricted to onlyPoolManager to prevent ETH from getting stuck in the contract.
@@ -17,6 +17,7 @@ import {ICoinV3} from "../../interfaces/ICoinV3.sol";
17
17
  import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
18
18
  import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
19
19
  import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
20
+ import {V3ToV4SwapLib} from "../../libs/V3ToV4SwapLib.sol";
20
21
 
21
22
  /// @title BuySupplyWithV4SwapHook
22
23
  /// @notice Hook for purchasing initial coin supply with flexible swap routing
@@ -67,9 +68,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
67
68
  // ============ ERRORS ============
68
69
 
69
70
  error OnlyPoolManager();
70
- error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
71
- error V3RouteCannotStartWithInputCurrency();
72
- error V3RouteDoesNotConnectToV4RouteStart();
73
71
  error InsufficientOutputAmount();
74
72
 
75
73
  // ============ CONSTRUCTOR ============
@@ -90,12 +88,20 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
90
88
  PoolKey[] memory v4Route = _buildV4RouteToCoin(coin, params.v4Route);
91
89
 
92
90
  // STEP 2: Validate routes
93
- _validateRoutes(params, v4Route);
91
+ V3ToV4SwapLib.validateRoutes(params.v3Route, params.inputCurrency, v4Route);
94
92
 
95
- _validateAndTransferInputCurrency(params);
93
+ V3ToV4SwapLib.validateAndTransferInputCurrency(params.inputCurrency, params.inputAmount, params.buyRecipient, msg.value);
96
94
 
97
95
  // STEP 3: Execute V3 swap (inputCurrency -> backing currency)
98
- (uint256 currencyAmount, address currencyReceived) = _executeV3Swap(params);
96
+ (uint256 currencyAmount, address currencyReceived) = V3ToV4SwapLib.executeV3Swap(
97
+ swapRouter,
98
+ V3ToV4SwapLib.V3SwapParams({
99
+ v3Route: params.v3Route,
100
+ inputCurrency: params.inputCurrency,
101
+ inputAmount: params.inputAmount,
102
+ recipient: address(this)
103
+ })
104
+ );
99
105
 
100
106
  // STEP 4: Execute V4 swaps if needed, then buy coin
101
107
  uint256 coinAmount = _executeV4Swap(v4Route, currencyAmount, currencyReceived, params.buyRecipient);
@@ -120,41 +126,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
120
126
 
121
127
  // ============ VALIDATION ============
122
128
 
123
- function _validateRoutes(InitialSupplyParams memory params, PoolKey[] memory v4Route) internal pure {
124
- // Determine what currency should be the input to the V4 route
125
- address v4InputCurrency;
126
- if (params.v3Route.length == 0) {
127
- // No V3 swap - input currency should directly match V4 route start
128
- v4InputCurrency = params.inputCurrency;
129
- } else {
130
- // V3 swap exists - V3 output should match V4 route start
131
- v4InputCurrency = _getV3RouteOutputCurrency(params.v3Route);
132
- }
133
-
134
- PoolKey memory firstPool = v4Route[0];
135
-
136
- require(
137
- v4InputCurrency == Currency.unwrap(firstPool.currency0) || v4InputCurrency == Currency.unwrap(firstPool.currency1),
138
- V3RouteDoesNotConnectToV4RouteStart()
139
- );
140
- }
141
-
142
- function _validateAndTransferInputCurrency(InitialSupplyParams memory params) internal {
143
- if (params.inputCurrency == address(0)) {
144
- uint256 providedAmount = msg.value;
145
-
146
- require(providedAmount == params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
147
- } else {
148
- uint256 providedAmount = IERC20(params.inputCurrency).allowance(params.buyRecipient, address(this));
149
-
150
- // must be enough allowance to transfer
151
- require(providedAmount >= params.inputAmount, InsufficientInputCurrency(params.inputAmount, providedAmount));
152
-
153
- // transfer from the buy recipient to this contract
154
- IERC20(params.inputCurrency).safeTransferFrom(params.buyRecipient, address(this), params.inputAmount);
155
- }
156
- }
157
-
158
129
  function _buildV4RouteToCoin(ICoin coin, PoolKey[] memory v4Route) internal view returns (PoolKey[] memory fullRoute) {
159
130
  fullRoute = new PoolKey[](v4Route.length + 1);
160
131
 
@@ -165,31 +136,7 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
165
136
  fullRoute[v4Route.length] = coin.getPoolKey();
166
137
  }
167
138
 
168
- // ============ V3 SWAP LOGIC ============
169
-
170
- function _executeV3Swap(InitialSupplyParams memory params) internal returns (uint256 amountCurrency, address currencyReceived) {
171
- if (params.v3Route.length == 0) {
172
- // No V3 swap needed - return inputAmount directly
173
- return (params.inputAmount, params.inputCurrency);
174
- }
175
-
176
- // for v3 swap section, we dont support currently having an input currency other than eth
177
- if (params.inputCurrency != address(0)) {
178
- revert V3RouteCannotStartWithInputCurrency();
179
- }
180
-
181
- // Build swap router call for exactInput
182
- ISwapRouter.ExactInputParams memory swapParams = ISwapRouter.ExactInputParams({
183
- path: params.v3Route,
184
- recipient: address(this),
185
- amountIn: params.inputAmount,
186
- amountOutMinimum: 0 // For testing - in production should have slippage protection
187
- });
188
-
189
- amountCurrency = swapRouter.exactInput{value: params.inputAmount}(swapParams);
190
-
191
- currencyReceived = _getV3RouteOutputCurrency(params.v3Route);
192
- }
139
+ // ============ V4 SWAP LOGIC ============
193
140
 
194
141
  function _executeV4Swap(PoolKey[] memory v4Route, uint256 amountIn, address currencyIn, address buyRecipient) internal returns (uint256 amountCoin) {
195
142
  Currency startingCurrency = Currency.wrap(currencyIn);
@@ -207,37 +154,16 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
207
154
  (PoolKey[], uint256, Currency, address)
208
155
  );
209
156
 
210
- Currency lastReceivedCurrency = startingCurrency;
211
- uint128 lastReceivedAmount = uint128(amountIn);
212
- // Execute swaps through the route
213
-
214
- uint128 outputAmount = 0;
215
- for (uint256 i = 0; i < v4Route.length; i++) {
216
- PoolKey memory poolKey = v4Route[i];
217
-
218
- // Determine swap direction based on current currency
219
- bool zeroForOne = lastReceivedCurrency == poolKey.currency0;
220
-
221
- BalanceDelta delta = poolManager.swap(
222
- poolKey,
223
- SwapParams(zeroForOne, -(int128(lastReceivedAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
224
- ""
225
- );
226
-
227
- // Extract output amount from delta
228
- outputAmount = zeroForOne ? uint128(delta.amount1()) : uint128(delta.amount0());
229
-
230
- // Update currentAmount for next iteration
231
- lastReceivedAmount = uint128(outputAmount);
232
-
233
- // Update current currency for next swap
234
- lastReceivedCurrency = zeroForOne ? poolKey.currency1 : poolKey.currency0;
235
- }
157
+ // Execute V4 multi-hop swap
158
+ V3ToV4SwapLib.V4SwapResult memory result = V3ToV4SwapLib.executeV4MultiHopSwap(
159
+ poolManager,
160
+ V3ToV4SwapLib.V4SwapParams({v4Route: v4Route, amountIn: amountIn, startingCurrency: startingCurrency})
161
+ );
236
162
 
237
163
  // Settle all currency deltas and get final amount
238
- _settleDeltas(startingCurrency, lastReceivedCurrency, buyRecipient, amountIn, outputAmount);
164
+ V3ToV4SwapLib.settleDeltas(poolManager, startingCurrency, result.outputCurrency, buyRecipient, amountIn, result.outputAmount);
239
165
 
240
- return abi.encode(lastReceivedAmount);
166
+ return abi.encode(result.outputAmount);
241
167
  }
242
168
 
243
169
  /// @notice Helper to decode V4 route data (external for try/catch)
@@ -249,22 +175,6 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
249
175
  return abi.encode(params);
250
176
  }
251
177
 
252
- function _settleDeltas(Currency inputCurrency, Currency outputCurrency, address to, uint256 inputAmount, uint128 outputAmount) private {
253
- // pay the input amount
254
- if (inputCurrency.isAddressZero()) {
255
- // For ETH, settle with msg.value
256
- poolManager.settle{value: inputAmount}();
257
- } else {
258
- // For ERC20, sync and transfer
259
- poolManager.sync(inputCurrency);
260
- inputCurrency.transfer(address(poolManager), inputAmount);
261
- poolManager.settle();
262
- }
263
-
264
- // transfer the output amount to the recipient
265
- poolManager.take(outputCurrency, to, outputAmount);
266
- }
267
-
268
178
  // ============ UTILITIES ============
269
179
 
270
180
  function _getCoinBackingCurrency(ICoin coin) internal view returns (Currency) {
@@ -275,36 +185,4 @@ contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
275
185
  }
276
186
  return poolKey.currency0;
277
187
  }
278
-
279
- function _getV3RouteOutputCurrency(bytes memory path) internal pure returns (address tokenOut) {
280
- if (path.length == 0) {
281
- // if no path, then output currency is eth
282
- return address(0);
283
- }
284
-
285
- // For a path with multiple pools, we need to traverse to the end
286
- // Path format: tokenA + fee + tokenB + fee + tokenC...
287
- // We want the final token (tokenC in this example)
288
-
289
- // Follow Uniswap's pattern: traverse the path to find the final token
290
- bytes memory currentPath = path;
291
-
292
- // Keep skipping tokens until we reach the final pool
293
- while (currentPath.hasMultiplePools()) {
294
- currentPath = currentPath.skipToken();
295
- }
296
-
297
- // The final segment contains the last pool, decode to get the output token
298
- (, tokenOut, ) = currentPath.decodeFirstPool();
299
- }
300
-
301
- function _getV3RouteInputCurrency(bytes memory path) internal pure returns (address tokenIn) {
302
- if (path.length == 0) {
303
- // if no path, then input currency is eth
304
- return address(0);
305
- }
306
-
307
- // Use Path library to get the input token (first token in the path)
308
- (tokenIn, , ) = path.decodeFirstPool();
309
- }
310
188
  }
@@ -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
 
@@ -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";