SPB Git

Repository Public

policy.ts 6.3 KB 92 lines typescript

1import { z } from 'zod';2import { baseUnits, parseUnits } from './amounts.ts';3import { NETWORKS } from './networks.ts';4import type { Intent } from './intent.ts';56const U = z.string().regex(/^(0|[1-9]\d{0,77})$/);7/** Signer-side policy. Limits are in base units, keyed by "<network>:<symbol>". Missing key = 0 = deny. */8export const PolicySchema = z.strictObject({9  version: z.number().int().nonnegative(),10  perTx: z.record(z.string(), U),11  daily: z.record(z.string(), U),12  maxFee: z.record(z.string(), U),13  maxApprovalTtlSec: z.number().int().min(30).max(900),14  /** when non-empty, only these destinations may receive funds ("<network>:<address>") */15  allowlist: z.array(z.string()),16});17export type Policy = z.infer<typeof PolicySchema>;1819/**20 * Per-transfer caps in whole units, sized to roughly 50 USD at the prices of 2026-11-10 (the daily cap is twice that).21 * A symbol missing here cannot be sent at all (default deny). Edit policy.json on the signer machine to change a limit.22 */23export const CAPS: Record<string, string> = {24  USDC: '50', USDT: '50', PYUSD: '50', DAI: '50', USDS: '50', USDe: '50', FDUSD: '50', RLUSD: '50', USDG: '50', EURC: '45',25  ETH: '0.025', WETH: '0.025', BTC: '0.0007', WBTC: '0.0007', SOL: '0.6', BNB: '0.075', AVAX: '6', HYPE: '0.7', POL: '550', LTC: '1', XLM: '350', XRP: '47', TRX: '150', DOGE: '700', BCH: '0.2',26  APT: '88', // 0.5675 USD on 2026-11-10 (CoinGecko)27  ATOM: '36',28  SUI: '60',29  NEAR: '14',30  TON: '45',31  LINK: '5', UNI: '7.5', AAVE: '0.4', LDO: '150', PAXG: '0.0125', ONDO: '135', RENDER: '30', JUP: '200', RAY: '30',32  BONK: '18000000', PEPE: '9000000', SHIB: '11000000',33  // Registry v5 (2026-11-10). Same size: about 50 USD per transfer at the prices of 2026-11-10 (DefiLlama price API, coins.llama.fi,34  // keyed by CoinGecko id; USD price in the comment). Tokens registered under an existing ticker (Binance-Peg ETH, XRP, DOGE, SOL,35  // LTC, AVAX on BNB Chain; bridged USDC, USDT, DAI, WBTC, WETH, LINK, UNI, AAVE) use the existing limit of that ticker, unchanged.36  USDT0: '50', 'USDC.e': '50', GHO: '50', crvUSD: '50', frxUSD: '50', USDD: '50', // 0.997–1.00037  cbBTC: '0.0007', tBTC: '0.0007', BTCB: '0.0007', 'BTC.b': '0.0007', // 73 604 / 73 488 / 73 418 / 73 50838  'WETH.e': '0.025', // 2 05839  wstETH: '0.02', rETH: '0.021', cbETH: '0.021', weETH: '0.022', // 2 567 / 2 416 / 2 360 / 2 27840  'LINK.e': '5', // 10.3841  ARB: '300', OP: '500', WPOL: '550', WAVAX: '6', WBNB: '0.075', WHYPE: '0.7', MNT: '120', // 0.1643 / 0.09997 / 0.08861 / 8.407 / 679.6 / 71.64 / 0.424442  cbXRP: '47', cbDOGE: '730', cbLTC: '1', cbADA: '285', ADA: '285', DOT: '60', // 1.050 / 0.06822 / 50.87 / 0.1748 / 0.1754 / 0.846443  MKR: '0.036', SKY: '840', CRV: '200', COMP: '2.5', SNX: '300', PENDLE: '32', ENA: '325', GMX: '6.5', // 1 381 / 0.05974 / 0.2499 / 19.95 / 0.1643 / 1.583 / 0.1536 / 7.65844  AERO: '90', VELO: '2100', CAKE: '27', JOE: '1850', MORPHO: '25', EIGEN: '300', // 0.5456 / 0.02373 / 1.835 / 0.02715 / 2.016 / 0.161545  BRETT: '13500', TOSHI: '550000', DEGEN: '60000', // 0.003725 / 0.00009058 / 0.000832946  JTO: '150', PYTH: '700', HNT: '200', ORCA: '40', mSOL: '0.43', jitoSOL: '0.46', bSOL: '0.45', // 0.3384 / 0.06958 / 0.2543 / 1.265 / 117.3 / 108.5 / 109.847};48const STABLE_FEE: Record<string, string> = {49  ethereum: '5000000000000000', base: '300000000000000', arbitrum: '300000000000000', optimism: '300000000000000',50  polygon: '500000000000000000', bsc: '1000000000000000', avalanche: '20000000000000000', hyperevm: '20000000000000000',51  bitcoin: '5000', litecoin: '100000', dogecoin: '100000000', bitcoincash: '50000', solana: '3000000', stellar: '1000000', xrpl: '100000',52  aptos: '10000000', // 0.1 APT: a transfer that creates the recipient's store uses ≈ 5 400 gas units (0.0054 APT at the minimum price)53  cosmos: '50000',  // 0.05 ATOM: a send costs about 0.0015 ATOM (200 000 gas at 0.005 uatom + margin); leaves room for a tenfold fee-market rise54  sui: '50000000',  // 0.05 SUI: a plain transfer charges about 0.002 SUI; the gas budget (the most that can be charged) is bounded by this55  near: '20000000000000000000000',  // 0.02 NEAR: worst case of a transfer that creates an implicit account (10 Tgas at the protocol's highest gas price)56  ton: '50000000',  // 0.05 TON: a transfer costs about 0.003 TON, the first one (wallet deployment) about 0.005 TON57  tron: '40000000', // 40 TRX: a USDT transfer to a first-time holder burns ≈13.5 TRX of energy at 100 sun, plus margin58};59function buildDefaultPolicy(): Policy {60  const perTx: Record<string, string> = {}, daily: Record<string, string> = {};61  for (const n of Object.values(NETWORKS)) for (const a of [n.native, ...n.tokens]) {62    const cap = CAPS[a.symbol];63    if (!cap) continue;64    const v = parseUnits(cap, a.decimals);65    perTx[`${n.id}:${a.symbol}`] = v.toString();66    daily[`${n.id}:${a.symbol}`] = (v * 2n).toString();67  }68  return { version: 3, perTx, daily, maxFee: STABLE_FEE, maxApprovalTtlSec: 300, allowlist: [] };69}70/** Conservative defaults sized for a wallet holding a few tens of dollars per asset. */71export const DEFAULT_POLICY: Policy = buildDefaultPolicy();7273export type PolicyVerdict = { ok: true } | { ok: false; reason: string };7475export function checkPolicy(policy: Policy, i: Intent, spentToday: bigint, nowSec: number, halted: boolean): PolicyVerdict {76  const deny = (reason: string): PolicyVerdict => ({ ok: false, reason });77  if (halted) return deny('emergency halt is active');78  if (i.policyVersion !== policy.version) return deny('policy version changed');79  if (i.expiresAt <= nowSec) return deny('approval window expired');80  if (i.createdAt > nowSec + 60) return deny('intent dated in the future');81  if (i.expiresAt - i.createdAt > policy.maxApprovalTtlSec) return deny('approval window too long');82  const key = `${i.network}:${i.symbol}`;83  const amount = baseUnits(i.amount);84  const perTx = baseUnits(policy.perTx[key] ?? '0'), daily = baseUnits(policy.daily[key] ?? '0');85  if (amount <= 0n) return deny('amount must be positive');86  if (amount > perTx) return deny(`amount above per-transaction limit for ${key}`);87  if (spentToday + amount > daily) return deny(`daily limit exceeded for ${key}`);88  if (baseUnits(i.maxFee) > baseUnits(policy.maxFee[i.network] ?? '0')) return deny('fee ceiling above policy');89  if (policy.allowlist.length > 0 && !policy.allowlist.includes(`${i.network}:${i.to}`)) return deny('destination not in signer allowlist');90  return { ok: true };91}92
Files main
Menu

Theme