SPB Git

Repository Public

chain-costs.ts 6.4 KB 88 lines typescript

1// Chain-side inputs of the derivatives router (simulation): the Arbitrum gas price and the minimum execution fee the GMX2// contracts require for a market increase order. READ ONLY: `eth_gasPrice` and `eth_call` on public RPC endpoints — nothing is3// signed, nothing is sent, no key exists here.4//5// GMX execution fee (docs/v4/venues/gmx.md §1, contracts gas/GasUtils.sol and data/Keys.sol, branch `updates`):6//   minimum fee at creation = tx.gasprice × adjustGasLimitForEstimate(estimatedGasLimit, oraclePriceCount)7//   estimatedGasLimit (increase order, no swap, no callback) = DataStore[INCREASE_ORDER_GAS_LIMIT]8//   adjustGasLimitForEstimate = DataStore[ESTIMATED_GAS_FEE_BASE_AMOUNT_V2_1] + DataStore[ESTIMATED_GAS_FEE_PER_ORACLE_PRICE] × oraclePriceCount9//                               + estimatedGasLimit × DataStore[ESTIMATED_GAS_FEE_MULTIPLIER_FACTOR] / 10^3010//   oraclePriceCount = 3 + swaps (GasUtils.estimateOrderOraclePriceCount)11// The keeper is paid only the gas it used; the rest is refunded. What is computed here is what must be SENT, an upper bound of12// the cost. A DataStore key is keccak256(abi.encode(<name>)).13import { encodeAbiParameters, keccak256, toFunctionSelector } from 'viem';14import { D, type ChainCosts, type DecStr } from '@spw/derivatives';1516/** GMX DataStore on Arbitrum One — docs/v4/venues/gmx.md §4 (the address is documented as permanent) */17export const GMX_DATASTORE_ARBITRUM = '0xFD70de6b91282D8017aA4E741e9Ae325CAb992d8';18export const ARBITRUM_RPCS = ['https://arb1.arbitrum.io/rpc', 'https://arbitrum-one-rpc.publicnode.com'];19export const GMX_GAS_KEYS = ['INCREASE_ORDER_GAS_LIMIT', 'ESTIMATED_GAS_FEE_BASE_AMOUNT_V2_1', 'ESTIMATED_GAS_FEE_PER_ORACLE_PRICE', 'ESTIMATED_GAS_FEE_MULTIPLIER_FACTOR'] as const;20export type GmxGasKey = (typeof GMX_GAS_KEYS)[number];21export const dataStoreKey = (name: string): `0x${string}` => keccak256(encodeAbiParameters([{ type: 'string' }], [name]));22const GET_UINT = toFunctionSelector('getUint(bytes32)');23/** oracle prices of a market increase without a swap path */24export const GMX_ORACLE_PRICE_COUNT = 3n;25export const CHAIN_COSTS_TTL_MS = 30_000;26/** DataStore gas parameters change by governance only: one read an hour is enough */27const PARAMS_TTL_MS = 3_600_000;28const WEI = D.from(10n ** 18n);2930export interface GmxGasParams { increaseOrderGasLimit: bigint; baseAmount: bigint; perOraclePrice: bigint; multiplierFactor: bigint }31/** `GasUtils.adjustGasLimitForEstimate` for a market increase order */32export function gmxEstimatedExecutionGas(p: GmxGasParams): bigint {33  return p.baseAmount + p.perOraclePrice * GMX_ORACLE_PRICE_COUNT + (p.increaseOrderGasLimit * p.multiplierFactor) / 10n ** 30n;34}3536export interface ChainCostsReader { read(ethUsd: { value: DecStr; source: string } | null): Promise<ChainCosts> }3738export function createChainCostsReader(o: { fetch?: typeof fetch; now?: () => number; rpcs?: string[]; timeoutMs?: number } = {}): ChainCostsReader {39  const f = o.fetch ?? fetch, now = o.now ?? Date.now, rpcs = o.rpcs ?? ARBITRUM_RPCS, timeoutMs = o.timeoutMs ?? 4000;40  let id = 0;41  const rpc = async (method: string, params: unknown[]): Promise<bigint> => {42    let last: unknown = new Error('no RPC endpoint');43    for (const url of rpcs) {44      try {45        const r = await f(url, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: ++id, method, params }), signal: AbortSignal.timeout(timeoutMs) });46        const j = await r.json() as { result?: unknown; error?: { message?: string } };47        if (typeof j.result !== 'string' || !/^0x[0-9a-fA-F]+$/.test(j.result)) throw new Error(j.error?.message ?? `unexpected answer to ${method}`);48        return BigInt(j.result);49      } catch (e) { last = e; }50    }51    throw last;52  };53  let gas: { at: number; wei: bigint } | null = null, params: { at: number; p: GmxGasParams } | null = null;54  const gasPrice = async (): Promise<bigint> => {55    if (gas && now() - gas.at < CHAIN_COSTS_TTL_MS) return gas.wei;56    const wei = await rpc('eth_gasPrice', []);57    if (wei <= 0n) throw new Error('gas price is not positive');58    gas = { at: now(), wei };59    return wei;60  };61  const gmxParams = async (): Promise<GmxGasParams> => {62    if (params && now() - params.at < PARAMS_TTL_MS) return params.p;63    const [increaseOrderGasLimit, baseAmount, perOraclePrice, multiplierFactor] = await Promise.all(GMX_GAS_KEYS.map((k) => rpc('eth_call', [{ to: GMX_DATASTORE_ARBITRUM, data: `${GET_UINT}${dataStoreKey(k).slice(2)}` }, 'latest'])));64    const p = { increaseOrderGasLimit: increaseOrderGasLimit!, baseAmount: baseAmount!, perOraclePrice: perOraclePrice!, multiplierFactor: multiplierFactor! };65    // a wrong key reads zero: a zero gas limit or multiplier means the keys are not what the contracts use — say unknown, never a fee of zero66    if (p.increaseOrderGasLimit === 0n || p.multiplierFactor === 0n) throw new Error('GMX DataStore answered zero for a gas parameter');67    params = { at: now(), p };68    return p;69  };70  const msg = (e: unknown): string => (e instanceof Error ? e.message : String(e)).slice(0, 120);7172  return {73    /** never throws: what cannot be read is null, with the reason in the note — the router then reports that cost as unknown */74    async read(ethUsd) {75      const out: ChainCosts = { gasPriceEth: null, ethUsd: ethUsd?.value ?? null, ethUsdSource: ethUsd?.source ?? 'no ETH price in the snapshot', gmxExecutionFeeEth: null, gmxExecutionFeeNote: '', ts: now() };76      let wei: bigint;77      try { wei = await gasPrice(); out.gasPriceEth = D.str(D.div(D.from(wei), WEI, 18, 'down')); }78      catch (e) { out.gmxExecutionFeeNote = `the Arbitrum gas price could not be read (${msg(e)})`; return out; }79      try {80        const p = await gmxParams(), limit = gmxEstimatedExecutionGas(p);81        out.gmxExecutionFeeEth = D.str(D.div(D.from(limit * wei), WEI, 18, 'up'));82        out.gmxExecutionFeeNote = `minimum the contracts require at this gas price: ${limit} gas (DataStore: increase order gas limit ${p.increaseOrderGasLimit}, base ${p.baseAmount}, ${p.perOraclePrice} per oracle price × ${GMX_ORACLE_PRICE_COUNT}, multiplier ${D.str(D.div(D.from(p.multiplierFactor), D.from(10n ** 30n), 6, 'half_even'))}) × ${wei} wei; the venue's interface adds a buffer, and the unused part is refunded`;83      } catch (e) { out.gmxExecutionFeeNote = `the GMX gas parameters could not be read from the DataStore (${msg(e)})`; }84      return out;85    },86  };87}88
Files main
Menu

Theme