@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoralabs/coins",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
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,20 +35,22 @@
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
+ "abi-check:check": "../../scripts/abi-check.sh check",
53
+ "abi-check:generate": "../../scripts/abi-check.sh generate",
52
54
  "test-gas": "forge test --gas-report",
53
55
  "update-contract-version": "pnpm exec update-contract-version",
54
56
  "wagmi:generate": "pnpm run build:contracts:minimal && wagmi generate && pnpm exec rename-generated-abi-casing ./package/wagmiGenerated.ts"
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/
package/src/BaseCoin.sol CHANGED
@@ -11,6 +11,7 @@ import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
11
11
  import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
12
12
  import {ICoin, IHasTotalSupplyForPositions, IHasCoinType} from "./interfaces/ICoin.sol";
13
13
  import {IHasRewardsRecipients} from "./interfaces/IHasRewardsRecipients.sol";
14
+ import {IHasCreationInfo} from "./interfaces/IHasCreationInfo.sol";
14
15
  import {ICoinComments} from "./interfaces/ICoinComments.sol";
15
16
  import {IERC7572} from "./interfaces/IERC7572.sol";
16
17
  import {IUniswapV3Factory} from "./interfaces/IUniswapV3Factory.sol";
@@ -52,7 +53,7 @@ import {PoolState} from "./types/PoolState.sol";
52
53
  \$$$$$$ | $$$$$$ |$$$$$$\ $$ | \$$ |
53
54
  \______/ \______/ \______|\__| \__|
54
55
  */
55
- abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
56
+ abstract contract BaseCoin is ICoin, IHasCreationInfo, ContractVersionBase, ERC20PermitUpgradeable, MultiOwnable, ERC165Upgradeable {
56
57
  using SafeERC20 for IERC20;
57
58
 
58
59
  /// @notice The address of the protocol rewards contract
@@ -84,6 +85,11 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
84
85
  string private _name;
85
86
  /// @notice The symbol of the token
86
87
  string private _symbol;
88
+ /// @notice The timestamp when the coin was created
89
+ uint256 private _creationTimestamp;
90
+
91
+ /// @dev Transient storage slot for tracking deployment state
92
+ bytes32 private constant _IS_DEPLOYING_SLOT = keccak256("BaseCoin.isDeploying");
87
93
 
88
94
  /**
89
95
  * @notice The constructor for the static Coin contract deployment shared across all Coins.
@@ -125,6 +131,12 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
125
131
  uint160 sqrtPriceX96,
126
132
  PoolConfiguration memory poolConfiguration_
127
133
  ) public virtual initializer {
134
+ // Set transient deploying flag for launch fee bypass
135
+ _setIsDeploying(true);
136
+
137
+ // Record creation timestamp for launch fee calculation
138
+ _creationTimestamp = block.timestamp;
139
+
128
140
  currency = currency_;
129
141
  // we need to set this before initialization, because
130
142
  // distributing currency relies on the poolkey being set since the hooks
@@ -253,7 +265,25 @@ abstract contract BaseCoin is ICoin, ContractVersionBase, ERC20PermitUpgradeable
253
265
  interfaceId == type(IHasPoolKey).interfaceId ||
254
266
  interfaceId == type(IHasCoinType).interfaceId ||
255
267
  interfaceId == type(IHasTotalSupplyForPositions).interfaceId ||
256
- interfaceId == type(IHasSwapPath).interfaceId;
268
+ interfaceId == type(IHasSwapPath).interfaceId ||
269
+ interfaceId == type(IHasCreationInfo).interfaceId;
270
+ }
271
+
272
+ /// @inheritdoc IHasCreationInfo
273
+ function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying) {
274
+ creationTimestamp = _creationTimestamp;
275
+ bytes32 slot = _IS_DEPLOYING_SLOT;
276
+ assembly {
277
+ isDeploying := tload(slot)
278
+ }
279
+ }
280
+
281
+ /// @dev Sets the transient deploying flag
282
+ function _setIsDeploying(bool value) internal {
283
+ bytes32 slot = _IS_DEPLOYING_SLOT;
284
+ assembly {
285
+ tstore(slot, value)
286
+ }
257
287
  }
258
288
 
259
289
  /// @dev Overrides ERC20's _update function to emit a superset `CoinTransfer` event
@@ -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,44 @@
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 {IHasCreationInfo} from "../interfaces/IHasCreationInfo.sol";
28
+ import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol";
29
+ import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
30
+ import {IUpgradeableV4Hook, IUpgradeableDestinationV4Hook, IUpgradeableDestinationV4HookWithUpdateableFee, BurnedPosition} from "../interfaces/IUpgradeableV4Hook.sol";
31
+ import {IHooksUpgradeGate} from "../interfaces/IHooksUpgradeGate.sol";
32
+ import {IZoraHookRegistry} from "../interfaces/IZoraHookRegistry.sol";
33
+ import {IZoraLimitOrderBookCoinsInterface} from "../interfaces/IZoraLimitOrderBookCoinsInterface.sol";
21
34
  import {LpPosition} from "../types/LpPosition.sol";
22
35
  import {V4Liquidity} from "../libs/V4Liquidity.sol";
23
36
  import {CoinRewardsV4} from "../libs/CoinRewardsV4.sol";
24
- import {ICoin} from "../interfaces/ICoin.sol";
25
- import {IDeployedCoinVersionLookup} from "../interfaces/IDeployedCoinVersionLookup.sol";
26
37
  import {CoinCommon} from "../libs/CoinCommon.sol";
27
38
  import {CoinDopplerMultiCurve} from "../libs/CoinDopplerMultiCurve.sol";
28
39
  import {PoolStateReader} from "../libs/PoolStateReader.sol";
29
- import {IHasRewardsRecipients} from "../interfaces/ICoin.sol";
30
40
  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";
41
+ import {CoinConstants} from "../libs/CoinConstants.sol";
38
42
  import {LiquidityAmounts} from "../utils/uniswap/LiquidityAmounts.sol";
39
43
  import {TickMath} from "../utils/uniswap/TickMath.sol";
40
44
  import {ContractVersionBase, IVersionedContract} from "../version/ContractVersionBase.sol";
41
- import {IHasCoinType} from "../interfaces/ICoin.sol";
42
- import {CoinConstants} from "../libs/CoinConstants.sol";
45
+ import {ISupportsLimitOrderFill} from "../interfaces/ISupportsLimitOrderFill.sol";
43
46
 
44
47
  /// @title ZoraV4CoinHook
45
48
  /// @notice Uniswap V4 hook that automatically handles fee collection and reward distributions on every swap,
@@ -78,26 +81,38 @@ contract ZoraV4CoinHook is
78
81
  /// @notice The trusted message sender lookup contract - used to determine if an address is trusted
79
82
  ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
80
83
 
84
+ /// @notice The Zora limit order book contract - used to fill limit orders during swaps
85
+ IZoraLimitOrderBookCoinsInterface internal immutable zoraLimitOrderBook;
86
+
87
+ /// @notice The Zora hook registry
88
+ IZoraHookRegistry internal immutable zoraHookRegistry;
89
+
81
90
  /// @notice The constructor for the ZoraV4CoinHook.
82
91
  /// @param poolManager_ The Uniswap V4 pool manager
83
92
  /// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
84
93
  /// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
85
94
  /// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
95
+ /// @param zoraLimitOrderBook_ The Zora limit order book contract for filling orders during swaps
96
+ /// @param zoraHookRegistry_ The Zora hook registry contract for identifying registered hooks
86
97
  constructor(
87
98
  IPoolManager poolManager_,
88
99
  IDeployedCoinVersionLookup coinVersionLookup_,
89
100
  ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
90
- IHooksUpgradeGate upgradeGate_
101
+ IHooksUpgradeGate upgradeGate_,
102
+ IZoraLimitOrderBookCoinsInterface zoraLimitOrderBook_,
103
+ IZoraHookRegistry zoraHookRegistry_
91
104
  ) BaseHook(poolManager_) {
92
105
  require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
93
-
94
106
  require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
95
-
107
+ require(address(zoraLimitOrderBook_) != address(0), ZoraLimitOrderBookCannotBeZeroAddress());
108
+ require(address(zoraHookRegistry_) != address(0), ZoraHookRegistryCannotBeZeroAddress());
96
109
  require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
97
110
 
98
111
  coinVersionLookup = coinVersionLookup_;
99
112
  upgradeGate = upgradeGate_;
100
113
  trustedMsgSenderLookup = trustedMsgSenderLookup_;
114
+ zoraLimitOrderBook = zoraLimitOrderBook_;
115
+ zoraHookRegistry = zoraHookRegistry_;
101
116
  }
102
117
 
103
118
  /// @notice Returns the trusted message sender lookup contract
@@ -116,7 +131,7 @@ contract ZoraV4CoinHook is
116
131
  afterAddLiquidity: false,
117
132
  beforeRemoveLiquidity: false,
118
133
  afterRemoveLiquidity: false,
119
- beforeSwap: false,
134
+ beforeSwap: true,
120
135
  afterSwap: true,
121
136
  beforeDonate: false,
122
137
  afterDonate: false,
@@ -148,7 +163,8 @@ contract ZoraV4CoinHook is
148
163
  super.supportsInterface(interfaceId) ||
149
164
  interfaceId == type(IUpgradeableDestinationV4Hook).interfaceId ||
150
165
  interfaceId == type(IUpgradeableDestinationV4HookWithUpdateableFee).interfaceId ||
151
- interfaceId == type(IVersionedContract).interfaceId;
166
+ interfaceId == type(IVersionedContract).interfaceId ||
167
+ interfaceId == type(ISupportsLimitOrderFill).interfaceId;
152
168
  }
153
169
 
154
170
  /// @notice Internal fn generating the positions for a given pool key.
@@ -281,6 +297,77 @@ contract ZoraV4CoinHook is
281
297
  V4Liquidity.lockAndMint(poolManager, key, positions);
282
298
  }
283
299
 
300
+ /// @notice Transiently stores the tick before a swap and calculates the launch fee.
301
+ /// @dev This is used in `_afterSwap` to determine the ticks crossed during the swap.
302
+ /// Also returns a dynamic fee that decays from 99% to 1% over 10 seconds after coin creation.
303
+ function _beforeSwap(
304
+ address sender,
305
+ PoolKey calldata key,
306
+ SwapParams calldata,
307
+ bytes calldata
308
+ ) internal virtual override returns (bytes4, BeforeSwapDelta, uint24) {
309
+ if (_isInternalSwap(sender)) {
310
+ return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), 0);
311
+ }
312
+
313
+ // Store tick for user-initiated swaps only
314
+ (, int24 currentTick, , ) = StateLibrary.getSlot0(poolManager, key.toId());
315
+
316
+ TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
317
+ TransientSlot.tstore(slot, int256(currentTick));
318
+
319
+ // Calculate launch fee
320
+ bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
321
+ address coin = poolCoins[poolKeyHash].coin;
322
+ uint24 fee = _calculateLaunchFee(coin);
323
+
324
+ return (BaseHook.beforeSwap.selector, BeforeSwapDelta.wrap(0), fee);
325
+ }
326
+
327
+ /// @notice Calculates the launch fee based on coin creation time
328
+ /// @dev Returns fee with OVERRIDE_FEE_FLAG to signal V4 to use this fee.
329
+ /// Fee decays linearly from LAUNCH_FEE_START (99%) to LP_FEE_V4 (1%) over LAUNCH_FEE_DURATION.
330
+ /// Returns LP_FEE_V4 for legacy coins that don't support IHasCreationInfo.
331
+ /// Bypasses launch fee (returns LP_FEE_V4) during initial coin deployment.
332
+ /// @param coin The coin address
333
+ /// @return fee The calculated fee with OVERRIDE_FEE_FLAG
334
+ function _calculateLaunchFee(address coin) internal view returns (uint24 fee) {
335
+ // Check if coin supports creation info interface (legacy coins won't)
336
+ try IERC165(coin).supportsInterface(type(IHasCreationInfo).interfaceId) returns (bool supported) {
337
+ if (!supported) {
338
+ // Legacy coin - use normal LP fee
339
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
340
+ }
341
+ } catch {
342
+ // supportsInterface call failed - use normal LP fee
343
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
344
+ }
345
+
346
+ // Get creation info from coin
347
+ (uint256 creationTimestamp, bool isDeploying) = IHasCreationInfo(coin).creationInfo();
348
+
349
+ // Bypass launch fee during initial deployment
350
+ if (isDeploying) {
351
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
352
+ }
353
+
354
+ // Calculate elapsed time since creation
355
+ uint256 elapsed = block.timestamp - creationTimestamp;
356
+
357
+ // If launch fee duration has passed, use normal LP fee
358
+ if (elapsed >= CoinConstants.LAUNCH_FEE_DURATION) {
359
+ return CoinConstants.OVERRIDE_FEE_FLAG | CoinConstants.LP_FEE_V4;
360
+ }
361
+
362
+ // Linear decay: fee = startFee - (elapsed / duration) * (startFee - endFee)
363
+ uint256 startFee = CoinConstants.LAUNCH_FEE_START;
364
+ uint256 endFee = CoinConstants.LP_FEE_V4;
365
+ uint256 feeReduction = (elapsed * (startFee - endFee)) / CoinConstants.LAUNCH_FEE_DURATION;
366
+ fee = uint24(startFee - feeReduction);
367
+
368
+ return CoinConstants.OVERRIDE_FEE_FLAG | fee;
369
+ }
370
+
284
371
  /// @notice Internal fn called when a swap is executed.
285
372
  /// @dev This hook is called from BaseHook library from uniswap v4.
286
373
  /// This hook:
@@ -301,6 +388,10 @@ contract ZoraV4CoinHook is
301
388
  BalanceDelta delta,
302
389
  bytes calldata hookData
303
390
  ) internal virtual override returns (bytes4, int128) {
391
+ if (_isInternalSwap(sender)) {
392
+ return (BaseHook.afterSwap.selector, 0);
393
+ }
394
+
304
395
  bytes32 poolKeyHash = CoinCommon.hashPoolKey(key);
305
396
 
306
397
  // get the coin address and positions for the pool key; they must have been set in the afterInitialize callback
@@ -343,9 +434,24 @@ contract ZoraV4CoinHook is
343
434
  );
344
435
  }
345
436
 
437
+ (int24 tickBeforeSwap, int24 tickAfterSwap) = _getSwapTickRange(key);
438
+
439
+ // Derive fill direction from actual tick movement
440
+ if (tickAfterSwap != tickBeforeSwap) {
441
+ bool isCurrency0 = tickAfterSwap > tickBeforeSwap;
442
+ zoraLimitOrderBook.fill(key, isCurrency0, tickBeforeSwap, tickAfterSwap, CoinConstants.SENTINEL_DEFAULT_LIMIT_ORDER_FILL_COUNT, address(0));
443
+ }
444
+
346
445
  return (BaseHook.afterSwap.selector, 0);
347
446
  }
348
447
 
448
+ /// @dev Get the tick range of a swap
449
+ function _getSwapTickRange(PoolKey calldata key) internal view returns (int24 tickBeforeSwap, int24 tickAfterSwap) {
450
+ TransientSlot.Int256Slot slot = TransientSlot.asInt256(CoinConstants._BEFORE_SWAP_TICK_SLOT);
451
+ tickBeforeSwap = int24(int256(TransientSlot.tload(slot)));
452
+ (, tickAfterSwap, , ) = StateLibrary.getSlot0(poolManager, key.toId());
453
+ }
454
+
349
455
  /// @dev Internal fn to allow for overriding market reward distribution logic
350
456
  function _distributeMarketRewards(Currency currency, uint128 fees, IHasRewardsRecipients coin, address tradeReferrer) internal virtual {
351
457
  // get rewards distribution methodology from the coin
@@ -395,6 +501,11 @@ contract ZoraV4CoinHook is
395
501
  delete poolCoins[poolKeyHash];
396
502
  }
397
503
 
504
+ /// @dev Checks if the swap is internal and should skip hook operations
505
+ function _isInternalSwap(address sender) internal view returns (bool) {
506
+ return sender == address(this) || sender == address(zoraLimitOrderBook) || zoraHookRegistry.isRegisteredHook(sender);
507
+ }
508
+
398
509
  /// @notice Receives ETH from the pool manager for ETH-backed coins during fee collection.
399
510
  /// @dev Only required for coins using ETH as backing currency (currency = address(0)).
400
511
  /// 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
  }