@zoralabs/coins 2.3.1 → 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 (126) hide show
  1. package/.turbo/turbo-build$colon$js.log +119 -128
  2. package/CHANGELOG.md +30 -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/ITrustedMsgSenderProviderLookup.json +21 -0
  18. package/abis/IZoraLimitOrderBookCoinsInterface.json +67 -0
  19. package/abis/IZoraV4CoinHook.json +15 -0
  20. package/abis/ProxyShim.json +15 -16
  21. package/abis/SafeCast.json +51 -0
  22. package/abis/{AddressConstants.json → SafeCast160.json} +1 -1
  23. package/abis/Strings.json +10 -0
  24. package/abis/TrustedMsgSenderProviderLookup.json +215 -0
  25. package/abis/UUPSUpgradeable.json +1 -1
  26. package/abis/V3ToV4SwapLib.json +28 -0
  27. package/abis/ZoraFactory.json +1 -1
  28. package/abis/ZoraFactoryImpl.json +22 -6
  29. package/abis/ZoraV4CoinHook.json +41 -51
  30. package/dist/index.cjs +950 -43
  31. package/dist/index.cjs.map +1 -1
  32. package/dist/index.js +948 -41
  33. package/dist/index.js.map +1 -1
  34. package/dist/wagmiGenerated.d.ts +1459 -76
  35. package/dist/wagmiGenerated.d.ts.map +1 -1
  36. package/foundry.toml +5 -1
  37. package/package/wagmiGenerated.ts +951 -44
  38. package/package.json +7 -7
  39. package/remappings.txt +2 -1
  40. package/src/ZoraFactoryImpl.sol +8 -0
  41. package/src/deployment/ForkedCoinsAddresses.sol +54 -0
  42. package/src/hooks/ZoraV4CoinHook.sol +92 -74
  43. package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +20 -142
  44. package/src/interfaces/ISupportsLimitOrderFill.sol +11 -0
  45. package/src/interfaces/ITrustedMsgSenderProviderLookup.sol +18 -0
  46. package/src/interfaces/IZoraLimitOrderBookCoinsInterface.sol +21 -0
  47. package/src/interfaces/IZoraV4CoinHook.sol +9 -0
  48. package/src/libs/CoinConstants.sol +6 -0
  49. package/src/libs/CoinDopplerMultiCurve.sol +1 -1
  50. package/src/libs/CoinRewardsV4.sol +0 -1
  51. package/src/libs/HooksDeployment.sol +25 -12
  52. package/src/libs/UniV4SwapHelper.sol +35 -0
  53. package/src/libs/V3ToV4SwapLib.sol +261 -0
  54. package/src/libs/V4Liquidity.sol +50 -6
  55. package/src/utils/TrustedMsgSenderProviderLookup.sol +73 -0
  56. package/src/version/ContractVersionBase.sol +1 -1
  57. package/test/BuySupplyWithV4SwapHook.t.sol +4 -3
  58. package/test/Coin.t.sol +7 -1
  59. package/test/CoinUniV4.t.sol +2 -1
  60. package/test/ContentCoinRewards.t.sol +5 -1
  61. package/test/CreatorCoin.t.sol +3 -1
  62. package/test/CreatorCoinRewards.t.sol +7 -1
  63. package/test/Factory.t.sol +20 -7
  64. package/test/HooksDeployment.t.sol +73 -8
  65. package/test/LiquidityMigration.t.sol +55 -43
  66. package/test/MultiOwnable.t.sol +2 -1
  67. package/test/TrustedMsgSenderProviderLookup.t.sol +112 -0
  68. package/test/Upgrades.t.sol +112 -78
  69. package/test/V4Liquidity.t.sol +1 -1
  70. package/test/mocks/MockSwapRouter.sol +33 -0
  71. package/test/mocks/MockZoraLimitOrderBook.sol +14 -0
  72. package/test/utils/BaseTest.sol +17 -425
  73. package/test/utils/FeeEstimatorHook.sol +8 -2
  74. package/test/utils/TrustedSenderTestHelper.sol +18 -0
  75. package/test/utils/V4TestSetup.sol +595 -0
  76. package/wagmi.config.ts +1 -1
  77. package/abis/BaseTest.json +0 -718
  78. package/abis/DeterministicDeployerAndCaller.json +0 -315
  79. package/abis/DeterministicUUPSProxyDeployer.json +0 -167
  80. package/abis/EIP712.json +0 -67
  81. package/abis/ERC20.json +0 -310
  82. package/abis/FeeEstimatorHook.json +0 -1915
  83. package/abis/IERC721.json +0 -287
  84. package/abis/IERC721Enumerable.json +0 -343
  85. package/abis/IERC721Metadata.json +0 -332
  86. package/abis/IERC721TokenReceiver.json +0 -36
  87. package/abis/IImmutableCreate2Factory.json +0 -93
  88. package/abis/IMulticall3.json +0 -440
  89. package/abis/ISafe.json +0 -15
  90. package/abis/ISymbol.json +0 -15
  91. package/abis/IUniswapV4Router04.json +0 -484
  92. package/abis/IUniversalRouter.json +0 -61
  93. package/abis/IV4Quoter.json +0 -310
  94. package/abis/ImmutableCreate2FactoryUtils.json +0 -15
  95. package/abis/LibString.json +0 -7
  96. package/abis/Math.json +0 -7
  97. package/abis/MockAirlock.json +0 -39
  98. package/abis/MockERC721.json +0 -350
  99. package/abis/ProtocolRewards.json +0 -494
  100. package/abis/ShortStrings.json +0 -18
  101. package/abis/SimpleERC20.json +0 -326
  102. package/abis/StdAssertions.json +0 -379
  103. package/abis/StdInvariant.json +0 -180
  104. package/abis/Test.json +0 -570
  105. package/abis/VmContractHelper239.json +0 -233
  106. package/abis/stdError.json +0 -119
  107. package/abis/stdStorageSafe.json +0 -52
  108. package/addresses/8453.json +0 -13
  109. package/addresses/84532.json +0 -10
  110. package/deterministicConfig/deployerAndCaller.json +0 -5
  111. package/deterministicConfig/zoraFactory.json +0 -8
  112. package/script/Deploy.s.sol +0 -23
  113. package/script/DeployAutoSwapper.s.sol +0 -30
  114. package/script/DeployDevFactory.s.sol +0 -21
  115. package/script/DeployPostDeploymentHooks.s.sol +0 -20
  116. package/script/DeployUpgradeGate.s.sol +0 -21
  117. package/script/GenerateDeterministicParams.s.sol +0 -43
  118. package/script/PrintRegisterUpgradePath.s.sol +0 -28
  119. package/script/PrintUpgradeCommand.s.sol +0 -13
  120. package/script/TestBackingCoinSwap.s.sol +0 -144
  121. package/script/TestV4Swap.s.sol +0 -133
  122. package/script/UpgradeCoinImpl.sol +0 -23
  123. package/script/UpgradeFactoryImpl.s.sol +0 -28
  124. package/script/UpgradeHooks.s.sol +0 -23
  125. package/src/deployment/CoinsDeployerBase.sol +0 -276
  126. /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.3.1",
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",
@@ -41,13 +41,13 @@
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,40 +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
- import {IHasSwapPath} from "../interfaces/ICoin.sol";
25
+ import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.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";
20
32
  import {LpPosition} from "../types/LpPosition.sol";
21
33
  import {V4Liquidity} from "../libs/V4Liquidity.sol";
22
34
  import {CoinRewardsV4} from "../libs/CoinRewardsV4.sol";
23
- import {ICoin} from "../interfaces/ICoin.sol";
24
- import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
25
35
  import {CoinCommon} from "../libs/CoinCommon.sol";
26
36
  import {CoinDopplerMultiCurve} from "../libs/CoinDopplerMultiCurve.sol";
27
37
  import {PoolStateReader} from "../libs/PoolStateReader.sol";
28
- import {IHasRewardsRecipients} from "../interfaces/ICoin.sol";
29
38
  import {CoinConfigurationVersions} from "../libs/CoinConfigurationVersions.sol";
30
- import {IUpgradeableV4Hook} from "../interfaces/IUpgradeableV4Hook.sol";
31
- import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
32
- import {MultiOwnable} from "../utils/MultiOwnable.sol";
33
- import {ERC165} from "@openzeppelin/contracts/utils/introspection/ERC165.sol";
34
- import {IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee} from "../interfaces/IUpgradeableV4Hook.sol";
35
- import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
36
- import {BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
39
+ import {CoinConstants} from "../libs/CoinConstants.sol";
37
40
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
38
41
  import {TickMath} from "../utils/uniswap/TickMath.sol";
39
42
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
40
- import {IHasCoinType} from "../interfaces/ICoin.sol";
41
- import {CoinConstants} from "../libs/CoinConstants.sol";
43
+ import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
42
44
 
43
45
  /// @title ZoraV4CoinHook
44
46
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -60,9 +62,10 @@ contract ZoraV4CoinHook is
60
62
  {
61
63
  using BalanceDeltaLibrary for BalanceDelta;
62
64
 
63
- /// @notice Mapping of trusted message senders - these are addresses that are trusted to provide a
64
- /// an original msg.sender
65
- mapping(address => bool) internal trustedMessageSender;
65
+ /// @dev DEPRECATED: This mapping is kept for storage compatibility. It doesn't matter that storage slots moved around
66
+ /// between versions since the contracts are immutable, but in some tests we do etching to test if a new hook fixes some bugs, so we want to maintain the storage slot order.
67
+ /// This slot previously held the mappings of trusted message senders.
68
+ mapping(address => bool) internal legacySlot0;
66
69
 
67
70
  /// @notice Mapping of pool keys to coins.
68
71
  mapping(bytes32 => IZoraV4CoinHook.PoolCoin) internal poolCoins;
@@ -73,27 +76,46 @@ contract ZoraV4CoinHook is
73
76
  /// @notice The upgrade gate contract - used to verify allowed upgrade paths
74
77
  IHooksUpgradeGate internal immutable upgradeGate;
75
78
 
79
+ /// @notice The trusted message sender lookup contract - used to determine if an address is trusted
80
+ ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
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
+
76
88
  /// @notice The constructor for the ZoraV4CoinHook.
77
89
  /// @param poolManager_ The Uniswap V4 pool manager
78
90
  /// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
79
- /// @param trustedMessageSenders_ The addresses of the trusted message senders - these are addresses that are trusted to provide a
91
+ /// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
80
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
81
95
  constructor(
82
96
  IPoolManager poolManager_,
83
97
  IDeployedCoinVersionLookup coinVersionLookup_,
84
- address[] memory trustedMessageSenders_,
85
- IHooksUpgradeGate upgradeGate_
98
+ ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
99
+ IHooksUpgradeGate upgradeGate_,
100
+ IZoraLimitOrderBookCoinsInterface zoraLimitOrderBook_,
101
+ IZoraHookRegistry zoraHookRegistry_
86
102
  ) BaseHook(poolManager_) {
87
103
  require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
88
-
89
104
  require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
105
+ require(address(zoraLimitOrderBook_) != address(0), ZoraLimitOrderBookCannotBeZeroAddress());
106
+ require(address(zoraHookRegistry_) != address(0), ZoraHookRegistryCannotBeZeroAddress());
107
+ require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
90
108
 
91
109
  coinVersionLookup = coinVersionLookup_;
92
110
  upgradeGate = upgradeGate_;
111
+ trustedMsgSenderLookup = trustedMsgSenderLookup_;
112
+ zoraLimitOrderBook = zoraLimitOrderBook_;
113
+ zoraHookRegistry = zoraHookRegistry_;
114
+ }
93
115
 
94
- for (uint256 i = 0; i < trustedMessageSenders_.length; i++) {
95
- trustedMessageSender[trustedMessageSenders_[i]] = true;
96
- }
116
+ /// @notice Returns the trusted message sender lookup contract
117
+ function getTrustedMsgSenderLookup() external view returns (ITrustedMsgSenderProviderLookup) {
118
+ return trustedMsgSenderLookup;
97
119
  }
98
120
 
99
121
  /// @notice Returns the uniswap v4 hook settings / permissions.
@@ -107,7 +129,7 @@ contract ZoraV4CoinHook is
107
129
  afterAddLiquidity: false,
108
130
  beforeRemoveLiquidity: false,
109
131
  afterRemoveLiquidity: false,
110
- beforeSwap: false,
132
+ beforeSwap: true,
111
133
  afterSwap: true,
112
134
  beforeDonate: false,
113
135
  afterDonate: false,
@@ -120,7 +142,7 @@ contract ZoraV4CoinHook is
120
142
 
121
143
  /// @inheritdoc IZoraV4CoinHook
122
144
  function isTrustedMessageSender(address sender) external view returns (bool) {
123
- return trustedMessageSender[sender];
145
+ return trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
124
146
  }
125
147
 
126
148
  /// @inheritdoc IZoraV4CoinHook
@@ -139,7 +161,8 @@ contract ZoraV4CoinHook is
139
161
  super.supportsInterface(interfaceId) ||
140
162
  interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
141
163
  interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
142
- interfaceId == type(IVersionedContract).interfaceId;
164
+ interfaceId == type(IVersionedContract).interfaceId ||
165
+ interfaceId == type(ISupportsLimitOrderFill).interfaceId;
143
166
  }
144
167
 
145
168
  /// @notice Internal fn generating the positions for a given pool key.
@@ -256,51 +279,6 @@ contract ZoraV4CoinHook is
256
279
 
257
280
  // Store the positions and mint the initial liquidity into the new pool
258
281
  _initializeForPositions(newKey, coin, positions);
259
-
260
- // Handle any remaining token balances by adding them to the last position
261
- // This ensures no tokens are left unminted during the migration process
262
- _mintExtraLiquidityAtLastPosition(sqrtPriceX96, newKey);
263
- }
264
-
265
- /// @notice Internal fn to add any remaining token balances to the last liquidity position.
266
- /// @param sqrtPriceX96 The sqrt price x96.
267
- /// @param poolKey The pool key.
268
- function _mintExtraLiquidityAtLastPosition(uint160 sqrtPriceX96, PoolKey memory poolKey) internal {
269
- // Check if there are any leftover token balances in the hook after migration
270
- // These could result from rounding or partial liquidity transfers
271
- uint256 currency0Balance = poolKey.currency0.balanceOfSelf();
272
- uint256 currency1Balance = poolKey.currency1.balanceOfSelf();
273
-
274
- // Get the stored positions for this pool to access the last position
275
- LpPosition[] storage positions = poolCoins[CoinCommon.hashPoolKey(poolKey)].positions;
276
-
277
- // Only proceed if there are actually leftover tokens to mint
278
- if (currency0Balance > 0 || currency1Balance > 0) {
279
- // Get reference to the last position where we'll add the extra liquidity
280
- LpPosition storage lastPosition = positions[positions.length - 1];
281
-
282
- // Calculate how much liquidity we can create with the remaining token balances
283
- // This uses the current pool price and the last position's tick range
284
- uint128 newLiquidity = LiquidityAmounts.getLiquidityForAmounts(
285
- sqrtPriceX96,
286
- TickMath.getSqrtPriceAtTick(lastPosition.tickLower),
287
- TickMath.getSqrtPriceAtTick(lastPosition.tickUpper),
288
- currency0Balance,
289
- currency1Balance
290
- );
291
-
292
- // Create a temporary array with just the last position to mint the extra liquidity
293
- LpPosition[] memory newPositions = new LpPosition[](1);
294
- newPositions[0] = lastPosition;
295
- newPositions[0].liquidity = newLiquidity; // Set the calculated liquidity amount
296
-
297
- // Mint the extra liquidity into the pool using the V4 liquidity manager
298
- V4Liquidity.lockAndMint(poolManager, poolKey, newPositions);
299
-
300
- // Update our internal tracking of the last position's liquidity
301
- // This keeps our records in sync with the actual pool state
302
- positions[positions.length - 1].liquidity += newPositions[0].liquidity;
303
- }
304
282
  }
305
283
 
306
284
  /// @notice Saves the positions for the coin and mints them into the pool
@@ -317,6 +295,27 @@ contract ZoraV4CoinHook is
317
295
  V4Liquidity.lockAndMint(poolManager, key, positions);
318
296
  }
319
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
+
320
319
  /// @notice Internal fn called when a swap is executed.
321
320
  /// @dev This hook is called from BaseHook library from uniswap v4.
322
321
  /// This hook:
@@ -337,6 +336,10 @@ contract ZoraV4CoinHook is
337
336
  BalanceDelta delta,
338
337
  bytes calldata hookData
339
338
  ) internal virtual override returns (bytes4, int128) {
339
+ if (_isInternalSwap(sender)) {
340
+ return (BaseHook.afterSwap.selector, 0);
341
+ }
342
+
340
343
  bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
341
344
 
342
345
  // get the coin address and positions for the pool key; they must have been set in the afterInitialize callback
@@ -379,9 +382,19 @@ contract ZoraV4CoinHook is
379
382
  );
380
383
  }
381
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
+
382
388
  return (BaseHook.afterSwap.selector, 0);
383
389
  }
384
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
+
385
398
  /// @dev Internal fn to allow for overriding market reward distribution logic
386
399
  function _distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal virtual {
387
400
  // get rewards distribution methodology from the coin
@@ -403,7 +416,7 @@ contract ZoraV4CoinHook is
403
416
  /// @return swapper The original message sender.
404
417
  /// @return senderIsTrusted Whether the sender is a trusted message sender.
405
418
  function _getOriginalMsgSender(address sender) internal view returns (address swapper, bool senderIsTrusted) {
406
- senderIsTrusted = trustedMessageSender[sender];
419
+ senderIsTrusted = trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
407
420
 
408
421
  // If getter function reverts, we return a 0 address by default and continue execution.
409
422
  try IMsgSender(sender).msgSender() returns (address _swapper) {
@@ -431,6 +444,11 @@ contract ZoraV4CoinHook is
431
444
  delete poolCoins[poolKeyHash];
432
445
  }
433
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
+
434
452
  /// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
435
453
  /// @dev Only required for coins using ETH as backing currency (currency = address(0)).
436
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,18 @@
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
+ /// @title ITrustedMsgSenderProviderLookup
11
+ /// @notice Interface for contracts that can determine if an address is a trusted message sender
12
+ /// @dev This interface allows the hook to delegate the trusted sender check to an external contract
13
+ interface ITrustedMsgSenderProviderLookup {
14
+ /// @notice Checks if an address is a trusted message sender provider
15
+ /// @param sender The address to check
16
+ /// @return true if the sender is trusted, false otherwise
17
+ function isTrustedMsgSenderProvider(address sender) external view returns (bool);
18
+ }