@zoralabs/coins 2.3.0 → 2.4.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.
- package/.turbo/turbo-build$colon$js.log +119 -101
- package/CHANGELOG.md +31 -1
- package/README.md +1 -0
- package/abis/AddressConstants.json +7 -0
- package/abis/BaseTest.json +65 -3
- package/abis/BuySupplyWithV4SwapHook.json +429 -0
- package/abis/FeeEstimatorHook.json +23 -0
- package/abis/ITrustedMsgSenderProviderLookup.json +21 -0
- package/abis/IUniswapV4Router04.json +484 -0
- package/abis/IZoraV4CoinHook.json +5 -0
- package/abis/MockAirlock.json +39 -0
- package/abis/SimpleERC20.json +326 -0
- package/abis/TrustedMsgSenderProviderLookup.json +215 -0
- package/abis/VmContractHelper242.json +233 -0
- package/abis/ZoraV4CoinHook.json +21 -3
- package/addresses/8453.json +7 -9
- package/audits/report-cantinacode-zora-1021.pdf +0 -0
- package/dist/index.cjs +140 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +139 -18
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +205 -28
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +5 -1
- package/package/wagmiGenerated.ts +139 -18
- package/package.json +3 -3
- package/script/DeployPostDeploymentHooks.s.sol +1 -3
- package/script/DeployTrustedMsgSenderLookup.s.sol +20 -0
- package/src/deployment/CoinsDeployerBase.sol +31 -9
- package/src/hooks/ZoraV4CoinHook.sol +19 -55
- package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
- package/src/interfaces/ITrustedMsgSenderProviderLookup.sol +18 -0
- package/src/interfaces/IZoraV4CoinHook.sol +3 -0
- package/src/libs/HooksDeployment.sol +9 -8
- package/src/libs/V4Liquidity.sol +50 -6
- package/src/utils/AutoSwapper.sol +1 -1
- package/src/utils/TrustedMsgSenderProviderLookup.sol +73 -0
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/BuySupplyWithV4SwapHook.t.sol +509 -0
- package/test/Coin.t.sol +21 -9
- package/test/CoinUniV4.t.sol +1 -2
- package/test/ContentCoinRewards.t.sol +1 -3
- package/test/CreatorCoin.t.sol +1 -4
- package/test/CreatorCoinRewards.t.sol +5 -3
- package/test/Factory.t.sol +3 -3
- package/test/HooksDeployment.t.sol +58 -6
- package/test/LiquidityMigration.t.sol +6 -2
- package/test/MultiOwnable.t.sol +4 -4
- package/test/TrustedMsgSenderProviderLookup.t.sol +112 -0
- package/test/Upgrades.t.sol +41 -27
- package/test/ZoraHookRegistry.t.sol +19 -9
- package/test/mocks/MockAirlock.sol +22 -0
- package/test/mocks/SimpleERC20.sol +8 -0
- package/test/utils/BaseTest.sol +185 -6
- package/test/utils/FeeEstimatorHook.sol +3 -1
- package/test/utils/TrustedSenderTestHelper.sol +18 -0
- package/test/utils/hookmate/README.md +50 -0
- package/test/utils/hookmate/artifacts/DeployHelper.sol +20 -0
- package/test/utils/hookmate/artifacts/Permit2.sol +16 -0
- package/test/utils/hookmate/artifacts/UniversalRouter.sol +29 -0
- package/test/utils/hookmate/artifacts/V4PoolManager.sol +17 -0
- package/test/utils/hookmate/artifacts/V4PositionManager.sol +23 -0
- package/test/utils/hookmate/artifacts/V4Quoter.sol +17 -0
- package/test/utils/hookmate/artifacts/V4Router.sol +18 -0
- package/test/utils/hookmate/constants/AddressConstants.sol +193 -0
- package/test/utils/hookmate/interfaces/router/IUniswapV4Router04.sol +173 -0
- package/test/utils/hookmate/interfaces/router/PathKey.sol +34 -0
- package/test/utils/hookmate/test/utils/SwapFeeEventAsserter.sol +24 -0
- package/wagmi.config.ts +1 -1
- package/src/utils/uniswap/BytesLib.sol +0 -35
- package/src/utils/uniswap/Path.sol +0 -31
- /package/abis/{VmContractHelper226.json → VmContractHelper235.json} +0 -0
|
@@ -18,6 +18,9 @@ import {ProxyShim} from "../../test/utils/ProxyShim.sol";
|
|
|
18
18
|
import {CreatorCoin} from "../CreatorCoin.sol";
|
|
19
19
|
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
|
|
20
20
|
import {HookUpgradeGate} from "../hooks/HookUpgradeGate.sol";
|
|
21
|
+
import {BuySupplyWithV4SwapHook} from "../hooks/deployment/BuySupplyWithV4SwapHook.sol";
|
|
22
|
+
import {TrustedMsgSenderProviderLookup} from "../utils/TrustedMsgSenderProviderLookup.sol";
|
|
23
|
+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
|
|
21
24
|
|
|
22
25
|
contract CoinsDeployerBase is ProxyDeployerScript {
|
|
23
26
|
address internal constant PROTOCOL_REWARDS = 0x7777777F279eba3d3Ad8F4E708545291A6fDBA8B;
|
|
@@ -38,6 +41,8 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
38
41
|
address buySupplyWithSwapRouterHook;
|
|
39
42
|
address zoraV4CoinHook;
|
|
40
43
|
address hookUpgradeGate;
|
|
44
|
+
// trusted sender lookup
|
|
45
|
+
address trustedMsgSenderLookup;
|
|
41
46
|
// Hook deployment salt (for deterministic deployment)
|
|
42
47
|
bytes32 zoraV4CoinHookSalt;
|
|
43
48
|
bool isDev;
|
|
@@ -69,6 +74,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
69
74
|
vm.serializeAddress(objectKey, "CREATOR_COIN_IMPL", deployment.creatorCoinImpl);
|
|
70
75
|
vm.serializeAddress(objectKey, "HOOK_UPGRADE_GATE", deployment.hookUpgradeGate);
|
|
71
76
|
vm.serializeAddress(objectKey, "ZORA_HOOK_REGISTRY", deployment.zoraHookRegistry);
|
|
77
|
+
vm.serializeAddress(objectKey, "TRUSTED_MSG_SENDER_LOOKUP", deployment.trustedMsgSenderLookup);
|
|
72
78
|
string memory result = vm.serializeAddress(objectKey, "COIN_V4_IMPL", deployment.coinV4Impl);
|
|
73
79
|
|
|
74
80
|
vm.writeJson(result, addressesFile(deployment.isDev));
|
|
@@ -97,6 +103,7 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
97
103
|
deployment.creatorCoinImpl = readAddressOrDefaultToZero(json, "CREATOR_COIN_IMPL");
|
|
98
104
|
deployment.hookUpgradeGate = readAddressOrDefaultToZero(json, "HOOK_UPGRADE_GATE");
|
|
99
105
|
deployment.zoraHookRegistry = readAddressOrDefaultToZero(json, "ZORA_HOOK_REGISTRY");
|
|
106
|
+
deployment.trustedMsgSenderLookup = readAddressOrDefaultToZero(json, "TRUSTED_MSG_SENDER_LOOKUP");
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
function deployCoinV4Impl() internal returns (ContentCoin) {
|
|
@@ -123,14 +130,14 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
123
130
|
return new ZoraFactoryImpl({coinV4Impl_: coinV4Impl_, creatorCoinImpl_: creatorCoinImpl_, hook_: hook_, zoraHookRegistry_: zoraHookRegistry_});
|
|
124
131
|
}
|
|
125
132
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
function deployBuySupplyWithV4SwapHook(CoinsDeployment memory deployment) internal returns (BuySupplyWithV4SwapHook) {
|
|
134
|
+
return
|
|
135
|
+
new BuySupplyWithV4SwapHook({
|
|
136
|
+
_factory: IZoraFactory(deployment.zoraFactory),
|
|
137
|
+
_swapRouter: getUniswapSwapRouter(),
|
|
138
|
+
_poolManager: getUniswapV4PoolManager()
|
|
139
|
+
});
|
|
140
|
+
}
|
|
134
141
|
|
|
135
142
|
function deployUpgradeGate(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
|
|
136
143
|
deployment.hookUpgradeGate = address(new HookUpgradeGate(getProxyAdmin()));
|
|
@@ -138,14 +145,23 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
138
145
|
return deployment;
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
function deployTrustedMsgSenderLookup(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
|
|
149
|
+
// Deploy the contract directly using constructor
|
|
150
|
+
deployment.trustedMsgSenderLookup = address(new TrustedMsgSenderProviderLookup(getDefaultTrustedMessageSenders(), getProxyAdmin()));
|
|
151
|
+
|
|
152
|
+
return deployment;
|
|
153
|
+
}
|
|
154
|
+
|
|
141
155
|
function deployZoraV4CoinHook(CoinsDeployment memory deployment) internal returns (IHooks hook, bytes32 salt) {
|
|
156
|
+
require(deployment.trustedMsgSenderLookup != address(0), "Trusted message sender lookup not deployed");
|
|
157
|
+
|
|
142
158
|
return
|
|
143
159
|
HooksDeployment.deployHookWithExistingOrNewSalt(
|
|
144
160
|
HooksDeployment.FOUNDRY_SCRIPT_ADDRESS,
|
|
145
161
|
HooksDeployment.makeHookCreationCode(
|
|
146
162
|
getUniswapV4PoolManager(),
|
|
147
163
|
deployment.zoraFactory,
|
|
148
|
-
|
|
164
|
+
ITrustedMsgSenderProviderLookup(deployment.trustedMsgSenderLookup),
|
|
149
165
|
deployment.hookUpgradeGate
|
|
150
166
|
),
|
|
151
167
|
deployment.zoraV4CoinHookSalt
|
|
@@ -174,6 +190,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
174
190
|
function deployImpls(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
|
|
175
191
|
// Deploy implementation contracts
|
|
176
192
|
|
|
193
|
+
// Deploy trusted message sender lookup first
|
|
194
|
+
deployment = deployTrustedMsgSenderLookup(deployment);
|
|
195
|
+
|
|
177
196
|
// Deploy hook first, then use its address for coin v4 impl
|
|
178
197
|
console.log("deploying content coin hook");
|
|
179
198
|
(IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
|
|
@@ -190,6 +209,9 @@ contract CoinsDeployerBase is ProxyDeployerScript {
|
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
function deployHooks(CoinsDeployment memory deployment) internal returns (CoinsDeployment memory) {
|
|
212
|
+
// Deploy trusted message sender lookup first
|
|
213
|
+
deployment = deployTrustedMsgSenderLookup(deployment);
|
|
214
|
+
|
|
193
215
|
// Deploy hook first, then use its address for coin v4 impl
|
|
194
216
|
(IHooks zoraV4CoinHook, bytes32 usedSalt) = deployZoraV4CoinHook(deployment);
|
|
195
217
|
deployment.zoraV4CoinHook = address(zoraV4CoinHook);
|
|
@@ -16,6 +16,7 @@ import {Currency} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
|
16
16
|
import {SwapParams} from "@uniswap/v4-core/src/types/PoolOperation.sol";
|
|
17
17
|
import {IZoraV4CoinHook} from "../interfaces/IZoraV4CoinHook.sol";
|
|
18
18
|
import {IMsgSender} from "../interfaces/IMsgSender.sol";
|
|
19
|
+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
|
|
19
20
|
import {IHasSwapPath} from "../interfaces/ICoin.sol";
|
|
20
21
|
import {LpPosition} from "../types/LpPosition.sol";
|
|
21
22
|
import {V4Liquidity} from "../libs/V4Liquidity.sol";
|
|
@@ -60,9 +61,10 @@ contract ZoraV4CoinHook is
|
|
|
60
61
|
{
|
|
61
62
|
using BalanceDeltaLibrary for BalanceDelta;
|
|
62
63
|
|
|
63
|
-
/// @
|
|
64
|
-
///
|
|
65
|
-
|
|
64
|
+
/// @dev DEPRECATED: This mapping is kept for storage compatibility. It doesn't matter that storage slots moved around
|
|
65
|
+
/// 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.
|
|
66
|
+
/// This slot previously held the mappings of trusted message senders.
|
|
67
|
+
mapping(address => bool) internal legacySlot0;
|
|
66
68
|
|
|
67
69
|
/// @notice Mapping of pool keys to coins.
|
|
68
70
|
mapping(bytes32 => IZoraV4CoinHook.PoolCoin) internal poolCoins;
|
|
@@ -73,27 +75,34 @@ contract ZoraV4CoinHook is
|
|
|
73
75
|
/// @notice The upgrade gate contract - used to verify allowed upgrade paths
|
|
74
76
|
IHooksUpgradeGate internal immutable upgradeGate;
|
|
75
77
|
|
|
78
|
+
/// @notice The trusted message sender lookup contract - used to determine if an address is trusted
|
|
79
|
+
ITrustedMsgSenderProviderLookup internal immutable trustedMsgSenderLookup;
|
|
80
|
+
|
|
76
81
|
/// @notice The constructor for the ZoraV4CoinHook.
|
|
77
82
|
/// @param poolManager_ The Uniswap V4 pool manager
|
|
78
83
|
/// @param coinVersionLookup_ The coin version lookup contract - used to determine if an address is a coin and what version it is.
|
|
79
|
-
/// @param
|
|
84
|
+
/// @param trustedMsgSenderLookup_ The trusted message sender lookup contract - used to determine if an address is trusted
|
|
80
85
|
/// @param upgradeGate_ The upgrade gate contract for managing hook upgrades
|
|
81
86
|
constructor(
|
|
82
87
|
IPoolManager poolManager_,
|
|
83
88
|
IDeployedCoinVersionLookup coinVersionLookup_,
|
|
84
|
-
|
|
89
|
+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup_,
|
|
85
90
|
IHooksUpgradeGate upgradeGate_
|
|
86
91
|
) BaseHook(poolManager_) {
|
|
87
92
|
require(address(coinVersionLookup_) != address(0), CoinVersionLookupCannotBeZeroAddress());
|
|
88
93
|
|
|
89
94
|
require(address(upgradeGate_) != address(0), UpgradeGateCannotBeZeroAddress());
|
|
90
95
|
|
|
96
|
+
require(address(trustedMsgSenderLookup_) != address(0), TrustedMsgSenderLookupCannotBeZeroAddress());
|
|
97
|
+
|
|
91
98
|
coinVersionLookup = coinVersionLookup_;
|
|
92
99
|
upgradeGate = upgradeGate_;
|
|
100
|
+
trustedMsgSenderLookup = trustedMsgSenderLookup_;
|
|
101
|
+
}
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
/// @notice Returns the trusted message sender lookup contract
|
|
104
|
+
function getTrustedMsgSenderLookup() external view returns (ITrustedMsgSenderProviderLookup) {
|
|
105
|
+
return trustedMsgSenderLookup;
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
/// @notice Returns the uniswap v4 hook settings / permissions.
|
|
@@ -120,7 +129,7 @@ contract ZoraV4CoinHook is
|
|
|
120
129
|
|
|
121
130
|
/// @inheritdoc IZoraV4CoinHook
|
|
122
131
|
function isTrustedMessageSender(address sender) external view returns (bool) {
|
|
123
|
-
return
|
|
132
|
+
return trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
|
|
124
133
|
}
|
|
125
134
|
|
|
126
135
|
/// @inheritdoc IZoraV4CoinHook
|
|
@@ -256,51 +265,6 @@ contract ZoraV4CoinHook is
|
|
|
256
265
|
|
|
257
266
|
// Store the positions and mint the initial liquidity into the new pool
|
|
258
267
|
_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
268
|
}
|
|
305
269
|
|
|
306
270
|
/// @notice Saves the positions for the coin and mints them into the pool
|
|
@@ -403,7 +367,7 @@ contract ZoraV4CoinHook is
|
|
|
403
367
|
/// @return swapper The original message sender.
|
|
404
368
|
/// @return senderIsTrusted Whether the sender is a trusted message sender.
|
|
405
369
|
function _getOriginalMsgSender(address sender) internal view returns (address swapper, bool senderIsTrusted) {
|
|
406
|
-
senderIsTrusted =
|
|
370
|
+
senderIsTrusted = trustedMsgSenderLookup.isTrustedMsgSenderProvider(sender);
|
|
407
371
|
|
|
408
372
|
// If getter function reverts, we return a 0 address by default and continue execution.
|
|
409
373
|
try IMsgSender(sender).msgSender() returns (address _swapper) {
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
5
|
+
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
|
|
6
|
+
import {Currency, CurrencyLibrary} from "@uniswap/v4-core/src/types/Currency.sol";
|
|
7
|
+
import {IPoolManager, SwapParams} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
8
|
+
import {TickMath} from "@uniswap/v4-core/src/libraries/TickMath.sol";
|
|
9
|
+
import {BalanceDelta, BalanceDeltaLibrary} from "@uniswap/v4-core/src/types/BalanceDelta.sol";
|
|
10
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
11
|
+
import {BaseCoinDeployHook} from "./BaseCoinDeployHook.sol";
|
|
12
|
+
import {IUniswapV3SwapCallback} from "../../interfaces/IUniswapV3SwapCallback.sol";
|
|
13
|
+
import {ICoin} from "../../interfaces/ICoin.sol";
|
|
14
|
+
import {ISwapRouter} from "../../interfaces/ISwapRouter.sol";
|
|
15
|
+
import {IZoraFactory} from "../../interfaces/IZoraFactory.sol";
|
|
16
|
+
import {ICoinV3} from "../../interfaces/ICoinV3.sol";
|
|
17
|
+
import {CoinConfigurationVersions} from "../../libs/CoinConfigurationVersions.sol";
|
|
18
|
+
import {TransientStateLibrary} from "@uniswap/v4-core/src/libraries/TransientStateLibrary.sol";
|
|
19
|
+
import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
|
|
20
|
+
|
|
21
|
+
/// @title BuySupplyWithV4SwapHook
|
|
22
|
+
/// @notice Hook for purchasing initial coin supply with flexible swap routing
|
|
23
|
+
/// @dev Capabilities:
|
|
24
|
+
/// - ETH → V3 swap → V4 swap → coin (e.g., ETH → ZORA → Creator Coin → Content Coin)
|
|
25
|
+
/// - ETH → V3 swap → coin (e.g., ETH → ZORA for ZORA-backed coin)
|
|
26
|
+
/// - ETH → V4 swap → coin (direct ETH-paired coins)
|
|
27
|
+
/// - ERC20 → V4 swap → coin (e.g., Creator Coins → Content Coin)
|
|
28
|
+
/// - Slippage protection with minAmountOut validation
|
|
29
|
+
///
|
|
30
|
+
/// Limitations:
|
|
31
|
+
/// - V3 swaps only support ETH as input currency
|
|
32
|
+
/// - ERC20 input currencies require pre-approval
|
|
33
|
+
/// - V3 and V4 routes must connect properly (V3 output = V4 input)
|
|
34
|
+
contract BuySupplyWithV4SwapHook is BaseCoinDeployHook {
|
|
35
|
+
using BalanceDeltaLibrary for BalanceDelta;
|
|
36
|
+
using SafeERC20 for IERC20;
|
|
37
|
+
using CurrencyLibrary for Currency;
|
|
38
|
+
using Path for bytes;
|
|
39
|
+
|
|
40
|
+
// ============ STATE VARIABLES ============
|
|
41
|
+
|
|
42
|
+
ISwapRouter public immutable swapRouter;
|
|
43
|
+
IPoolManager public immutable poolManager;
|
|
44
|
+
|
|
45
|
+
// ============ STRUCTS ============
|
|
46
|
+
|
|
47
|
+
struct InitialSupplyParams {
|
|
48
|
+
address buyRecipient; // Who gets the coins
|
|
49
|
+
bytes v3Route; // V3 route from ETH to backing currency
|
|
50
|
+
PoolKey[] v4Route; // V4 route from backing currency to coin
|
|
51
|
+
address inputCurrency; // Currency to use for the V3 swap
|
|
52
|
+
uint256 inputAmount; // Amount of input currency to use for the V3 swap
|
|
53
|
+
uint256 minAmountOut; // Minimum amount of coins to receive from final swap
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
event BuyInitialSupply(
|
|
57
|
+
address indexed coin,
|
|
58
|
+
address indexed recipient,
|
|
59
|
+
uint256 indexed coinsPurchased,
|
|
60
|
+
bytes v3Route,
|
|
61
|
+
PoolKey[] v4Route,
|
|
62
|
+
address inputCurrency,
|
|
63
|
+
uint256 inputAmount,
|
|
64
|
+
uint256 v4SwapInput
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// ============ ERRORS ============
|
|
68
|
+
|
|
69
|
+
error OnlyPoolManager();
|
|
70
|
+
error InsufficientInputCurrency(uint256 inputAmount, uint256 availableAmount);
|
|
71
|
+
error V3RouteCannotStartWithInputCurrency();
|
|
72
|
+
error V3RouteDoesNotConnectToV4RouteStart();
|
|
73
|
+
error InsufficientOutputAmount();
|
|
74
|
+
|
|
75
|
+
// ============ CONSTRUCTOR ============
|
|
76
|
+
|
|
77
|
+
constructor(IZoraFactory _factory, address _swapRouter, address _poolManager) BaseCoinDeployHook(_factory) {
|
|
78
|
+
swapRouter = ISwapRouter(_swapRouter);
|
|
79
|
+
poolManager = IPoolManager(_poolManager);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============ MAIN HOOK FUNCTION ============
|
|
83
|
+
|
|
84
|
+
/// @notice Hook that buys supply for a coin using V3->V4 two-step swap routing
|
|
85
|
+
/// @dev Returns abi encoded (uint256 amountCurrency, uint256 coinsPurchased)
|
|
86
|
+
function _afterCoinDeploy(address, ICoin coin, bytes calldata hookData) internal override returns (bytes memory) {
|
|
87
|
+
// STEP 1: Decode parameters
|
|
88
|
+
InitialSupplyParams memory params = abi.decode(hookData, (InitialSupplyParams));
|
|
89
|
+
|
|
90
|
+
PoolKey[] memory v4Route = _buildV4RouteToCoin(coin, params.v4Route);
|
|
91
|
+
|
|
92
|
+
// STEP 2: Validate routes
|
|
93
|
+
_validateRoutes(params, v4Route);
|
|
94
|
+
|
|
95
|
+
_validateAndTransferInputCurrency(params);
|
|
96
|
+
|
|
97
|
+
// STEP 3: Execute V3 swap (inputCurrency -> backing currency)
|
|
98
|
+
(uint256 currencyAmount, address currencyReceived) = _executeV3Swap(params);
|
|
99
|
+
|
|
100
|
+
// STEP 4: Execute V4 swaps if needed, then buy coin
|
|
101
|
+
uint256 coinAmount = _executeV4Swap(v4Route, currencyAmount, currencyReceived, params.buyRecipient);
|
|
102
|
+
|
|
103
|
+
// Validate minimum amount of coins received from final swap
|
|
104
|
+
require(coinAmount >= params.minAmountOut, InsufficientOutputAmount());
|
|
105
|
+
|
|
106
|
+
emit BuyInitialSupply({
|
|
107
|
+
recipient: params.buyRecipient,
|
|
108
|
+
coin: address(coin),
|
|
109
|
+
v3Route: params.v3Route,
|
|
110
|
+
v4Route: v4Route,
|
|
111
|
+
inputCurrency: params.inputCurrency,
|
|
112
|
+
inputAmount: params.inputAmount,
|
|
113
|
+
v4SwapInput: currencyAmount,
|
|
114
|
+
coinsPurchased: coinAmount
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// STEP 5: Return results
|
|
118
|
+
return abi.encode(currencyAmount, coinAmount);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============ VALIDATION ============
|
|
122
|
+
|
|
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
|
+
function _buildV4RouteToCoin(ICoin coin, PoolKey[] memory v4Route) internal view returns (PoolKey[] memory fullRoute) {
|
|
159
|
+
fullRoute = new PoolKey[](v4Route.length + 1);
|
|
160
|
+
|
|
161
|
+
for (uint256 i = 0; i < v4Route.length; i++) {
|
|
162
|
+
fullRoute[i] = v4Route[i];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fullRoute[v4Route.length] = coin.getPoolKey();
|
|
166
|
+
}
|
|
167
|
+
|
|
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
|
+
}
|
|
193
|
+
|
|
194
|
+
function _executeV4Swap(PoolKey[] memory v4Route, uint256 amountIn, address currencyIn, address buyRecipient) internal returns (uint256 amountCoin) {
|
|
195
|
+
Currency startingCurrency = Currency.wrap(currencyIn);
|
|
196
|
+
bytes memory data = abi.encode(v4Route, amountIn, startingCurrency, buyRecipient);
|
|
197
|
+
bytes memory result = poolManager.unlock(data);
|
|
198
|
+
amountCoin = abi.decode(result, (uint256));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/// @notice Callback for V4 swaps through route or coin purchase
|
|
202
|
+
function unlockCallback(bytes calldata data) external returns (bytes memory) {
|
|
203
|
+
require(msg.sender == address(poolManager), OnlyPoolManager());
|
|
204
|
+
|
|
205
|
+
(PoolKey[] memory v4Route, uint256 amountIn, Currency startingCurrency, address buyRecipient) = abi.decode(
|
|
206
|
+
data,
|
|
207
|
+
(PoolKey[], uint256, Currency, address)
|
|
208
|
+
);
|
|
209
|
+
|
|
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
|
+
}
|
|
236
|
+
|
|
237
|
+
// Settle all currency deltas and get final amount
|
|
238
|
+
_settleDeltas(startingCurrency, lastReceivedCurrency, buyRecipient, amountIn, outputAmount);
|
|
239
|
+
|
|
240
|
+
return abi.encode(lastReceivedAmount);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// @notice Helper to decode V4 route data (external for try/catch)
|
|
244
|
+
function decodeV4RouteData(bytes calldata data) external pure returns (PoolKey[] memory v4Route, uint256 startAmount) {
|
|
245
|
+
return abi.decode(data, (PoolKey[], uint256));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function encodeBuySupplyWithV4SwapHookData(InitialSupplyParams memory params) external pure returns (bytes memory) {
|
|
249
|
+
return abi.encode(params);
|
|
250
|
+
}
|
|
251
|
+
|
|
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
|
+
// ============ UTILITIES ============
|
|
269
|
+
|
|
270
|
+
function _getCoinBackingCurrency(ICoin coin) internal view returns (Currency) {
|
|
271
|
+
PoolKey memory poolKey = coin.getPoolKey();
|
|
272
|
+
|
|
273
|
+
if (Currency.unwrap(poolKey.currency0) == address(coin)) {
|
|
274
|
+
return poolKey.currency1;
|
|
275
|
+
}
|
|
276
|
+
return poolKey.currency0;
|
|
277
|
+
}
|
|
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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -44,6 +44,9 @@ interface IZoraV4CoinHook is IUpgradeableV4Hook {
|
|
|
44
44
|
/// @notice Upgrade gate cannot be the zero address.
|
|
45
45
|
error UpgradeGateCannotBeZeroAddress();
|
|
46
46
|
|
|
47
|
+
/// @notice Trusted message sender lookup cannot be the zero address.
|
|
48
|
+
error TrustedMsgSenderLookupCannotBeZeroAddress();
|
|
49
|
+
|
|
47
50
|
/// @notice Thrown when a pool is not initialized for the hook.
|
|
48
51
|
/// @param key The pool key struct to identify the pool.
|
|
49
52
|
error NoCoinForHook(PoolKey key);
|
|
@@ -14,6 +14,7 @@ import {HookMiner} from "@uniswap/v4-periphery/src/utils/HookMiner.sol";
|
|
|
14
14
|
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
15
15
|
import {Create2} from "@openzeppelin/contracts/utils/Create2.sol";
|
|
16
16
|
import {IHooks} from "@uniswap/v4-core/src/interfaces/IHooks.sol";
|
|
17
|
+
import {ITrustedMsgSenderProviderLookup} from "../interfaces/ITrustedMsgSenderProviderLookup.sol";
|
|
17
18
|
|
|
18
19
|
Vm constant vm = Vm(address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))));
|
|
19
20
|
|
|
@@ -86,10 +87,10 @@ library HooksDeployment {
|
|
|
86
87
|
address deployer,
|
|
87
88
|
address poolManager,
|
|
88
89
|
address coinVersionLookup,
|
|
89
|
-
|
|
90
|
+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
|
|
90
91
|
address upgradeGate
|
|
91
92
|
) internal returns (address hookAddress, bytes32 salt) {
|
|
92
|
-
bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup,
|
|
93
|
+
bytes memory hookCreationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
|
|
93
94
|
(salt, ) = mineAndCacheSalt(deployer, hookCreationCode);
|
|
94
95
|
hookAddress = HookMinerWithCreationCodeArgs.deterministicHookAddress(deployer, salt, hookCreationCode);
|
|
95
96
|
}
|
|
@@ -131,19 +132,19 @@ library HooksDeployment {
|
|
|
131
132
|
function hookConstructorArgs(
|
|
132
133
|
address poolManager,
|
|
133
134
|
address coinVersionLookup,
|
|
134
|
-
|
|
135
|
+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
|
|
135
136
|
address upgradeGate
|
|
136
137
|
) internal pure returns (bytes memory) {
|
|
137
|
-
return abi.encode(poolManager, coinVersionLookup,
|
|
138
|
+
return abi.encode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
|
|
138
139
|
}
|
|
139
140
|
|
|
140
141
|
function makeHookCreationCode(
|
|
141
142
|
address poolManager,
|
|
142
143
|
address coinVersionLookup,
|
|
143
|
-
|
|
144
|
+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
|
|
144
145
|
address upgradeGate
|
|
145
146
|
) internal pure returns (bytes memory) {
|
|
146
|
-
return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup,
|
|
147
|
+
return abi.encodePacked(type(ZoraV4CoinHook).creationCode, hookConstructorArgs(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate));
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
/// @notice Deploys or returns existing ContentCoinHook using deterministic deployment. Ensures that if a hooks is already
|
|
@@ -151,11 +152,11 @@ library HooksDeployment {
|
|
|
151
152
|
function deployZoraV4CoinHook(
|
|
152
153
|
address poolManager,
|
|
153
154
|
address coinVersionLookup,
|
|
154
|
-
|
|
155
|
+
ITrustedMsgSenderProviderLookup trustedMsgSenderLookup,
|
|
155
156
|
address upgradeGate,
|
|
156
157
|
bytes32 salt
|
|
157
158
|
) internal returns (IHooks hook) {
|
|
158
|
-
bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup,
|
|
159
|
+
bytes memory creationCode = makeHookCreationCode(poolManager, coinVersionLookup, trustedMsgSenderLookup, upgradeGate);
|
|
159
160
|
return deployHookWithSalt(creationCode, salt);
|
|
160
161
|
}
|
|
161
162
|
|