Skip to content
markpaper

src/onchain/global.ts

v0.1.0 · 5.4 KB

Download file
import { createHash } from 'node:crypto';
import { base58Encode } from '../address/base58.js';
import { PROGRAM_ADDRESS } from './header.js';

const sha256 = (s: string) => createHash('sha256').update(s).digest();
export const GLOBAL_CONFIGURATION = '2zskx2iyCvb6Stg7RBZkt1f6MrF4dpYtMG3yMvKwqtUZ'; // privacy-allow: public Phoenix mainnet global configuration, rise-public
export const LOG_AUTHORITY = 'GdxfTLSsdSY37G6fZoYtdGDSfgFnbT2EmRpuePZxWShS'; // privacy-allow: public Phoenix mainnet log authority, rise-public
export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111'; // privacy-allow: public Solana compute budget program
export const MAX_TX_BYTES = 1232; // Solana transaction wire size ceiling
export const MAX_CANCEL_IDS = 30; // Conservative validated ceiling; actual wire size is checked too
export const MAX_LIMIT_ORDERS_PER_SIDE = 64; // rise 0.5.28 scaleOrders.ts
export interface ExchangeKeys {
  globalConfig: string;
  perpAssetMap: string;
  globalTraderIndex: string[];
  activeTraderBuffer: string[];
}
export const PREF_DISABLE_COLLATERAL_SWEEP = 1 << 0;
export const PREF_DISABLE_POSITION_AUTHORITY_SWAP = 1 << 1;
const SPOT_FLAG_IS_ACTIVE = 1 << 0;
const SPOT_FLAG_DISABLE_POSITION_AUTHORITY_SWAP = 1 << 2;
export function describePreferences(bits: number) {
  const b = Number(bits) >>> 0;
  return {
    bits: b,
    disableCollateralSweep: (b & PREF_DISABLE_COLLATERAL_SWEEP) !== 0,
    disablePositionAuthoritySwap: (b & PREF_DISABLE_POSITION_AUTHORITY_SWAP) !== 0,
    reservedBits: b & ~(PREF_DISABLE_COLLATERAL_SWEEP | PREF_DISABLE_POSITION_AUTHORITY_SWAP),
  };
}
export const GLOBAL_CONFIG_DISCRIMINANT = sha256('account:global_configuration').subarray(0, 8);
export const GLOBAL_CONFIG_LAYOUT = {
  ACCOUNT_KEY: 8,
  PERP_ASSET_MAP: 360,
  GTI_HEADER: 392,
  ATB_HEADER: 424,
  QUOTE_DECIMALS: 505,
  SPOT_META: 776,
  SPOT_FLAGS: 776 + 104,
  LEN: 1104,
};
export function parseGlobalConfig(data: Uint8Array, owner: string | null) {
  if (owner !== PROGRAM_ADDRESS)
    return { ok: false, why: `GlobalConfig owner ${owner ?? 'missing'} differs from mainnet program` };
  if (!(data instanceof Uint8Array) || data.length < GLOBAL_CONFIG_LAYOUT.LEN)
    return {
      ok: false,
      why: `Global configuration requires ${GLOBAL_CONFIG_LAYOUT.LEN} bytes; received ${data?.length ?? 0}`,
    };
  if (!Buffer.from(data.subarray(0, 8)).equals(GLOBAL_CONFIG_DISCRIMINANT))
    return { ok: false, why: 'Invalid account:global_configuration discriminator' };
  const key = (o: number) => base58Encode(data.subarray(o, o + 32));
  const spotFlags = data[GLOBAL_CONFIG_LAYOUT.SPOT_FLAGS]!;
  return {
    ok: true,
    accountKey: key(GLOBAL_CONFIG_LAYOUT.ACCOUNT_KEY),
    perpAssetMap: key(GLOBAL_CONFIG_LAYOUT.PERP_ASSET_MAP),
    globalTraderIndexHeader: key(GLOBAL_CONFIG_LAYOUT.GTI_HEADER),
    activeTraderBufferHeader: key(GLOBAL_CONFIG_LAYOUT.ATB_HEADER),
    quoteDecimals: data[GLOBAL_CONFIG_LAYOUT.QUOTE_DECIMALS],
    nativeSol: {
      flags: spotFlags,
      active: (spotFlags & SPOT_FLAG_IS_ACTIVE) !== 0,
      positionAuthoritySwapDisabled: (spotFlags & SPOT_FLAG_DISABLE_POSITION_AUTHORITY_SWAP) !== 0,
    },
  };
}
export function checkViewKeysAgainstChain(keys: ExchangeKeys, gc: ReturnType<typeof parseGlobalConfig>) {
  const p: string[] = [];
  if (!gc?.ok) return [gc?.why ?? 'Global configuration unavailable'];
  if (gc.accountKey !== GLOBAL_CONFIGURATION)
    p.push(`GlobalConfig.account_key ${gc.accountKey} ≠ ${GLOBAL_CONFIGURATION}`);
  if (keys.perpAssetMap !== gc.perpAssetMap)
    p.push(`REST perpAssetMap ${keys.perpAssetMap} differs from on-chain key ${gc.perpAssetMap}`);
  if (keys.globalTraderIndex[0] !== gc.globalTraderIndexHeader)
    p.push(
      `REST globalTraderIndex[0] ${keys.globalTraderIndex[0]} differs from on-chain key ${gc.globalTraderIndexHeader}`,
    );
  if (keys.activeTraderBuffer[0] !== gc.activeTraderBufferHeader)
    p.push(
      `REST activeTraderBuffer[0] ${keys.activeTraderBuffer[0]} differs from on-chain key ${gc.activeTraderBufferHeader}`,
    );
  if (gc.quoteDecimals !== 6) p.push(`Quote decimals ${gc.quoteDecimals} differs from the supported protocol scale 6`);
  return p;
}
/** @experimental SwapNative semantics follow rise-public Rust source; the attack path has not been verified. */
export function keyRiskOf(
  prefs: ReturnType<typeof describePreferences> | null,
  gc: ReturnType<typeof parseGlobalConfig> | null,
) {
  const traderOptOut = prefs ? prefs.disablePositionAuthoritySwap : null;
  const exchangeKill = gc?.ok && gc.nativeSol ? gc.nativeSol.positionAuthoritySwapDisabled : null;
  const featureActive = gc?.ok && gc.nativeSol ? gc.nativeSol.active : null;
  let positionAuthoritySwap:
    | 'closed_by_trader'
    | 'closed_by_exchange'
    | 'unknown'
    | 'open'
    | 'open_when_feature_active';
  if (traderOptOut === true) positionAuthoritySwap = 'closed_by_trader';
  else if (exchangeKill === true) positionAuthoritySwap = 'closed_by_exchange';
  else if (traderOptOut === null || exchangeKill === null) positionAuthoritySwap = 'unknown';
  else positionAuthoritySwap = featureActive ? 'open' : 'open_when_feature_active';
  const warning =
    positionAuthoritySwap === 'open' || positionAuthoritySwap === 'open_when_feature_active'
      ? 'position_authority_swap_enabled'
      : null;
  return {
    positionAuthoritySwap,
    warning,
    traderOptOut,
    exchangeKillSwitch: exchangeKill,
    nativeSolCollateralActive: featureActive,
  };
}
All files