Imported from fame-lady-society/www (
src/service/zora/AGENTS.md). Install upstream withnpx skills add fame-lady-society/www --skill zora. Copyright stays with the author.
LICENSE
These code snippets are not exactly open source, and are included here strictly for inference purposes of generating compatible payloads
Reference solidity library
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
// This software is licensed under the Zora Delayed Open Source License.
// Under this license, you may use, copy, modify, and distribute this software for
// non-commercial purposes only. Commercial use and competitive products are prohibited
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
// at which point this software automatically becomes available under the MIT License.
// Full license terms available at: https://docs.zora.co/coins/license
pragma solidity ^0.8.23;
library CoinConstants {
/// @dev Constant used to increase precision during calculations
uint256 internal constant WAD = 1e18;
/// @notice The maximum total supply
/// @dev Set to 1 billion coins with 18 decimals
uint256 internal constant MAX_TOTAL_SUPPLY = 1_000_000_000e18;
/// @notice The total supply for creator coins (same as MAX_TOTAL_SUPPLY)
/// @dev 1 billion coins
uint256 internal constant TOTAL_SUPPLY = 1_000_000_000e18;
/// @notice The number of coins allocated to the liquidity pool for content coins
/// @dev 990 million coins
uint256 internal constant CONTENT_COIN_MARKET_SUPPLY = 990_000_000e18;
/// @notice The number of coins allocated to the liquidity pool for creator coins
/// @dev 500 million coins
uint256 internal constant CREATOR_COIN_MARKET_SUPPLY = 500_000_000e18;
/// @notice The number of coins rewarded to the creator for content coins on launch
/// @dev 10 million coins
uint256 internal constant CONTENT_COIN_INITIAL_CREATOR_SUPPLY = TOTAL_SUPPLY - CONTENT_COIN_MARKET_SUPPLY;
/// @notice Creator coin vesting supply for creator
/// @dev 500 million coins
uint256 internal constant CREATOR_COIN_CREATOR_VESTING_SUPPLY = TOTAL_SUPPLY - CREATOR_COIN_MARKET_SUPPLY;
/// @notice Creator coin vesting duration
/// @dev 5 years with leap years accounted for
uint256 internal constant CREATOR_VESTING_DURATION = (5 * 365.25 days);
/// @notice The backing currency for creator coins
/// @dev ETH backing currency address
address internal constant CREATOR_COIN_CURRENCY = 0x1111111111166b7FE7bd91427724B487980aFc69;
/// @notice The LP fee
/// @dev 10000 basis points = 1%
uint24 internal constant LP_FEE_V4 = 10_000;
/// @notice The spacing for 1% pools
/// @dev 200 ticks
int24 internal constant TICK_SPACING = 200;
// Creator gets 62.5% of market rewards (0.50% of total 1% fee)
// Market rewards = 80% of total fee (0.80% of 1%)
uint256 internal constant CREATOR_REWARD_BPS = 6250;
// Platform referrer gets 25% of market rewards (0.20% of total 1% fee)
uint256 internal constant CREATE_REFERRAL_REWARD_BPS = 2500;
// Trade referrer gets 5% of market rewards (0.04% of total 1% fee)
uint256 internal constant TRADE_REFERRAL_REWARD_BPS = 500;
// Doppler gets 1.25% of market rewards (0.01% of total 1% fee)
uint256 internal constant DOPPLER_REWARD_BPS = 125;
// LPs get 20% of total fee (0.20% of 1%)
uint256 internal constant LP_REWARD_BPS = 2000;
int24 internal constant DEFAULT_DISCOVERY_TICK_LOWER = -777000;
int24 internal constant DEFAULT_DISCOVERY_TICK_UPPER = 222000;
uint16 internal constant DEFAULT_NUM_DISCOVERY_POSITIONS = 10; // will be 11 total with tail position
uint256 internal constant DEFAULT_DISCOVERY_SUPPLY_SHARE = 0.495e18; // half of the 990m total pool supply
}
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
// This software is licensed under the Zora Delayed Open Source License.
// Under this license, you may use, copy, modify, and distribute this software for
// non-commercial purposes only. Commercial use and competitive products are prohibited
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
// at which point this software automatically becomes available under the MIT License.
// Full license terms available at: https://docs.zora.co/coins/license
pragma solidity ^0.8.23;
import {PoolConfigurationV4} from "../interfaces/ICoin.sol";
import {CoinConfigurationVersions} from "./CoinConfigurationVersions.sol";
import {ICoin} from "../interfaces/ICoin.sol";
import {CoinCommon} from "./CoinCommon.sol";
import {CoinConstants} from "./CoinConstants.sol";
import {TickMath} from "../utils/uniswap/TickMath.sol";
import {IPoolManager, PoolKey, Currency, IHooks} from "@uniswap/v4-core/src/interfaces/IPoolManager.sol";
import {Actions} from "@uniswap/v4-periphery/src/libraries/Actions.sol";
import {IPositionManager} from "@uniswap/v4-periphery/src/interfaces/IPositionManager.sol";
import {LpPosition} from "../types/LpPosition.sol";
import {CoinDopplerMultiCurve, PoolConfiguration} from "./CoinDopplerMultiCurve.sol";
library CoinSetup {
function generatePoolConfig(
address coin,
bytes memory poolConfig_
) internal pure returns (uint8 version, address currency, uint160 sqrtPriceX96, bool isCoinToken0, PoolConfiguration memory poolConfiguration) {
// Extract version and currency from pool config
(version, currency) = CoinConfigurationVersions.decodeVersionAndCurrency(poolConfig_);
isCoinToken0 = CoinCommon.sortTokens(coin, currency);
(sqrtPriceX96, poolConfiguration) = setupPoolWithVersion(version, poolConfig_, isCoinToken0);
}
function buildPoolKey(address coin, address currency, bool isCoinToken0, IHooks hooks) internal pure returns (PoolKey memory poolKey) {
Currency currency0 = isCoinToken0 ? Currency.wrap(coin) : Currency.wrap(currency);
Currency currency1 = isCoinToken0 ? Currency.wrap(currency) : Currency.wrap(coin);
poolKey = PoolKey({currency0: currency0, currency1: currency1, fee: CoinConstants.LP_FEE_V4, tickSpacing: CoinConstants.TICK_SPACING, hooks: hooks});
}
function setupPoolWithVersion(
uint8 version,
bytes memory poolConfig_,
bool isCoinToken0
) internal pure returns (uint160 sqrtPriceX96, PoolConfiguration memory poolConfiguration) {
if (version == CoinConfigurationVersions.DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION) {
(sqrtPriceX96, poolConfiguration) = CoinDopplerMultiCurve.setupPool(isCoinToken0, poolConfig_);
} else {
revert ICoin.InvalidPoolVersion();
}
}
}
// SPDX-License-Identifier: ZORA-DELAYED-OSL-v1
// This software is licensed under the Zora Delayed Open Source License.
// Under this license, you may use, copy, modify, and distribute this software for
// non-commercial purposes only. Commercial use and competitive products are prohibited
// until the "Open Date" (3 years from first public distribution or earlier at Zora's discretion),
// at which point this software automatically becomes available under the MIT License.
// Full license terms available at: https://docs.zora.co/coins/license
pragma solidity ^0.8.23;
import {CoinConstants} from "./CoinConstants.sol";
library CoinConfigurationVersions {
uint8 constant LEGACY_POOL_VERSION = 1;
uint8 constant DOPPLER_UNI_V3_POOL_VERSION = 2;
uint8 constant DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION = 4;
function getVersion(bytes memory poolConfig) internal pure returns (uint8 version) {
return (version) = abi.decode(poolConfig, (uint8));
}
function isV3(uint8 version) internal pure returns (bool) {
return version == DOPPLER_UNI_V3_POOL_VERSION || version == LEGACY_POOL_VERSION;
}
function isV4(uint8 version) internal pure returns (bool) {
return version == DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION;
}
function decodeVersionAndCurrency(bytes memory poolConfig) internal pure returns (uint8 version, address currency) {
(version, currency) = abi.decode(poolConfig, (uint8, address));
}
function decodeVanillaUniV4(bytes memory poolConfig) internal pure returns (uint8 version, address currency, int24 tickLower_) {
(version, currency, tickLower_) = abi.decode(poolConfig, (uint8, address, int24));
}
function encodeDopplerMultiCurveUniV4(
address currency,
int24[] memory tickLower_,
int24[] memory tickUpper_,
uint16[] memory numDiscoveryPositions_,
uint256[] memory maxDiscoverySupplyShare_
) internal pure returns (bytes memory) {
return abi.encode(DOPPLER_MULTICURVE_UNI_V4_POOL_VERSION, currency, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_);
}
function decodeDopplerMultiCurveUniV4(
bytes memory poolConfig
)
internal
pure
returns (
uint8 version,
address currency,
int24[] memory tickLower_,
int24[] memory tickUpper_,
uint16[] memory numDiscoveryPositions_,
uint256[] memory maxDiscoverySupplyShare_
)
{
(version, currency, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_) = abi.decode(
poolConfig,
(uint8, address, int24[], int24[], uint16[], uint256[])
);
}
function defaultDopplerMultiCurveUniV4(address currency) internal pure returns (bytes memory) {
int24[] memory tickLower = new int24[](2);
int24[] memory tickUpper = new int24[](2);
uint16[] memory numDiscoveryPositions = new uint16[](2);
uint256[] memory maxDiscoverySupplyShare = new uint256[](2);
// todo: configure defaults
// Curve 1
tickLower[0] = -328_000;
tickUpper[0] = -300_000;
numDiscoveryPositions[0] = 2;
maxDiscoverySupplyShare[0] = 0.1e18;
// Curve 2
tickLower[1] = -200_000;
tickUpper[1] = -100_000;
numDiscoveryPositions[1] = 2;
maxDiscoverySupplyShare[1] = 0.1e18;
return encodeDopplerMultiCurveUniV4(currency, tickLower, tickUpper, numDiscoveryPositions, maxDiscoverySupplyShare);
}
function defaultConfig(address currency) internal pure returns (bytes memory) {
return defaultDopplerMultiCurveUniV4(currency);
}
}
Handle Ops
Zora coins are typically deployed with UserOperation[] which are passed to an
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.12;
/* solhint-disable no-inline-assembly */
import {calldataKeccak} from "../core/Helpers.sol";
/**
* User Operation struct
* @param sender the sender account of this request.
* @param nonce unique value the sender uses to verify it is not a replay.
* @param initCode if set, the account contract will be created by this constructor/
* @param callData the method call to execute on this account.
* @param callGasLimit the gas limit passed to the callData method call.
* @param verificationGasLimit gas used for validateUserOp and validatePaymasterUserOp.
* @param preVerificationGas gas not calculated by the handleOps method, but added to the gas paid. Covers batch overhead.
* @param maxFeePerGas same as EIP-1559 gas parameter.
* @param maxPriorityFeePerGas same as EIP-1559 gas parameter.
* @param paymasterAndData if set, this field holds the paymaster address and paymaster-specific data. the paymaster will pay for the transaction instead of the sender.
* @param signature sender-verified signature over the entire request, the EntryPoint address and the chain ID.
*/
struct UserOperation {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}
/**
* Utility functions helpful when working with UserOperation structs.
*/
library UserOperationLib {
function getSender(UserOperation calldata userOp) internal pure returns (address) {
address data;
//read sender from userOp, which is first userOp member (saves 800 gas...)
assembly {data := calldataload(userOp)}
return address(uint160(data));
}
//relayer/block builder might submit the TX with higher priorityFee, but the user should not
// pay above what he signed for.
function gasPrice(UserOperation calldata userOp) internal view returns (uint256) {
unchecked {
uint256 maxFeePerGas = userOp.maxFeePerGas;
uint256 maxPriorityFeePerGas = userOp.maxPriorityFeePerGas;
if (maxFeePerGas == maxPriorityFeePerGas) {
//legacy mode (for networks that don't support basefee opcode)
return maxFeePerGas;
}
return min(maxFeePerGas, maxPriorityFeePerGas + block.basefee);
}
}
function pack(UserOperation calldata userOp) internal pure returns (bytes memory ret) {
address sender = getSender(userOp);
uint256 nonce = userOp.nonce;
bytes32 hashInitCode = calldataKeccak(userOp.initCode);
bytes32 hashCallData = calldataKeccak(userOp.callData);
uint256 callGasLimit = userOp.callGasLimit;
uint256 verificationGasLimit = userOp.verificationGasLimit;
uint256 preVerificationGas = userOp.preVerificationGas;
uint256 maxFeePerGas = userOp.maxFeePerGas;
uint256 maxPriorityFeePerGas = userOp.maxPriorityFeePerGas;
bytes32 hashPaymasterAndData = calldataKeccak(userOp.paymasterAndData);
return abi.encode(
sender, nonce,
hashInitCode, hashCallData,
callGasLimit, verificationGasLimit, preVerificationGas,
maxFeePerGas, maxPriorityFeePerGas,
hashPaymasterAndData
);
}
function hash(UserOperation calldata userOp) internal pure returns (bytes32) {
return keccak256(pack(userOp));
}
function min(uint256 a, uint256 b) internal pure returns (uint256) {
return a < b ? a : b;
}
}
Pool Config Generation
When coming across a poolConfig, the bytes can be calculated with abiEncoding. See relevant functions:
function encodeDopplerMultiCurveUniV4(
address currency,
int24[] memory tickLower_,
int24[] memory tickUpper_,
uint16[] memory numDiscoveryPositions_,
uint256[] memory maxDiscoverySupplyShare_
) internal pure returns (bytes memory) {
return abi.encode(4, currency, tickLower_, tickUpper_, numDiscoveryPositions_, maxDiscoverySupplyShare_);
}
which maps to types: ["uint8", "address", "int24[]", "int24[]", "uint16[]", "uint256[]"]