@zoralabs/coins 2.4.1 → 2.6.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/.abi-stability +923 -0
- package/.turbo/turbo-build$colon$js.log +143 -129
- package/CHANGELOG.md +38 -16
- package/abis/BaseCoin.json +23 -0
- package/abis/ContentCoin.json +23 -0
- package/abis/CreatorCoin.json +18 -0
- package/abis/ICoin.json +5 -0
- package/abis/ICoinV3.json +5 -0
- package/abis/IHasCreationInfo.json +20 -0
- package/abis/ITrendCoin.json +130 -0
- package/abis/ITrendCoinErrors.json +23 -0
- package/abis/IUniversalRouter.json +61 -0
- package/abis/IZoraFactory.json +227 -0
- package/abis/TrendCoin.json +2043 -0
- package/abis/ZoraFactoryImpl.json +232 -0
- package/dist/index.cjs +962 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +960 -117
- package/dist/index.js.map +1 -1
- package/dist/wagmiGenerated.d.ts +1404 -131
- package/dist/wagmiGenerated.d.ts.map +1 -1
- package/package/wagmiGenerated.ts +970 -119
- package/package.json +4 -2
- package/src/BaseCoin.sol +44 -14
- 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 +73 -8
- package/src/interfaces/ICoin.sol +5 -1
- package/src/interfaces/ICreatorCoin.sol +0 -3
- package/src/interfaces/IHasCreationInfo.sol +12 -0
- package/src/interfaces/IPoolManager.sol +13 -0
- package/src/interfaces/ITrendCoin.sol +26 -0
- package/src/interfaces/ITrendCoinErrors.sol +18 -0
- package/src/interfaces/IZoraFactory.sol +60 -1
- package/src/libs/CoinConstants.sol +25 -1
- package/src/libs/CoinRewardsV4.sol +67 -19
- package/src/libs/CoinSetup.sol +7 -1
- package/src/libs/TickerUtils.sol +84 -0
- package/src/libs/UniV4SwapToCurrency.sol +2 -1
- package/src/libs/V3ToV4SwapLib.sol +7 -3
- package/src/version/ContractVersionBase.sol +1 -1
- package/test/CoinUniV4.t.sol +4 -0
- package/test/ContentCoinRewards.t.sol +1 -0
- package/test/CreatorCoin.t.sol +2 -1
- package/test/CreatorCoinRewards.t.sol +1 -0
- package/test/Factory.t.sol +31 -5
- package/test/LaunchFee.t.sol +284 -0
- package/test/LiquidityMigration.t.sol +0 -2
- package/test/TrendCoin.t.sol +1077 -0
- package/test/Upgrades.t.sol +16 -3
- package/test/utils/FeeEstimatorHook.sol +33 -8
- package/test/utils/V4TestSetup.sol +36 -4
- package/wagmi.config.ts +2 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
/// @title IHasCreationInfo
|
|
5
|
+
/// @notice Interface for coins that support launch fee functionality
|
|
6
|
+
/// @dev Legacy coins that don't implement this interface will use the normal LP fee
|
|
7
|
+
interface IHasCreationInfo {
|
|
8
|
+
/// @notice Returns creation info for the coin used by the launch fee calculation
|
|
9
|
+
/// @return creationTimestamp The block.timestamp when the coin was initialized
|
|
10
|
+
/// @return isDeploying True if the coin is being deployed (transient), false otherwise
|
|
11
|
+
function creationInfo() external view returns (uint256 creationTimestamp, bool isDeploying);
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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.28;
|
|
9
|
+
|
|
10
|
+
// these needed to be imported so that their abis can be included in the generated package output.
|
|
11
|
+
|
|
12
|
+
import {IPoolManager} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
|
|
13
|
+
import {IUniversalRouter} from "@uniswap/universal-router/contracts/interfaces/IUniversalRouter.sol";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
import {PoolKey} from "@uniswap/v4-core/src/types/PoolKey.sol";
|
|
5
|
+
import {PoolConfiguration} from "../types/PoolConfiguration.sol";
|
|
6
|
+
import {ITrendCoinErrors} from "./ITrendCoinErrors.sol";
|
|
7
|
+
|
|
8
|
+
interface ITrendCoin is ITrendCoinErrors {
|
|
9
|
+
/// @notice Thrown when an operation is attempted by an entity other than the metadata manager
|
|
10
|
+
error OnlyMetadataManager();
|
|
11
|
+
|
|
12
|
+
/// @notice Initializes a trend coin with simplified parameters
|
|
13
|
+
/// @dev Ticker validation, URI generation, and name derivation happen internally
|
|
14
|
+
/// @param owners_ Array of owner addresses for the coin
|
|
15
|
+
/// @param symbol_ The ticker symbol (also used as name)
|
|
16
|
+
/// @param poolKey_ The Uniswap V4 pool key
|
|
17
|
+
/// @param sqrtPriceX96 The initial sqrt price for the pool
|
|
18
|
+
/// @param poolConfiguration_ The pool configuration settings
|
|
19
|
+
function initializeTrendCoin(
|
|
20
|
+
address[] memory owners_,
|
|
21
|
+
string memory symbol_,
|
|
22
|
+
PoolKey memory poolKey_,
|
|
23
|
+
uint160 sqrtPriceX96,
|
|
24
|
+
PoolConfiguration memory poolConfiguration_
|
|
25
|
+
) external;
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
pragma solidity ^0.8.23;
|
|
3
|
+
|
|
4
|
+
/// @title ITrendCoinErrors
|
|
5
|
+
/// @notice Shared error interface for TrendCoin-related errors
|
|
6
|
+
/// @dev Used by both TrendCoin and ZoraFactoryImpl for consistent error handling
|
|
7
|
+
interface ITrendCoinErrors {
|
|
8
|
+
/// @notice Thrown when ticker symbol contains invalid characters
|
|
9
|
+
/// @dev Allowed characters: space (0x20), dash (0x2D), 0-9, A-Z, a-z
|
|
10
|
+
error InvalidTickerCharacters();
|
|
11
|
+
|
|
12
|
+
/// @notice Thrown when attempting to deploy a trend coin with a ticker that already exists
|
|
13
|
+
/// @param symbol The ticker symbol that was already used
|
|
14
|
+
error TickerAlreadyUsed(string symbol);
|
|
15
|
+
|
|
16
|
+
/// @notice Thrown when attempting to use the legacy initialize function for a trend coin
|
|
17
|
+
error UseSpecificTrendCoinInitialize();
|
|
18
|
+
}
|
|
@@ -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,29 @@ 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 Flag to enable dynamic fees for the pool
|
|
57
|
+
/// @dev When set in pool key fee, enables hook to override fee per-swap
|
|
58
|
+
uint24 internal constant DYNAMIC_FEE_FLAG = 0x800000;
|
|
59
|
+
|
|
60
|
+
/// @notice Flag to override the fee in beforeSwap return value
|
|
61
|
+
/// @dev Combined with fee value to signal V4 to use the returned fee
|
|
62
|
+
uint24 internal constant OVERRIDE_FEE_FLAG = 0x400000;
|
|
63
|
+
|
|
64
|
+
/// @notice Starting fee for launch fee (99%)
|
|
65
|
+
/// @dev 990,000 pips = 99% (1,000,000 pips = 100%)
|
|
66
|
+
uint24 internal constant LAUNCH_FEE_START = 990_000;
|
|
67
|
+
|
|
68
|
+
/// @notice Duration over which launch fee decays from start to end fee
|
|
69
|
+
/// @dev 10 seconds
|
|
70
|
+
uint256 internal constant LAUNCH_FEE_DURATION = 10 seconds;
|
|
71
|
+
|
|
56
72
|
/// @notice The spacing for 1% pools
|
|
57
73
|
/// @dev 200 ticks
|
|
58
74
|
int24 internal constant TICK_SPACING = 200;
|
|
@@ -77,4 +93,12 @@ library CoinConstants {
|
|
|
77
93
|
int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
|
|
78
94
|
uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
|
|
79
95
|
uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
|
|
96
|
+
|
|
97
|
+
/// @notice The default pool configuration for TrendCoins
|
|
98
|
+
/// @dev Pre-encoded bytes for version 4 with 3 curves and ZORA currency
|
|
99
|
+
/// Curve 1: ticks [-89200, -75200], 11 positions, 5% max supply
|
|
100
|
+
/// Curve 2: ticks [-77200, -68200], 11 positions, 12.5% max supply
|
|
101
|
+
/// Curve 3: ticks [-71200, -68200], 11 positions, 20% max supply
|
|
102
|
+
bytes internal constant TREND_COIN_DEFAULT_POOL_CONFIG =
|
|
103
|
+
hex"00000000000000000000000000000000000000000000000000000000000000040000000000000000000000001111111111166b7fe7bd91427724b487980afc6900000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c000000000000000000000000000000000000000000000000000000000000002400000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffea390fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed270fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffee9e00000000000000000000000000000000000000000000000000000000000000003fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffeda40fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef598fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffef5980000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000b000000000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000b1a2bc2ec5000000000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000000000000000000000000000002c68af0bb140000";
|
|
80
104
|
}
|
|
@@ -41,27 +41,61 @@ library CoinRewardsV4 {
|
|
|
41
41
|
return hookData.length >= 20 ? 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
|
|
|
@@ -188,7 +222,8 @@ library CoinRewardsV4 {
|
|
|
188
222
|
platformReferrer,
|
|
189
223
|
protocolRewardRecipient,
|
|
190
224
|
doppler,
|
|
191
|
-
tradeReferrer
|
|
225
|
+
tradeReferrer,
|
|
226
|
+
coinType
|
|
192
227
|
);
|
|
193
228
|
|
|
194
229
|
IZoraV4CoinHook.MarketRewardsV4 memory marketRewards = IZoraV4CoinHook.MarketRewardsV4({
|
|
@@ -242,9 +277,10 @@ library CoinRewardsV4 {
|
|
|
242
277
|
address platformReferrer,
|
|
243
278
|
address protocolRewardRecipient,
|
|
244
279
|
address doppler,
|
|
245
|
-
address tradeReferral
|
|
280
|
+
address tradeReferral,
|
|
281
|
+
IHasCoinType.CoinType coinType
|
|
246
282
|
) internal returns (MarketRewards memory rewards) {
|
|
247
|
-
rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0));
|
|
283
|
+
rewards = _computeMarketRewards(fee, tradeReferral != address(0), platformReferrer != address(0), coinType);
|
|
248
284
|
|
|
249
285
|
// Notes on ETH transfer fallback behavior:
|
|
250
286
|
// - 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 +312,24 @@ library CoinRewardsV4 {
|
|
|
276
312
|
}
|
|
277
313
|
}
|
|
278
314
|
|
|
279
|
-
function _computeMarketRewards(
|
|
315
|
+
function _computeMarketRewards(
|
|
316
|
+
uint128 fee,
|
|
317
|
+
bool hasTradeReferral,
|
|
318
|
+
bool hasCreateReferral,
|
|
319
|
+
IHasCoinType.CoinType coinType
|
|
320
|
+
) internal pure returns (MarketRewards memory rewards) {
|
|
280
321
|
if (fee == 0) {
|
|
281
322
|
return rewards;
|
|
282
323
|
}
|
|
283
324
|
|
|
284
325
|
uint256 totalAmount = uint256(fee);
|
|
326
|
+
|
|
327
|
+
// TrendCoins: 100% of market rewards go to protocol (80% of total fees, with 20% already going to LPs)
|
|
328
|
+
if (coinType == IHasCoinType.CoinType.Trend) {
|
|
329
|
+
rewards.protocolAmount = totalAmount;
|
|
330
|
+
return rewards;
|
|
331
|
+
}
|
|
332
|
+
|
|
285
333
|
rewards.platformReferrerAmount = hasCreateReferral ? calculateReward(totalAmount, CoinConstants.CREATE_REFERRAL_REWARD_BPS) : 0;
|
|
286
334
|
rewards.tradeReferrerAmount = hasTradeReferral ? calculateReward(totalAmount, CoinConstants.TRADE_REFERRAL_REWARD_BPS) : 0;
|
|
287
335
|
rewards.creatorAmount = calculateReward(totalAmount, CoinConstants.CREATOR_REWARD_BPS);
|
package/src/libs/CoinSetup.sol
CHANGED
|
@@ -36,7 +36,13 @@ library CoinSetup {
|
|
|
36
36
|
Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
|
|
37
37
|
Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
|
|
38
38
|
|
|
39
|
-
poolKey = PoolKey({
|
|
39
|
+
poolKey = PoolKey({
|
|
40
|
+
currency0: currency0,
|
|
41
|
+
currency1: currency1,
|
|
42
|
+
fee: CoinConstants.DYNAMIC_FEE_FLAG,
|
|
43
|
+
tickSpacing: CoinConstants.TICK_SPACING,
|
|
44
|
+
hooks: hooks
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function setupPoolWithVersion(
|
|
@@ -0,0 +1,84 @@
|
|
|
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 TickerUtils
|
|
11
|
+
/// @notice Library for ASCII case-folding ticker symbols for uniqueness checking
|
|
12
|
+
library TickerUtils {
|
|
13
|
+
/// @notice Converts a ticker string to lowercase (ASCII case-folding)
|
|
14
|
+
/// @param ticker The ticker symbol to fold
|
|
15
|
+
/// @return The lowercase ticker bytes
|
|
16
|
+
function foldTicker(string memory ticker) internal pure returns (bytes memory) {
|
|
17
|
+
bytes memory tickerBytes = bytes(ticker);
|
|
18
|
+
bytes memory result = new bytes(tickerBytes.length);
|
|
19
|
+
|
|
20
|
+
for (uint256 i = 0; i < tickerBytes.length; i++) {
|
|
21
|
+
bytes1 char = tickerBytes[i];
|
|
22
|
+
// If uppercase A-Z (0x41-0x5A), convert to lowercase (add 0x20)
|
|
23
|
+
if (char >= 0x41 && char <= 0x5A) {
|
|
24
|
+
result[i] = bytes1(uint8(char) + 32);
|
|
25
|
+
} else {
|
|
26
|
+
result[i] = char;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// @notice Computes a hash of the case-folded ticker for uniqueness checking
|
|
34
|
+
/// @param ticker The ticker symbol to hash
|
|
35
|
+
/// @return The keccak256 hash of the lowercase ticker
|
|
36
|
+
function tickerHash(string memory ticker) internal pure returns (bytes32) {
|
|
37
|
+
return keccak256(foldTicker(ticker));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// @notice Converts spaces in ticker to '+' for URI encoding
|
|
41
|
+
/// @param ticker The ticker symbol to encode
|
|
42
|
+
/// @return The ticker with spaces replaced by '+'
|
|
43
|
+
function tickerToUri(string memory ticker) internal pure returns (string memory) {
|
|
44
|
+
bytes memory tickerBytes = bytes(ticker);
|
|
45
|
+
bytes memory result = new bytes(tickerBytes.length);
|
|
46
|
+
|
|
47
|
+
for (uint256 i = 0; i < tickerBytes.length; i++) {
|
|
48
|
+
bytes1 char = tickerBytes[i];
|
|
49
|
+
// Replace space (0x20) with '+' (0x2B)
|
|
50
|
+
if (char == 0x20) {
|
|
51
|
+
result[i] = 0x2B;
|
|
52
|
+
} else {
|
|
53
|
+
result[i] = char;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return string(result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// @notice Validates that a ticker symbol contains only allowed characters
|
|
61
|
+
/// @dev Allowed characters: space (0x20), 0-9 (0x30-0x39), A-Z (0x41-0x5A), a-z (0x61-0x7A), dash (0x2D)
|
|
62
|
+
/// @param ticker The ticker symbol to validate
|
|
63
|
+
/// @return true if all characters are valid
|
|
64
|
+
function validateTickerCharacters(string memory ticker) internal pure returns (bool) {
|
|
65
|
+
bytes memory tickerBytes = bytes(ticker);
|
|
66
|
+
// Empty string is not allowed
|
|
67
|
+
if (tickerBytes.length == 0) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
for (uint256 i = 0; i < tickerBytes.length; i++) {
|
|
71
|
+
bytes1 char = tickerBytes[i];
|
|
72
|
+
bool isValid = char == 0x20 || // space
|
|
73
|
+
char == 0x2D || // dash (-)
|
|
74
|
+
(char >= 0x30 && char <= 0x39) || // 0-9
|
|
75
|
+
(char >= 0x41 && char <= 0x5A) || // A-Z
|
|
76
|
+
(char >= 0x61 && char <= 0x7A); // a-z
|
|
77
|
+
|
|
78
|
+
if (!isValid) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -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
|
|
|
@@ -56,6 +56,7 @@ library V3ToV4SwapLib {
|
|
|
56
56
|
struct V4SwapResult {
|
|
57
57
|
uint128 outputAmount; // Final output amount
|
|
58
58
|
Currency outputCurrency; // Final output currency
|
|
59
|
+
BalanceDelta targetPoolDelta; // Delta from final (target) pool swap
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
// ============ VALIDATION ============
|
|
@@ -167,6 +168,7 @@ library V3ToV4SwapLib {
|
|
|
167
168
|
function executeV4MultiHopSwap(IPoolManager poolManager, V4SwapParams memory params) internal returns (V4SwapResult memory result) {
|
|
168
169
|
Currency currentCurrency = params.startingCurrency;
|
|
169
170
|
uint128 currentAmount = uint128(params.amountIn);
|
|
171
|
+
BalanceDelta lastDelta;
|
|
170
172
|
|
|
171
173
|
// Execute swaps through the route
|
|
172
174
|
for (uint256 i = 0; i < params.v4Route.length; i++) {
|
|
@@ -175,14 +177,14 @@ library V3ToV4SwapLib {
|
|
|
175
177
|
// Determine swap direction based on current currency
|
|
176
178
|
bool zeroForOne = currentCurrency == poolKey.currency0;
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
lastDelta = poolManager.swap(
|
|
179
181
|
poolKey,
|
|
180
182
|
SwapParams(zeroForOne, -(int128(currentAmount)), zeroForOne ? TickMath.MIN_SQRT_PRICE + 1 : TickMath.MAX_SQRT_PRICE - 1),
|
|
181
183
|
""
|
|
182
184
|
);
|
|
183
185
|
|
|
184
186
|
// Extract output amount from delta
|
|
185
|
-
uint128 outputAmount = zeroForOne ? uint128(
|
|
187
|
+
uint128 outputAmount = zeroForOne ? uint128(lastDelta.amount1()) : uint128(lastDelta.amount0());
|
|
186
188
|
|
|
187
189
|
// Update for next iteration
|
|
188
190
|
currentAmount = outputAmount;
|
|
@@ -191,6 +193,7 @@ library V3ToV4SwapLib {
|
|
|
191
193
|
|
|
192
194
|
result.outputAmount = currentAmount;
|
|
193
195
|
result.outputCurrency = currentCurrency;
|
|
196
|
+
result.targetPoolDelta = lastDelta;
|
|
194
197
|
}
|
|
195
198
|
|
|
196
199
|
// ============ DELTA SETTLEMENT ============
|
|
@@ -212,7 +215,8 @@ library V3ToV4SwapLib {
|
|
|
212
215
|
) internal {
|
|
213
216
|
// Pay the input amount
|
|
214
217
|
if (inputCurrency.isAddressZero()) {
|
|
215
|
-
// For ETH, settle with msg.value
|
|
218
|
+
// For ETH, sync and settle with msg.value
|
|
219
|
+
poolManager.sync(inputCurrency);
|
|
216
220
|
poolManager.settle{value: inputAmount}();
|
|
217
221
|
} else {
|
|
218
222
|
// For ERC20, sync and transfer
|
|
@@ -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.0";
|
|
13
13
|
}
|
|
14
14
|
}
|
package/test/CoinUniV4.t.sol
CHANGED
|
@@ -34,6 +34,7 @@ import {Hooks} from "@uniswap/v4-core/src/libraries/Hooks.sol";
|
|
|
34
34
|
import {ICoin, IHasSwapPath, PathKey} from "../src/interfaces/ICoin.sol";
|
|
35
35
|
import {IDeployedCoinVersionLookup} from "../src/interfaces/IDeployedCoinVersionLookup.sol";
|
|
36
36
|
|
|
37
|
+
/// forge-config: default.isolate = true
|
|
37
38
|
contract CoinUniV4Test is BaseTest {
|
|
38
39
|
MockERC20 internal mockERC20A;
|
|
39
40
|
MockERC20 internal mockERC20B;
|
|
@@ -113,6 +114,9 @@ contract CoinUniV4Test is BaseTest {
|
|
|
113
114
|
address currency = address(mockERC20A);
|
|
114
115
|
_deployV4Coin(currency);
|
|
115
116
|
|
|
117
|
+
// Skip past launch fee period to test normal LP fees
|
|
118
|
+
vm.warp(block.timestamp + 1 days);
|
|
119
|
+
|
|
116
120
|
uint128 amountIn = uint128(0.00001 ether);
|
|
117
121
|
uint128 minAmountOut = uint128(0);
|
|
118
122
|
|
|
@@ -17,6 +17,7 @@ import {CreatorCoin} from "../src/CreatorCoin.sol";
|
|
|
17
17
|
import {ICoin} from "../src/interfaces/ICoin.sol";
|
|
18
18
|
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
19
19
|
|
|
20
|
+
/// forge-config: default.isolate = true
|
|
20
21
|
contract ContentCoinRewardsTest is BaseTest {
|
|
21
22
|
ContentCoin internal contentCoin;
|
|
22
23
|
CreatorCoin internal backingCreatorCoin;
|
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
|
|
|
@@ -17,6 +17,7 @@ import {CreatorCoin} from "../src/CreatorCoin.sol";
|
|
|
17
17
|
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
|
|
18
18
|
import {CoinConfigurationVersions} from "../src/libs/CoinConfigurationVersions.sol";
|
|
19
19
|
|
|
20
|
+
/// forge-config: default.isolate = true
|
|
20
21
|
contract CreatorCoinRewardsTest is BaseTest {
|
|
21
22
|
CreatorCoin internal creatorCoin;
|
|
22
23
|
|
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), "");
|