@zoralabs/coins 2.5.0 → 2.6.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 +136 -130
- package/CHANGELOG.md +28 -17
- package/abis/BaseCoin.json +5 -0
- package/abis/ContentCoin.json +5 -0
- package/abis/ICoin.json +5 -0
- package/abis/ICoinV3.json +5 -0
- package/abis/ITrendCoin.json +140 -0
- package/abis/ITrendCoinErrors.json +33 -0
- package/abis/IUniversalRouter.json +61 -0
- package/abis/IZoraFactory.json +237 -0
- package/abis/TrendCoin.json +2053 -0
- package/abis/ZoraFactoryImpl.json +242 -0
- package/dist/index.cjs +955 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +953 -138
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +1388 -149
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/foundry.toml +1 -0
- package/package/wagmiGenerated.ts +962 -139
- package/package.json +2 -2
- package/src/BaseCoin.sol +12 -12
- package/src/ContentCoin.sol +20 -1
- package/src/CreatorCoin.sol +3 -0
- package/src/TrendCoin.sol +117 -0
- package/src/ZoraFactoryImpl.sol +142 -1
- package/src/hooks/ZoraV4CoinHook.sol +17 -7
- package/src/interfaces/ICoin.sol +5 -1
- package/src/interfaces/ICreatorCoin.sol +0 -3
- package/src/interfaces/IPoolManager.sol +13 -0
- package/src/interfaces/ITrendCoin.sol +26 -0
- package/src/interfaces/ITrendCoinErrors.sol +24 -0
- package/src/interfaces/IZoraFactory.sol +60 -1
- package/src/libs/CoinConstants.sol +13 -1
- package/src/libs/CoinRewardsV4.sol +82 -21
- package/src/libs/TickerUtils.sol +66 -0
- package/src/libs/UniV4SwapToCurrency.sol +2 -1
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinRewardsV4.t.sol +48 -0
- package/test/CreatorCoin.t.sol +2 -1
- package/test/Factory.t.sol +31 -5
- package/test/LaunchFee.t.sol +0 -2
- package/test/LiquidityMigration.t.sol +0 -2
- package/test/TrendCoin.t.sol +1128 -0
- package/test/Upgrades.t.sol +16 -3
- package/test/utils/FeeEstimatorHook.sol +36 -10
- package/test/utils/V4TestSetup.sol +36 -4
- package/wagmi.config.ts +2 -0
|
@@ -4,8 +4,9 @@ pragma solidity ^0.8.23;
|
|
|
4
4
|
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
5
5
|
import {PoolKeyStruct} from "./ICoin.sol";
|
|
6
6
|
import {IDeployedCoinVersionLookup} from "./IDeployedCoinVersionLookup.sol";
|
|
7
|
+
import {ITrendCoinErrors} from "./ITrendCoinErrors.sol";
|
|
7
8
|
|
|
8
|
-
interface IZoraFactory is IDeployedCoinVersionLookup {
|
|
9
|
+
interface IZoraFactory is IDeployedCoinVersionLookup, ITrendCoinErrors {
|
|
9
10
|
/// @notice Emitted when a coin is created
|
|
10
11
|
/// @param caller The msg.sender address
|
|
11
12
|
/// @param payoutRecipient The address of the creator payout recipient
|
|
@@ -80,6 +81,16 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
|
|
|
80
81
|
string version
|
|
81
82
|
);
|
|
82
83
|
|
|
84
|
+
/// @notice Emitted when a trend coin is created
|
|
85
|
+
/// @param caller The msg.sender address
|
|
86
|
+
/// @param symbol The symbol/ticker of the coin
|
|
87
|
+
/// @param coin The address of the coin
|
|
88
|
+
/// @param poolKey The uniswap v4 pool key
|
|
89
|
+
/// @param poolKeyHash The hash of the pool key
|
|
90
|
+
/// @param poolConfig The encoded pool configuration (curve config)
|
|
91
|
+
/// @param version The coin contract version
|
|
92
|
+
event TrendCoinCreated(address indexed caller, string symbol, address coin, PoolKey poolKey, bytes32 poolKeyHash, bytes poolConfig, string version);
|
|
93
|
+
|
|
83
94
|
/// @notice Thrown when ETH is sent with a transaction but the currency is not WETH
|
|
84
95
|
error EthTransferInvalid();
|
|
85
96
|
|
|
@@ -97,6 +108,13 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
|
|
|
97
108
|
/// @notice Thrwon when an invalid config version is provided
|
|
98
109
|
error InvalidConfig();
|
|
99
110
|
|
|
111
|
+
/// @notice Thrown when trying to deploy a trend coin before the pool config has been set
|
|
112
|
+
error TrendCoinPoolConfigNotSet();
|
|
113
|
+
|
|
114
|
+
/// @notice Emitted when the trend coin pool config is updated
|
|
115
|
+
/// @param poolConfig The new pool configuration
|
|
116
|
+
event TrendCoinPoolConfigUpdated(bytes poolConfig);
|
|
117
|
+
|
|
100
118
|
/// @dev Deprecated: use `deployCreatorCoin` instead that has a salt and post-deploy hook specified
|
|
101
119
|
function deployCreatorCoin(
|
|
102
120
|
address payoutRecipient,
|
|
@@ -219,4 +237,45 @@ interface IZoraFactory is IDeployedCoinVersionLookup {
|
|
|
219
237
|
|
|
220
238
|
/// @notice The address of the Zora hook registry
|
|
221
239
|
function zoraHookRegistry() external view returns (address);
|
|
240
|
+
|
|
241
|
+
/// @notice Creates a new trend coin with an optional hook that runs after the coin is deployed.
|
|
242
|
+
/// Enables buying initial supply by supporting ETH transfers to the post-deploy hook.
|
|
243
|
+
/// @dev TrendCoins have no payout recipient or platform referrer, and 100% of supply goes to the liquidity pool
|
|
244
|
+
/// @param symbol The ticker symbol for the trend coin (must be unique, case-insensitive)
|
|
245
|
+
/// @param postDeployHook The address of the hook to run after the coin is deployed
|
|
246
|
+
/// @param postDeployHookData The data to pass to the hook
|
|
247
|
+
/// @return coin The address of the deployed trend coin
|
|
248
|
+
/// @return postDeployHookDataOut The data returned by the hook
|
|
249
|
+
function deployTrendCoin(
|
|
250
|
+
string calldata symbol,
|
|
251
|
+
address postDeployHook,
|
|
252
|
+
bytes calldata postDeployHookData
|
|
253
|
+
) external payable returns (address coin, bytes memory postDeployHookDataOut);
|
|
254
|
+
|
|
255
|
+
/// @notice Predicts the address of a trend coin that will be deployed with the given ticker
|
|
256
|
+
/// @param symbol The ticker symbol for the trend coin
|
|
257
|
+
/// @return The address of the trend coin contract
|
|
258
|
+
function trendCoinAddress(string calldata symbol) external view returns (address);
|
|
259
|
+
|
|
260
|
+
/// @notice The trend coin contract implementation address
|
|
261
|
+
function trendCoinImpl() external view returns (address);
|
|
262
|
+
|
|
263
|
+
/// @notice Sets the pool configuration for trend coins
|
|
264
|
+
/// @param currency The currency address for the pool (e.g., ZORA token)
|
|
265
|
+
/// @param tickLower Array of lower tick bounds for each curve
|
|
266
|
+
/// @param tickUpper Array of upper tick bounds for each curve
|
|
267
|
+
/// @param numDiscoveryPositions Array of number of discovery positions for each curve
|
|
268
|
+
/// @param maxDiscoverySupplyShare Array of max supply share (in WAD) for each curve
|
|
269
|
+
/// @dev Can only be called by the contract owner. Arrays must all be the same length.
|
|
270
|
+
function setTrendCoinPoolConfig(
|
|
271
|
+
address currency,
|
|
272
|
+
int24[] memory tickLower,
|
|
273
|
+
int24[] memory tickUpper,
|
|
274
|
+
uint16[] memory numDiscoveryPositions,
|
|
275
|
+
uint256[] memory maxDiscoverySupplyShare
|
|
276
|
+
) external;
|
|
277
|
+
|
|
278
|
+
/// @notice Returns the current pool configuration for trend coins
|
|
279
|
+
/// @return The encoded pool configuration
|
|
280
|
+
function trendCoinPoolConfig() external view returns (bytes memory);
|
|
222
281
|
}
|
|
@@ -46,13 +46,17 @@ library CoinConstants {
|
|
|
46
46
|
uint256 internal constant CREATOR_VESTING_DURATION = (5 * 365.25 days);
|
|
47
47
|
|
|
48
48
|
/// @notice The backing currency for creator coins
|
|
49
|
-
/// @dev
|
|
49
|
+
/// @dev ZORA currency backing currency address
|
|
50
50
|
address internal constant CREATOR_COIN_CURRENCY = 0x1111111111166b7FE7bd91427724B487980aFc69;
|
|
51
51
|
|
|
52
52
|
/// @notice The LP fee
|
|
53
53
|
/// @dev 10000 basis points = 1%
|
|
54
54
|
uint24 internal constant LP_FEE_V4 = 10_000;
|
|
55
55
|
|
|
56
|
+
/// @notice The LP fee for trend coins (after launch fee period)
|
|
57
|
+
/// @dev 100 pips = 1 basis point = 0.01%
|
|
58
|
+
uint24 internal constant TREND_LP_FEE_V4 = 100;
|
|
59
|
+
|
|
56
60
|
/// @notice Flag to enable dynamic fees for the pool
|
|
57
61
|
/// @dev When set in pool key fee, enables hook to override fee per-swap
|
|
58
62
|
uint24 internal constant DYNAMIC_FEE_FLAG = 0x800000;
|
|
@@ -93,4 +97,12 @@ library CoinConstants {
|
|
|
93
97
|
int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
|
|
94
98
|
uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
|
|
95
99
|
uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
|
|
100
|
+
|
|
101
|
+
/// @notice The default pool configuration for TrendCoins
|
|
102
|
+
/// @dev Pre-encoded bytes for version 4 with 3 curves and ZORA currency
|
|
103
|
+
/// Curve 1: ticks [-89200, -75200], 11 positions, 5% max supply
|
|
104
|
+
/// Curve 2: ticks [-77200, -68200], 11 positions, 12.5% max supply
|
|
105
|
+
/// Curve 3: ticks [-71200, -68200], 11 positions, 20% max supply
|
|
106
|
+
bytes internal constant TREND_COIN_DEFAULT_POOL_CONFIG =
|
|
107
|
+
hex"00000000000000000000000000000000000000000000000000000000000000040000000000000000000000001111111111166b7fe7bd91427724b487980afc6900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea390fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed270fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee9e00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeda40fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef598fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef5980000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000002c68af0bb140000";
|
|
96
108
|
}
|
|
@@ -38,30 +38,64 @@ library CoinRewardsV4 {
|
|
|
38
38
|
using SafeERC20 for IERC20;
|
|
39
39
|
|
|
40
40
|
function getTradeReferral(bytes calldata hookData) internal pure returns (address) {
|
|
41
|
-
return hookData.length >=
|
|
41
|
+
return hookData.length >= 32 ? abi.decode(hookData, (address)) : address(0);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/// @dev
|
|
45
|
-
///
|
|
44
|
+
/// @dev Swaps collected fees through the payout path and distributes all deltas as rewards.
|
|
45
|
+
/// This handles partial swap execution where swaps may hit price limits and leave unsettled deltas.
|
|
46
|
+
/// After executing the swap path, all positive deltas (both intermediate and final) are taken and distributed.
|
|
46
47
|
/// @param poolManager The pool manager instance
|
|
47
48
|
/// @param fees0 The amount of fees collected in currency0
|
|
48
49
|
/// @param fees1 The amount of fees collected in currency1
|
|
49
|
-
/// @param payoutSwapPath The swap path
|
|
50
|
-
/// @
|
|
51
|
-
/// @
|
|
52
|
-
|
|
50
|
+
/// @param payoutSwapPath The swap path for converting fees to the payout currency
|
|
51
|
+
/// @param coin The coin interface for getting reward recipients
|
|
52
|
+
/// @param tradeReferrer The trade referrer address
|
|
53
|
+
/// @param coinType The coin type for proper event emission
|
|
54
|
+
function swapFeesToPayoutAndDistribute(
|
|
53
55
|
IPoolManager poolManager,
|
|
54
56
|
uint128 fees0,
|
|
55
57
|
uint128 fees1,
|
|
56
|
-
IHasSwapPath.PayoutSwapPath memory payoutSwapPath
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
IHasSwapPath.PayoutSwapPath memory payoutSwapPath,
|
|
59
|
+
IHasRewardsRecipients coin,
|
|
60
|
+
address tradeReferrer,
|
|
61
|
+
IHasCoinType.CoinType coinType
|
|
62
|
+
) internal {
|
|
63
|
+
// Execute the swap path - may only partially execute if hitting price limits
|
|
64
|
+
UniV4SwapToCurrency.swapToPath(poolManager, fees0, fees1, payoutSwapPath.currencyIn, payoutSwapPath.path);
|
|
65
|
+
|
|
66
|
+
// After swap path execution, iterate through all currencies and take/distribute any positive deltas
|
|
67
|
+
// This handles cases where swaps only partially execute, leaving unsettled amounts
|
|
68
|
+
|
|
69
|
+
// Check currencyIn — partial swap remainder lives here
|
|
70
|
+
_takeAndDistributeIfPositiveDelta(poolManager, payoutSwapPath.currencyIn, coin, tradeReferrer, coinType);
|
|
71
|
+
|
|
72
|
+
// Check all intermediate currencies in the path (includes the other pool currency as path[0])
|
|
73
|
+
for (uint256 i = 0; i < payoutSwapPath.path.length; i++) {
|
|
74
|
+
Currency intermediateCurrency = payoutSwapPath.path[i].intermediateCurrency;
|
|
75
|
+
_takeAndDistributeIfPositiveDelta(poolManager, intermediateCurrency, coin, tradeReferrer, coinType);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// @dev Checks if a currency has a positive delta, and if so, takes it and distributes as rewards
|
|
80
|
+
/// @param poolManager The pool manager instance
|
|
81
|
+
/// @param currency The currency to check
|
|
82
|
+
/// @param coin The coin interface for getting reward recipients
|
|
83
|
+
/// @param tradeReferrer The trade referrer address
|
|
84
|
+
/// @param coinType The coin type for proper event emission
|
|
85
|
+
function _takeAndDistributeIfPositiveDelta(
|
|
86
|
+
IPoolManager poolManager,
|
|
87
|
+
Currency currency,
|
|
88
|
+
IHasRewardsRecipients coin,
|
|
89
|
+
address tradeReferrer,
|
|
90
|
+
IHasCoinType.CoinType coinType
|
|
91
|
+
) private {
|
|
92
|
+
int256 delta = TransientStateLibrary.currencyDelta(poolManager, address(this), currency);
|
|
93
|
+
// Only take if there's a positive delta
|
|
94
|
+
// Note: delta might be 0 if we already took from this currency in a previous call
|
|
95
|
+
if (delta > 0) {
|
|
96
|
+
uint128 amount = uint128(uint256(delta));
|
|
97
|
+
poolManager.take(currency, address(this), amount);
|
|
98
|
+
distributeMarketRewards(currency, amount, coin, tradeReferrer, coinType);
|
|
65
99
|
}
|
|
66
100
|
}
|
|
67
101
|
|
|
@@ -101,8 +135,15 @@ library CoinRewardsV4 {
|
|
|
101
135
|
IPoolManager poolManager,
|
|
102
136
|
PoolKey calldata key,
|
|
103
137
|
int128 fees0,
|
|
104
|
-
int128 fees1
|
|
138
|
+
int128 fees1,
|
|
139
|
+
IHasCoinType.CoinType coinType
|
|
105
140
|
) internal returns (uint128 marketRewardsAmount0, uint128 marketRewardsAmount1) {
|
|
141
|
+
if (coinType == IHasCoinType.CoinType.Trend) {
|
|
142
|
+
marketRewardsAmount0 = fees0 > 0 ? uint128(fees0) : 0;
|
|
143
|
+
marketRewardsAmount1 = fees1 > 0 ? uint128(fees1) : 0;
|
|
144
|
+
return (marketRewardsAmount0, marketRewardsAmount1);
|
|
145
|
+
}
|
|
146
|
+
|
|
106
147
|
if (fees0 > 0) {
|
|
107
148
|
uint128 lpRewardAmount0 = computeLpReward(uint128(fees0));
|
|
108
149
|
if (lpRewardAmount0 > 0) {
|
|
@@ -188,7 +229,8 @@ library CoinRewardsV4 {
|
|
|
188
229
|
platformReferrer,
|
|
189
230
|
protocolRewardRecipient,
|
|
190
231
|
doppler,
|
|
191
|
-
tradeReferrer
|
|
232
|
+
tradeReferrer,
|
|
233
|
+
coinType
|
|
192
234
|
);
|
|
193
235
|
|
|
194
236
|
IZoraV4CoinHook.MarketRewardsV4 memory marketRewards = IZoraV4CoinHook.MarketRewardsV4({
|
|
@@ -242,9 +284,16 @@ library CoinRewardsV4 {
|
|
|
242
284
|
address platformReferrer,
|
|
243
285
|
address protocolRewardRecipient,
|
|
244
286
|
address doppler,
|
|
245
|
-
address tradeReferral
|
|
287
|
+
address tradeReferral,
|
|
288
|
+
IHasCoinType.CoinType coinType
|
|
246
289
|
) internal returns (MarketRewards memory rewards) {
|
|
247
|
-
|
|
290
|
+
if (coinType == IHasCoinType.CoinType.Trend) {
|
|
291
|
+
rewards.protocolAmount = fee;
|
|
292
|
+
_transferCurrency(currency, fee, protocolRewardRecipient, address(0));
|
|
293
|
+
return rewards;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0), coinType);
|
|
248
297
|
|
|
249
298
|
// Notes on ETH transfer fallback behavior:
|
|
250
299
|
// - If the platform referrer is immutable; if it is set to an address that cannot receive ETH, it can brick swaps on the coin, as they would revert.
|
|
@@ -276,12 +325,24 @@ library CoinRewardsV4 {
|
|
|
276
325
|
}
|
|
277
326
|
}
|
|
278
327
|
|
|
279
|
-
function _computeMarketRewards(
|
|
328
|
+
function _computeMarketRewards(
|
|
329
|
+
uint128 fee,
|
|
330
|
+
bool hasTradeReferral,
|
|
331
|
+
bool hasCreateReferral,
|
|
332
|
+
IHasCoinType.CoinType coinType
|
|
333
|
+
) internal pure returns (MarketRewards memory rewards) {
|
|
280
334
|
if (fee == 0) {
|
|
281
335
|
return rewards;
|
|
282
336
|
}
|
|
283
337
|
|
|
284
338
|
uint256 totalAmount = uint256(fee);
|
|
339
|
+
|
|
340
|
+
// TrendCoins: 100% of market rewards go to protocol (80% of total fees, with 20% already going to LPs)
|
|
341
|
+
if (coinType == IHasCoinType.CoinType.Trend) {
|
|
342
|
+
rewards.protocolAmount = totalAmount;
|
|
343
|
+
return rewards;
|
|
344
|
+
}
|
|
345
|
+
|
|
285
346
|
rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CoinConstants.CREATE_REFERRAL_REWARD_BPS) : 0;
|
|
286
347
|
rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, CoinConstants.TRADE_REFERRAL_REWARD_BPS) : 0;
|
|
287
348
|
rewards.creatorAmount = calculateReward(totalAmount, CoinConstants.CREATOR_REWARD_BPS);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
|
|
2
|
+
// This software is licensed under the Zora Delayed Open Source License.
|
|
3
|
+
// Under this license, you may use, copy, modify, and distribute this software for
|
|
4
|
+
// non-commercial purposes only. Commercial use and competitive products are prohibited
|
|
5
|
+
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
|
|
6
|
+
// at which point this software automatically becomes available under the MIT License.
|
|
7
|
+
// Full license terms available at: https://docs.zora.co/coins/license
|
|
8
|
+
pragma solidity ^0.8.23;
|
|
9
|
+
|
|
10
|
+
import {ITrendCoinErrors} from "../interfaces/ITrendCoinErrors.sol";
|
|
11
|
+
|
|
12
|
+
/// @title TickerUtils
|
|
13
|
+
/// @notice Library for ASCII case-folding ticker symbols for uniqueness checking
|
|
14
|
+
library TickerUtils {
|
|
15
|
+
/// @notice Converts a ticker string to lowercase (ASCII case-folding)
|
|
16
|
+
/// @param ticker The ticker symbol to fold
|
|
17
|
+
/// @return The lowercase ticker bytes
|
|
18
|
+
function lowercaseTicker(string memory ticker) internal pure returns (bytes memory) {
|
|
19
|
+
bytes memory tickerBytes = bytes(ticker);
|
|
20
|
+
bytes memory result = new bytes(tickerBytes.length);
|
|
21
|
+
|
|
22
|
+
for (uint256 i = 0; i < tickerBytes.length; i++) {
|
|
23
|
+
bytes1 char = tickerBytes[i];
|
|
24
|
+
// If uppercase A-Z (0x41-0x5A), convert to lowercase (add 0x20)
|
|
25
|
+
if (char >= 0x41 && char <= 0x5A) {
|
|
26
|
+
result[i] = bytes1(uint8(char) + 32);
|
|
27
|
+
} else {
|
|
28
|
+
result[i] = char;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// @notice Computes a hash of the case-folded ticker for uniqueness checking
|
|
36
|
+
/// @param ticker The ticker symbol to hash
|
|
37
|
+
/// @return The keccak256 hash of the lowercase ticker
|
|
38
|
+
function tickerHash(string memory ticker) internal pure returns (bytes32) {
|
|
39
|
+
return keccak256(lowercaseTicker(ticker));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// @notice Validates that a ticker symbol contains only allowed characters and has valid length
|
|
43
|
+
/// @dev Allowed characters: 0-9 (0x30-0x39), A-Z (0x41-0x5A), a-z (0x61-0x7A). Length must be 2-32.
|
|
44
|
+
/// Reverts if parameters are invalid
|
|
45
|
+
/// @param ticker The ticker symbol to validate
|
|
46
|
+
function requireValidateTickerCharacters(string memory ticker) internal pure {
|
|
47
|
+
bytes memory tickerBytes = bytes(ticker);
|
|
48
|
+
// Ticker is between 2 and 32 chars in length, only ascii A-Z 0-9 a-z
|
|
49
|
+
if (tickerBytes.length < 2) {
|
|
50
|
+
revert ITrendCoinErrors.TickerTooShort();
|
|
51
|
+
}
|
|
52
|
+
if (tickerBytes.length > 32) {
|
|
53
|
+
revert ITrendCoinErrors.TickerTooLong();
|
|
54
|
+
}
|
|
55
|
+
for (uint256 i = 0; i < tickerBytes.length; i++) {
|
|
56
|
+
bytes1 char = tickerBytes[i];
|
|
57
|
+
bool isValid = (char >= 0x30 && char <= 0x39) || // 0-9
|
|
58
|
+
(char >= 0x41 && char <= 0x5A) || // A-Z
|
|
59
|
+
(char >= 0x61 && char <= 0x7A); // a-z
|
|
60
|
+
|
|
61
|
+
if (!isValid) {
|
|
62
|
+
revert ITrendCoinErrors.TickerInvalidCharacters();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -63,7 +63,8 @@ library UniV4SwapToCurrency {
|
|
|
63
63
|
if (inputAmount == 0) {
|
|
64
64
|
outputAmount = initialAmountCurrency;
|
|
65
65
|
} else {
|
|
66
|
-
|
|
66
|
+
int128 swapResult = _swap(poolManager, poolKey, zeroForOne, -int128(inputAmount), bytes(""));
|
|
67
|
+
outputAmount = initialAmountCurrency + uint128(swapResult);
|
|
67
68
|
}
|
|
68
69
|
}
|
|
69
70
|
|
|
@@ -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.
|
|
12
|
+
return "2.6.1";
|
|
13
13
|
}
|
|
14
14
|
}
|
package/test/CoinRewardsV4.t.sol
CHANGED
|
@@ -5,7 +5,20 @@ import "forge-std/Test.sol";
|
|
|
5
5
|
import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
|
|
6
6
|
import {SafeCast} from "@uniswap/v4-core/src/libraries/SafeCast.sol";
|
|
7
7
|
|
|
8
|
+
contract CoinRewardsV4Harness {
|
|
9
|
+
function getTradeReferral(bytes calldata hookData) external pure returns (address) {
|
|
10
|
+
return CoinRewardsV4.getTradeReferral(hookData);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
contract CoinRewardsV4Test is Test {
|
|
15
|
+
CoinRewardsV4Harness internal harness;
|
|
16
|
+
address internal constant TRADE_REFERRER = 0x1234567890123456789012345678901234567890;
|
|
17
|
+
|
|
18
|
+
function setUp() public {
|
|
19
|
+
harness = new CoinRewardsV4Harness();
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
function test_convertDeltaToPositiveUint128_success_with_valid_positive_values() public pure {
|
|
10
23
|
// Test with small positive value
|
|
11
24
|
int256 smallDelta = 1000;
|
|
@@ -30,4 +43,39 @@ contract CoinRewardsV4Test is Test {
|
|
|
30
43
|
}
|
|
31
44
|
CoinRewardsV4.convertDeltaToPositiveUint128(difference);
|
|
32
45
|
}
|
|
46
|
+
|
|
47
|
+
function test_getTradeReferral_returnsZeroAddress_forHookDataUnderTwentyBytes(uint8 length) public view {
|
|
48
|
+
vm.assume(length < 20);
|
|
49
|
+
|
|
50
|
+
assertEq(harness.getTradeReferral(_bytesOfLength(length)), address(0));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function test_getTradeReferral_returnsZeroAddress_forTwentyByteHookData() public view {
|
|
54
|
+
assertEq(harness.getTradeReferral(_bytesOfLength(20)), address(0));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function test_getTradeReferral_returnsZeroAddress_forTwentyOneThroughThirtyOneByteHookData(uint8 length) public view {
|
|
58
|
+
length = uint8(bound(length, 21, 31));
|
|
59
|
+
|
|
60
|
+
assertEq(harness.getTradeReferral(_bytesOfLength(length)), address(0));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function test_getTradeReferral_decodesAbiEncodedAddress() public view {
|
|
64
|
+
assertEq(harness.getTradeReferral(abi.encode(TRADE_REFERRER)), TRADE_REFERRER);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function test_getTradeReferral_decodesAbiEncodedAddressWithTrailingData() public view {
|
|
68
|
+
bytes memory hookData = abi.encodePacked(abi.encode(TRADE_REFERRER), bytes12(uint96(0xabcdef)));
|
|
69
|
+
|
|
70
|
+
assertGt(hookData.length, 32);
|
|
71
|
+
assertEq(harness.getTradeReferral(hookData), TRADE_REFERRER);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _bytesOfLength(uint256 length) private pure returns (bytes memory data) {
|
|
75
|
+
data = new bytes(length);
|
|
76
|
+
|
|
77
|
+
for (uint256 i; i < length; i++) {
|
|
78
|
+
data[i] = bytes1(uint8(i + 1));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
33
81
|
}
|
package/test/CreatorCoin.t.sol
CHANGED
|
@@ -4,6 +4,7 @@ pragma solidity ^0.8.13;
|
|
|
4
4
|
import {BaseTest} from "./utils/BaseTest.sol";
|
|
5
5
|
|
|
6
6
|
import {ICreatorCoin} from "../src/interfaces/ICreatorCoin.sol";
|
|
7
|
+
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
7
8
|
import {ICreatorCoinHook} from "../src/interfaces/ICreatorCoinHook.sol";
|
|
8
9
|
import {CoinConstants} from "../src/libs/CoinConstants.sol";
|
|
9
10
|
import {CoinRewardsV4} from "../src/libs/CoinRewardsV4.sol";
|
|
@@ -92,7 +93,7 @@ contract CreatorCoinTest is BaseTest {
|
|
|
92
93
|
);
|
|
93
94
|
|
|
94
95
|
vm.prank(users.creator);
|
|
95
|
-
vm.expectRevert(
|
|
96
|
+
vm.expectRevert(ICoin.InvalidCurrency.selector);
|
|
96
97
|
factory.deployCreatorCoin(users.creator, _getDefaultOwners(), "https://test.com", "Testcoin", "TEST", poolConfig, address(0), bytes32(0));
|
|
97
98
|
}
|
|
98
99
|
|
package/test/Factory.t.sol
CHANGED
|
@@ -19,7 +19,13 @@ contract FactoryTest is BaseTest {
|
|
|
19
19
|
|
|
20
20
|
function test_factory_constructor_and_proxy_setup() public {
|
|
21
21
|
// Impl constructor test
|
|
22
|
-
ZoraFactoryImpl impl = new ZoraFactoryImpl(
|
|
22
|
+
ZoraFactoryImpl impl = new ZoraFactoryImpl(
|
|
23
|
+
address(coinV4Impl),
|
|
24
|
+
address(creatorCoinImpl),
|
|
25
|
+
address(trendCoinImpl),
|
|
26
|
+
address(hook),
|
|
27
|
+
address(zoraHookRegistry)
|
|
28
|
+
);
|
|
23
29
|
assertEq(ZoraFactoryImpl(address(factory)).owner(), users.factoryOwner);
|
|
24
30
|
assertEq(ZoraFactoryImpl(address(factory)).coinV4Impl(), address(coinV4Impl));
|
|
25
31
|
|
|
@@ -51,7 +57,9 @@ contract FactoryTest is BaseTest {
|
|
|
51
57
|
|
|
52
58
|
assertEq(ZoraFactoryImpl(address(factory)).pendingOwner(), address(0));
|
|
53
59
|
|
|
54
|
-
address newFactoryImpl = address(
|
|
60
|
+
address newFactoryImpl = address(
|
|
61
|
+
new ZoraFactoryImpl(address(coinV4Impl), address(creatorCoinImpl), address(trendCoinImpl), address(hook), address(zoraHookRegistry))
|
|
62
|
+
);
|
|
55
63
|
|
|
56
64
|
// Upgrade to current / new impl
|
|
57
65
|
vm.prank(users.factoryOwner);
|
|
@@ -77,7 +85,13 @@ contract FactoryTest is BaseTest {
|
|
|
77
85
|
}
|
|
78
86
|
|
|
79
87
|
function test_upgrade() public {
|
|
80
|
-
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
88
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
89
|
+
address(coinV4Impl),
|
|
90
|
+
address(creatorCoinImpl),
|
|
91
|
+
address(trendCoinImpl),
|
|
92
|
+
address(hook),
|
|
93
|
+
address(zoraHookRegistry)
|
|
94
|
+
);
|
|
81
95
|
|
|
82
96
|
vm.prank(users.factoryOwner);
|
|
83
97
|
ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
|
|
@@ -98,7 +112,13 @@ contract FactoryTest is BaseTest {
|
|
|
98
112
|
}
|
|
99
113
|
|
|
100
114
|
function test_revert_invalid_owner() public {
|
|
101
|
-
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
115
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
116
|
+
address(coinV4Impl),
|
|
117
|
+
address(creatorCoinImpl),
|
|
118
|
+
address(trendCoinImpl),
|
|
119
|
+
address(hook),
|
|
120
|
+
address(zoraHookRegistry)
|
|
121
|
+
);
|
|
102
122
|
|
|
103
123
|
vm.prank(users.creator);
|
|
104
124
|
vm.expectRevert(abi.encodeWithSelector(OwnableUpgradeable.OwnableUnauthorizedAccount.selector, users.creator));
|
|
@@ -184,7 +204,13 @@ contract FactoryTest is BaseTest {
|
|
|
184
204
|
_deployHooks(address(new MockZoraLimitOrderBook())); // Deploys new content and creator coin hook addresses
|
|
185
205
|
|
|
186
206
|
// Deploy new factory impl with new content and creator coin hook addresses
|
|
187
|
-
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
207
|
+
ZoraFactoryImpl newImpl = new ZoraFactoryImpl(
|
|
208
|
+
address(coinV4Impl),
|
|
209
|
+
address(creatorCoinImpl),
|
|
210
|
+
address(trendCoinImpl),
|
|
211
|
+
address(hook),
|
|
212
|
+
address(zoraHookRegistry)
|
|
213
|
+
);
|
|
188
214
|
|
|
189
215
|
vm.prank(users.factoryOwner);
|
|
190
216
|
ZoraFactoryImpl(address(factory)).upgradeToAndCall(address(newImpl), "");
|
package/test/LaunchFee.t.sol
CHANGED
|
@@ -204,8 +204,6 @@ contract LaunchFeeTest is BaseTest {
|
|
|
204
204
|
// The initial supply purchase during deployment should bypass launch fee
|
|
205
205
|
// This is verified by checking the creator receives coins during deployment
|
|
206
206
|
|
|
207
|
-
uint256 creatorBalanceBefore = 0; // Creator has no coins before deployment
|
|
208
|
-
|
|
209
207
|
_deployCoin();
|
|
210
208
|
|
|
211
209
|
uint256 creatorBalanceAfter = coin.balanceOf(users.creator);
|