@zoralabs/coins 2.3.0 → 2.3.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.
- package/.turbo/turbo-build$colon$js.log +116 -97
- package/CHANGELOG.md +7 -1
- package/README.md +1 -0
- package/abis/AddressConstants.json +7 -0
- package/abis/BaseTest.json +62 -0
- package/abis/BuySupplyWithV4SwapHook.json +429 -0
- package/abis/IUniswapV4Router04.json +484 -0
- package/abis/MockAirlock.json +39 -0
- package/abis/SimpleERC20.json +326 -0
- 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/package/wagmiGenerated.ts +139 -18
- package/package.json +1 -1
- package/script/DeployPostDeploymentHooks.s.sol +1 -3
- package/src/deployment/CoinsDeployerBase.sol +9 -8
- package/src/hooks/deployment/BuySupplyWithV4SwapHook.sol +310 -0
- package/src/utils/AutoSwapper.sol +1 -1
- 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 +1 -3
- package/test/Factory.t.sol +3 -3
- package/test/MultiOwnable.t.sol +4 -4
- package/test/Upgrades.t.sol +26 -17
- 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 +155 -2
- 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 → VmContractHelper239.json} +0 -0
|
@@ -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
|
+
}
|
|
@@ -9,7 +9,7 @@ pragma solidity ^0.8.28;
|
|
|
9
9
|
|
|
10
10
|
import {ISwapRouter} from "@zoralabs/shared-contracts/interfaces/uniswap/ISwapRouter.sol";
|
|
11
11
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
12
|
-
import {Path} from "
|
|
12
|
+
import {Path} from "@zoralabs/shared-contracts/libs/UniswapV3/Path.sol";
|
|
13
13
|
|
|
14
14
|
/// @title AutoSwapper
|
|
15
15
|
/// @notice A contract that allows for swapping of tokens via a uniswap v3 swap router. Only works with Uniswap V3 swaps.
|
|
@@ -9,6 +9,6 @@ import {IVersionedContract} from "@zoralabs/shared-contracts/interfaces/IVersion
|
|
|
9
9
|
contract ContractVersionBase is IVersionedContract {
|
|
10
10
|
/// @notice The version of the contract
|
|
11
11
|
function contractVersion() external pure override returns (string memory) {
|
|
12
|
-
return "2.3.
|
|
12
|
+
return "2.3.1";
|
|
13
13
|
}
|
|
14
14
|
}
|