diff --git a/benchmarks/perp-exit-custody.yml b/benchmarks/perp-exit-custody.yml deleted file mode 100644 index ca661618..00000000 --- a/benchmarks/perp-exit-custody.yml +++ /dev/null @@ -1,187 +0,0 @@ -# OpenChainBench. Bench 123 - -slug: perp-exit-custody -number: "123" -title: "Perp DEX exit custody: worst-case hours to withdraw without operator help, live" -seo_title: "Perp DEX custody risk 2026" -seo_description: "Lighter leads perp DEX exit independence at 336h worst-case (14-day Desert Mode, self-provable ZK exit). Ostium 720h (public tryNewSettlement() fallback). gains.trade, GMX, Hyperliquid and Aster have no permissionless exit path. Withdrawal depends on operator liveness. Live settlement age monitored." -subtitle: "Worst-case hours a user must wait to withdraw funds without any operator or validator cooperation. Lower is better. Venues with no permissionless exit path are ranked last. Settlement freshness monitored live via RPC for venues with on-chain settlement mechanisms." -category: Trading -status: draft -metric: Worst-case exit hours (no operator) -unit: count -higher_is_better: false - -disclaimer: | - Worst-case exit hours: if the protocol operator (keeper, sequencer, validator set) went offline permanently, how long until a user can withdraw without any cooperation? Venues with no permissionless path are marked infinity. This is a custody-risk metric, not a UX metric. In normal operation all venues process withdrawals faster. Evidence tier (bytecode/documented) noted per venue. - -seo_intro: | - The standard perp DEX comparison covers fees, speed and liquidity. It - almost never covers the question that matters most for large capital: - if the team disappeared tomorrow, could you get your money out? - This benchmark measures the worst-case hours to permissionless - withdrawal for seven major perp venues: gains.trade, GMX v2, Ostium, - Vertex, Hyperliquid, Lighter and Aster, using on-chain source code - analysis (Slither, cast) and official documentation as evidence. - - The results are not intuitive. The protocol with the most - censorship-resistant LP exit on Arbitrum is Ostium, not through - any design claim, but because its vault source code contains a - public function, tryNewSettlement(), that anyone can call once - the maxSettlementInterval elapses. gains.trade and GMX v2 have - no equivalent: gains LP withdrawals require oracle epochs to - advance (oracle-triggered, not time-triggered), and GMX withdrawals - require keeper execution with the CONTROLLER role. Hyperliquid, - the volume leader, has no escape hatch: the bridge contract on - Arbitrum is upgradeable by a single EOA with no timelock, and - withdrawals require 2/3 validator co-signature with no user override. - The only venue with a formally bounded exit window under operator - failure is Lighter, via a 14-day Desert Mode triggered from the - Ethereum priority queue. - -abstract: | - The harness combines two layers. The first is a static registry - (registry.yaml) encoding the exit architecture per venue: whether - a permissionless exit path exists, what its trigger mechanism is, - and the worst-case duration in hours. This registry is derived from - verified smart contract source code (via deficollective/permission-scanner - with Slither) for EVM venues and from official documentation plus - L2Beat for non-EVM venues. The confidence tier (bytecode/onchain/documented) - is stored per field. - - The second layer is a live RPC/HTTP probe that runs every 5 minutes - and emits fresh metrics for venues with on-chain settlement mechanisms. - For Ostium the harness reads lastSettlementTs and maxSettlementInterval - from the OstiumVault proxy to compute settlement age in seconds and - a flag for whether tryNewSettlement() is currently callable. For - gains.trade the harness reads currentEpoch() and currentEpochStart() - from each gToken vault to track epoch freshness. - - The worst-case exit hours for each venue: - gains.trade: infinity (oracle must advance epochs; no time-based override). - GMX v2: infinity (keeper CONTROLLER role required to execute withdrawals; no override). - Ostium: 720h (3 settlements x 30-day maxSettlementInterval; tryNewSettlement() public). - Vertex: unknown (implementation contracts not verified on Arbiscan; architecture opaque). - Hyperliquid: infinity (2/3 validator co-sign required; no escape hatch; bridge upgradeable by EOA). - Lighter: 336h (14-day Desert Mode from priority queue; self-provable ZK exit; circuits open source). - Aster: infinity (2/3 internal validator co-sign required; no escape hatch documented). - -methodology: - - "Static architecture: contract source code downloaded via Etherscan API v2 for EVM venues; analyzed with deficollective/permission-scanner (Slither). Evidence tier noted per field: bytecode (from verified source), onchain (from direct RPC read), documented (from official docs or L2Beat)." - - "gains.trade (gToken vaults, solc 0.8.9): withdraw()/redeem() have no admin modifier (ERC4626 checks only), but maxWithdraw(holder) = 0 for real holders. Mandatory 2-step: makeWithdrawRequest() then wait 3 oracle epochs then withdraw(). Epochs advance via openTradesPnlFeed oracle callback, not by time. If oracle stops, epochs freeze indefinitely. Current epoch: {{metric:perp_exit_epoch_number{venue=gains}}}." - - "GMX v2 (DataStore solc 0.8.18, Timelock solc 0.8.18): scanner found 39 DataStore functions gated onlyController. Withdrawals via WithdrawalHandler require keeper bots with CONTROLLER role. cancelWithdrawal() lets users recover pending requests but not execute them. Worst-case: keepers offline, no exit. Custom Timelock TIMELOCK_ADMIN held by 2 plain EOAs among 4 holders." - - "Ostium (OstiumVault impl solc 0.8.24): requestWithdraw has checks modifier; claimWithdraw has none. Settlement: (a) operator calls _settlement(); (b) anyone calls tryNewSettlement() PUBLIC once maxSettlementInterval (30 days, changed from 24h by gov) elapses. withdrawSettlementDelay=2 means 3 settlements needed. Worst-case: 3x30 = 90 days. Live: settlement age {{metric:perp_exit_settlement_age_hours{venue=ostium}}}h, callable: {{metric:perp_exit_settlement_callable{venue=ostium}}}." - - "Vertex (Arbitrum, Endpoint proxy solc 0.8.2): ALL implementation contracts (Endpoint impl 0x91ffc8, Clearinghouse impl 0x63a497, SpotEngine 0x32d91a, PerpEngine 0xb74c78, OffchainExchange 0xa43698) are NOT verified on Arbiscan. GitHub repo documents submitSlowModeTransaction() slow-mode mechanism but bytecode cannot be compared to repo. Worst-case: unknown. Displayed as infinity." - - "Hyperliquid (bridge on Arbitrum 0x2Df1c5, verified): L2Beat confirmed bridge upgradeable by EOA with no timelock delay. Withdrawals require 2/3+ staking-weighted validator co-signature (~27 nodes, historically team-controlled). No escape hatch documented. Worst-case: infinity. L2Beat: no window for users to exit since contracts are instantly upgradeable. Category: Other (not rated as L2)." - - "Lighter (Ethereum mainnet, ZkLighter 0x3B4D79, verified, L2Beat Stage 0): Desert Mode triggers when sequencer ignores priority queue for 14 days. Rollup freezes; user self-generates ZK proof of balance from Ethereum blob data and exits directly from contract. Prover circuits open source at github.com/elliottech/lighter-prover, audited by ZKSecurity. UpgradeGatekeeper has no delay, concurrent risk. Worst-case: 14 days = 336h." - - "Aster (multi-chain: BNB primary, ETH, ARB, SOL): cross-chain withdrawals require 2-of-3 internal validator co-signature. No forced exit or escape hatch documented. Core chain (Aster PoSA L1, March 2026) closed source. Upgrade key management not disclosed. Worst-case: infinity. Tier: documented (bridge contracts not independently verified at measurement time)." - - "Live probe cadence: every 5 minutes. Ostium: cast call OstiumVault lastSettlementTs() and maxSettlementInterval() on Arbitrum. gains: cast call gDAI currentEpoch() and currentEpochStart(). Hyperliquid: bridge USDC balance on Arbitrum (sanity check). RPC: public Arbitrum One and Ethereum endpoints." - -findings: - - "Lighter is the only venue in the cohort with a formally bounded worst-case exit: 336 hours (14 days) via Desert Mode, requiring only a self-generated ZK proof from public on-chain data. No operator cooperation needed after the 14-day window." - - "Ostium is the only EVM venue on Arbitrum with a permissionless exit fallback: tryNewSettlement() is a PUBLIC function anyone can call once maxSettlementInterval (currently 30 days) elapses. Worst-case: 3 settlements x 30 days = 90 days. Settlement age live: {{metric:perp_exit_settlement_age_hours{venue=ostium}}}h." - - "gains.trade LP exit has no admin permission gate (withdraw/redeem use ERC4626 checks modifier) but requires 3 oracle epoch advances. Epochs are triggered by openTradesPnlFeed oracle callbacks, not by time. If the oracle stops, epochs freeze indefinitely. Worst-case: infinity. Current epoch: {{metric:perp_exit_epoch_number{venue=gains}}}." - - "GMX v2 withdrawals require keeper bots with the CONTROLLER role to execute. No user override exists. Worst-case: infinity. Additional finding: 2 of 4 TIMELOCK_ADMIN holders in GMX custom Timelock are plain EOAs (single private key, no multisig)." - - "Hyperliquid bridge (Arbitrum 0x2Df1c5) is upgradeable by a single EOA with no timelock (L2Beat CRITICAL flag). Withdrawals require 2/3+ validator co-signature. No escape hatch. Worst-case: infinity." - - "Vertex implementation contracts (Endpoint, Clearinghouse, SpotEngine, PerpEngine, OffchainExchange) are NOT verified on Arbiscan. The GitHub repo documents a slow-mode exit but bytecode cannot be confirmed to match. Worst-case: unknown." - - "Aster cross-chain withdrawals require 2-of-3 internal validator co-sign. No forced exit documented. Core chain closed source. Worst-case: infinity. Tier: documented only." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/permissions-scan - -prometheus: - window: 24h - expected_freshness_seconds: 600 - freshness_metric: perp_exit_settlement_age_seconds - -faq: - - q: "What does 'worst-case exit hours' mean?" - a: "The hours a user would wait to withdraw all funds if the protocol operator (keeper, sequencer, validator set) went permanently offline right now, with no operator cooperation. Venues with no permissionless path are marked infinity. In normal operation all venues process withdrawals much faster." - - q: "Why does Ostium rank better than gains.trade for LP custody?" - a: "Ostium's OstiumVault has tryNewSettlement(), a public function anyone can call once maxSettlementInterval (30 days currently) has passed since the last settlement. After it fires, claimWithdraw() has no permission modifier and executes immediately. gains.trade gToken epochs advance only when the openTradesPnlFeed oracle provides a new value; there is no time-based override. If the oracle stops, gains LP withdrawals are frozen with no user override." - - q: "Does 'no admin gate on withdraw()' mean permissionless exit?" - a: "Not necessarily. The Slither permission scanner only reports modifier-level access control. It does not detect state-based locks: epoch locks (gains), settlement delays (ostium), keeper-required execution (GMX), or time-gated queues. maxWithdraw(holder) returning 0 for real holders with positive balances is the concrete on-chain test. This bench tested real gDAI holders and confirmed maxWithdraw = 0 in all cases; meaning withdraw() would revert despite having no onlyOwner modifier." - - q: "Why is Lighter rated Stage 0 if it has an escape hatch?" - a: "L2Beat's Stage 1 requires no security council or upgrade delay. Lighter's UpgradeGatekeeper EOA can upgrade the contract instantly with no delay. An instant-upgrade key can drain funds before Desert Mode would ever trigger. Stage 0 correctly reflects that the upgrade risk exists regardless of the Desert Mode mechanism." - - q: "Why are Hyperliquid's implementation contracts not on Arbitrum?" - a: "Hyperliquid runs its own L1 (HyperBFT). Only the USDC bridge is on Arbitrum. User funds are escrowed on Arbitrum; the trading logic runs on the Hyperliquid L1, which is closed source." - - q: "Why is Vertex shown as unknown rather than infinity?" - a: "Unknown is distinct from infinity. Infinity means we know no permissionless exit exists. Unknown means the bytecode of the deployed contracts is not verifiable (implementation contracts are not verified on Arbiscan), so we cannot determine from on-chain evidence whether a slow-mode exit exists in the deployed code. The GitHub repo documents one, but the repo describes intent, not the deployed system." - - q: "Is Ostium's tryNewSettlement() guaranteed to work if I call it after 30 days?" - a: "It is permissionless once the time condition is met, but it has a dependency: _updateAccPnlPerTokenUsed() is called internally, which reads from the oracle price. If the oracle feed is stale or reverts, the settlement may fail. The permissionless path is time-gated, not fully oracle-independent." - - q: "How often is this data updated?" - a: "Live metrics (settlement age, epoch number) are updated every 5 minutes via RPC probes. Architecture findings (worst-case exit hours, ownership chain, upgrade delay) are derived from versioned scanner output and updated when a governance action or contract upgrade changes the architecture. The measurement block and timestamp are stored in harnesses/permissions-scan/out/*.json." - -providers: - - slug: gains - name: gains.trade - tag: EVM Arbitrum — gToken vaults — bytecode verified - confidence_tier: bytecode - worst_case_exit_hours: .inf - formula: "Worst-case exit: infinity. Oracle must advance epochs (withdrawEpochsTimelock=3 oracle cycles). Live: perp_exit_epoch_number{venue=gains} from gDAI.currentEpoch() on Arbitrum." - queries: - current: perp_exit_epoch_number{venue="gains"} - epoch_age_hours: perp_exit_epoch_age_hours{venue="gains"} - settlement_callable: perp_exit_settlement_callable{venue="gains"} - series: perp_exit_epoch_age_hours{venue="gains"} - - - slug: gmx - name: GMX v2 - tag: EVM Arbitrum — WithdrawalHandler keeper — bytecode verified - confidence_tier: bytecode - worst_case_exit_hours: .inf - formula: "Worst-case exit: infinity. Keeper CONTROLLER role required to execute withdrawals; no user override. Live: no recurrent settlement metric (operator-gated)." - queries: - current: perp_exit_worst_case_hours{venue="gmx"} - series: perp_exit_worst_case_hours{venue="gmx"} - - - slug: ostium - name: Ostium - tag: EVM Arbitrum — tryNewSettlement() public — bytecode verified - confidence_tier: bytecode - worst_case_exit_hours: 720 - formula: "Worst-case: 3x maxSettlementInterval (30 days = 720h per round). tryNewSettlement() is public; callable by anyone once interval passes. Live: perp_exit_settlement_age_hours{venue=ostium}." - queries: - current: perp_exit_settlement_age_hours{venue="ostium"} - settlement_callable: perp_exit_settlement_callable{venue="ostium"} - last_settlement_id: perp_exit_last_settlement_id{venue="ostium"} - series: perp_exit_settlement_age_hours{venue="ostium"} - - - slug: vertex - name: Vertex - tag: EVM Arbitrum — impl unverified — unknown - confidence_tier: unknown - worst_case_exit_hours: .inf - formula: "Worst-case exit: unknown. Endpoint impl, Clearinghouse impl, SpotEngine, PerpEngine, OffchainExchange are NOT verified on Arbiscan. GitHub repo documents slow-mode but bytecode cannot be confirmed." - queries: - current: perp_exit_worst_case_hours{venue="vertex"} - series: perp_exit_worst_case_hours{venue="vertex"} - - - slug: hyperliquid - name: Hyperliquid - tag: Custom L1 HyperBFT — ARB bridge EOA-upgradeable — documented - confidence_tier: documented - worst_case_exit_hours: .inf - formula: "Worst-case exit: infinity. 2/3+ validator co-sign required. Bridge upgradeable by single EOA with no timelock. No escape hatch. L2Beat CRITICAL flag. Live: perp_exit_bridge_balance_usd{venue=hyperliquid}." - queries: - current: perp_exit_bridge_balance_usd{venue="hyperliquid"} - series: perp_exit_bridge_balance_usd{venue="hyperliquid"} - - - slug: lighter - name: Lighter - tag: ETH ZK-rollup — Desert Mode 14d — documented - confidence_tier: documented - worst_case_exit_hours: 336 - formula: "Worst-case: 336h (14 days). Desert Mode triggers from Ethereum priority queue after sequencer ignores for 14 days. User self-generates ZK proof and exits. Prover open source. UpgradeGatekeeper EOA has no delay; concurrent risk." - queries: - current: perp_exit_worst_case_hours{venue="lighter"} - series: perp_exit_worst_case_hours{venue="lighter"} - - - slug: aster - name: Aster - tag: Multi-chain BNB/ETH/ARB/SOL — 2/3 internal validators — documented - confidence_tier: documented - worst_case_exit_hours: .inf - formula: "Worst-case exit: infinity. 2/3 internal validator co-sign required. No forced exit or escape hatch documented. Core chain closed source." - queries: - current: perp_exit_worst_case_hours{venue="aster"} - series: perp_exit_worst_case_hours{venue="aster"} diff --git a/benchmarks/perp-fees-at-size.yml b/benchmarks/perp-fees-at-size.yml deleted file mode 100644 index 7ccf07a8..00000000 --- a/benchmarks/perp-fees-at-size.yml +++ /dev/null @@ -1,212 +0,0 @@ -# OpenChainBench. Bench 120 - -slug: perp-fees-at-size -number: "120" -title: Perp DEX all-in cost at $100k, live bps ranked across 10 venues -seo_title: "Perp DEX fees at $100k 2026" -seo_description: "{{best_name}} leads cheapest perp DEX at $100k at {{best_p50}} all-in (24h avg). All-in bps at $100,000 notional: taker fee plus spread plus impact, live across gains.trade, Lighter, Hyperliquid, dYdX, GMX, Paradex, Extended, Aster, edgeX, Polymarket." -subtitle: "All-in cost in basis points to open a long at $100,000 notional, measured live on ETH, BTC and SOL. Oracle-priced venues (gains.trade, GMX) stay at their rack rate at any size. Orderbook venues add price impact on top of their taker fee. Refreshed every 5 minutes." -category: Trading -status: draft -metric: All-in cost at $100k -unit: bps -higher_is_better: false - -seo_intro: | - The cheapest-perp-DEX question changes depending on trade size. At $1k - the taker fee dominates; at $100k price impact starts to matter. Oracle - venues (gains.trade, GMX v2) charge a flat percentage of position size - regardless of how large the trade is, so their all-in bps at $100k - equals their all-in bps at $1k. Orderbook venues (Hyperliquid, Lighter, - dYdX, Paradex, Extended, Aster, edgeX, Polymarket) add a spread and - impact term that grows with size: a $100k market order walks deeper into - the book than a $1k order, widening the effective fill price. This bench - reports the 24h average of the perp-fees harness $100k tier directly, - so the ranking reflects the last 24 hours of actual measurements at that - notional, not a simulation or rack-rate extrapolation. - -abstract: | - All-in bps at $100,000 notional per venue per asset, derived from the - perp_fees_all_in_bps_tier series published by the perp-fees harness - (bench 007) with notional=100000. Oracle-priced venues (gains.trade, - GMX v2) repeat their $1k figure exactly because there is no orderbook - impact term. Orderbook venues add a spread-plus-impact component that - grows with notional. Refreshed every 5 minutes. Chain tabs scope the - board to ETH, BTC or SOL individually. - -methodology: - - "Cadence: derived from perp_fees_all_in_bps_tier{notional=100000}, refreshed every 5 minutes by the perp-fees harness (bench 007). No new collection." - - "All-in formula: taker fee plus half-spread plus price impact at $100k notional. Both components emitted separately by the harness; this bench reads the combined all_in_bps_tier gauge." - - "Oracle-priced venues (gains.trade on Base, GMX v2 on Arbitrum): fee is a flat percentage of position size with no orderbook. All-in at $100k equals all-in at $1k exactly, by construction." - - "Orderbook venues (Hyperliquid, Lighter, dYdX, Paradex, Extended, Aster, edgeX, Polymarket): the $100k tier walks deeper into each venue's visible book than the $1k tier, so the spread-plus-impact component grows and the all-in figure rises above the rack rate." - - "Missing tier: when a book cannot absorb $100k the harness skips the sample rather than extrapolating. A venue with many skipped samples shows a low sample count and should be treated as low-confidence." - - "Chain tabs: ETH, BTC and SOL scoped separately because book depth differs significantly across assets. Cross-asset averages are not shown." - - "Failures: a venue that errors keeps its last gauge and its health drops to 0." - -findings: - - "{{best_name}} leads the cohort at {{best_p50}} all-in (24h avg) for a $100k market order, including taker fee plus spread plus price impact." - - "{{name:gains}} and {{name:gmx}} are the only venues in the cohort that charge the same basis points at $100k as at $1k. Oracle pricing eliminates the impact term entirely, so the all-in figure at any size equals the protocol open fee plus half of spreadP." - - "{{name:hyperliquid}} sits at {{p50:hyperliquid}} all-in at $100k (24h avg). The HyperBFT L1 book is the deepest onchain book, keeping its $100k impact well below thinner venues." - - "{{name:lighter}} sits at {{p50:lighter}} all-in at $100k (24h avg). Zero taker fee plus the half-spread and impact at $100k notional on its zkSync book." - - "{{name:dydx}} sits at {{p50:dydx}} all-in at $100k (24h avg). Cosmos appchain orderbook with 5 bps tier-0 taker plus $100k impact." - - "{{name:paradex}} sits at {{p50:paradex}} all-in at $100k (24h avg). Starknet L2, 100-level book depth." - - "{{name:extended}} sits at {{p50:extended}} all-in at $100k (24h avg). StarkEx perps, 2.5 bps documented base rate." - - "{{name:aster}} sits at {{p50:aster}} all-in at $100k (24h avg). BNB Chain perps DEX, Binance-compatible book." - - "{{name:edgex}} sits at {{p50:edgex}} all-in at $100k (24h avg). Offchain CLOB on zkSync." - - "{{name:polymarket}} sits at {{p50:polymarket}} all-in at $100k (24h avg). Polygon perps CLOB, up to 500 book levels." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-fees - -prometheus: - window: 24h - expected_freshness_seconds: 1800 - freshness_metric: perp_fees_all_in_bps_tier - -faq: - - q: "Why does the ranking differ from the perp-fees ($1k) leaderboard?" - a: "At $1k the taker fee dominates: zero-taker venues win by definition. At $100k price impact starts to matter, and oracle venues (gains, GMX) benefit because they have no book to walk: their $100k all-in equals their $1k all-in exactly. Orderbook venues add a growing impact term. The cross-over point depends on each venue's book depth and taker fee combination." - - q: "Why are gains.trade and GMX the same bps at $100k as at $1k?" - a: "Both venues use oracle pricing with no orderbook. The fee is a flat percentage of position size: $100k notional costs the same bps as $1k notional, by construction. This is the core design tradeoff: flat cost at any size, in exchange for using a price oracle rather than a live book." - - q: "What happens if a venue cannot fill $100k?" - a: "The harness skips that sample and increments perp_fees_tier_skipped_total. The sample_size metric shows how often the tier was successfully filled in the last 24h. A low fill count at $100k means the venue cannot reliably absorb that notional, which is itself a depth signal." - - q: "How does this differ from perp-cost-slope (bench 117)?" - a: "perp-cost-slope reports the ratio of $1M cost to $1k cost: a relative measure showing how much costs grow between the two sizes. This bench reports the absolute all-in bps at $100k: the actual cost figure a trader would pay at that size. Use this bench to compare absolute cost at $100k; use perp-cost-slope to compare how much a venue's cost grows from $1k to $1M." - - q: "Is this bench the same as perp-fees?" - a: "perp-fees (bench 007) sets the headline at $1k notional, the size where taker fee dominates. This bench (bench 120) sets the headline at $100k, where impact begins to separate oracle-priced from orderbook venues. Both are derived from the same perp_fees_all_in_bps_tier series; only the notional tier in the query differs." - -dimensions: - chain: - - { value: ETH, label: ETH } - - { value: BTC, label: BTC } - - { value: SOL, label: SOL } - -providers: - - slug: gains - name: gains.trade - tag: "Synthetic perps, oracle-priced, flat cost at any size" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=gains,notional=100000}[24h]). Oracle-priced: $100k all-in equals $1k all-in, no impact term." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="gains"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="gains",notional="100000"} - - - slug: gmx - name: GMX v2 - tag: "Synthetics on Arbitrum, oracle-priced, flat cost at any size" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=gmx,notional=100000}[24h]). Oracle-priced: $100k all-in equals $1k all-in, no impact term." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="gmx"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="gmx",notional="100000"} - - - slug: lighter - name: Lighter - tag: "zk-rollup, zero taker fee, onchain orderbook" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=lighter,notional=100000}[24h]). Zero taker; spread plus impact at $100k on zkSync book." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="lighter"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="lighter",notional="100000"} - - - slug: hyperliquid - name: Hyperliquid - tag: "HyperBFT L1 perp DEX, deepest onchain book" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=hyperliquid,notional=100000}[24h]). 4.5 bps taker plus spread plus impact at $100k on the HyperBFT book." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="hyperliquid"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="hyperliquid",notional="100000"} - - - slug: dydx - name: dYdX v4 - tag: "Cosmos appchain orderbook" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=dydx,notional=100000}[24h]). 5 bps tier-0 taker plus spread plus impact at $100k on the dYdX v4 indexer book." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="dydx"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="dydx",notional="100000"} - - - slug: paradex - name: Paradex - tag: "Starknet L2 perps, 100-level book" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=paradex,notional=100000}[24h]). Starknet appchain; 100-level book depth caps visible depth at about $1M on majors." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="paradex",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="paradex",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="paradex"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="paradex",notional="100000"} - - - slug: extended - name: Extended - tag: "StarkEx perps, 2.5 bps documented base rate" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=extended,notional=100000}[24h]). Full public StarkEx book walked at $100k. Taker fee at documented 2.5 bps base." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="extended"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="extended",notional="100000"} - - - slug: aster - name: Aster - tag: "BNB Chain perp DEX, Binance-style REST book" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=aster,notional=100000}[24h]). BNB Chain CLOB, fapi depth endpoint walked at $100k notional." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="aster"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="aster",notional="100000"} - - - slug: edgex - name: edgeX - tag: "Offchain CLOB perp DEX on zkSync" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=edgex,notional=100000}[24h]). Offchain CLOB, public getDepth endpoint walked at $100k notional." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="edgex"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="edgex",notional="100000"} - - - slug: polymarket - name: Polymarket - tag: "Polygon perps, up to 500 book levels" - formula: "avg_over_time(perp_fees_all_in_bps_tier{venue=polymarket,notional=100000}[24h]). Polygon CLOB, up to 500 book levels walked at $100k notional." - queries: - p50: avg_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"}[24h]) - p90: quantile_over_time(0.90, perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"}[24h]) - p99: quantile_over_time(0.99, perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"}[24h]) - mean: avg_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"}[24h]) - success: avg_over_time(perp_fees_health{venue="polymarket"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="polymarket",notional="100000"} diff --git a/benchmarks/perp-fill-rate-1m.yml b/benchmarks/perp-fill-rate-1m.yml deleted file mode 100644 index 402ab690..00000000 --- a/benchmarks/perp-fill-rate-1m.yml +++ /dev/null @@ -1,198 +0,0 @@ -# OpenChainBench. Bench 124 - -slug: perp-fill-rate-1m -number: "124" -title: "Perp DEX $1M fill rate: fraction of cycles where $1M order fully absorbed, live ranked" -seo_title: "Perp DEX $1M order fill rate 2026" -seo_description: "{{best_name}} leads perp $1M fill rate at {{best_p50}} (24h avg). Fraction of 5-minute cycles in which the venue's visible book absorbed a full $1M buy. Oracle-priced venues always fill; thin orderbook venues may drop below 50%." -subtitle: "Fraction of 5-minute measurement cycles in which the venue's visible orderbook successfully absorbed a $1,000,000 buy. Oracle-priced venues (gains.trade, GMX v2) score 100% by construction. Orderbook venues score lower when book depth is insufficient." -category: Trading -status: draft -metric: "$1M fill rate" -unit: pct -higher_is_better: true - -seo_intro: | - Every perp DEX publishes a taker fee rate, but a fee rate is meaningless if the - venue cannot actually absorb your order. A $1M market buy on a thin orderbook either - fails outright or executes at severe slippage once the visible depth is exhausted. - This benchmark answers a simpler question: what fraction of the time can each venue - reliably absorb a full $1M buy? Oracle-priced venues (gains.trade, GMX v2) score - 100% by construction: their fee is a flat percentage of notional, with no orderbook - to deplete. Orderbook venues score lower when their visible book is thinner than - $1M on the ask side during a measurement cycle. The fill rate is derived every - five minutes from the perp-fees harness (bench 007). - -abstract: | - The $1M fill rate is the fraction of 5-minute measurement cycles in which a - venue's visible orderbook contained at least $1M of ask-side depth at the time - of sampling. When the book is too thin, the perp-fees harness skips the - $1M tier and increments perp_fees_tier_skipped_total rather than extrapolating - a cost figure. The fill rate is therefore the ratio of successful $1M measurements - to total $1k measurements over the same 24-hour window. A fill rate of 100% means - every 5-minute probe found sufficient depth. A fill rate of 50% means the book was - deep enough half the time. A fill rate of 0% means the venue never had $1M of - visible depth during the window. Oracle-priced venues always score 100% because - their fee is protocol-level and does not depend on orderbook depth. - -methodology: - - "Cadence: derived from perp_fees_all_in_bps_tier, refreshed every 5 minutes by the perp-fees harness (bench 007). Values are 24-hour averages." - - "Fill rate = count_over_time(all_in_bps_tier{notional=1000000}[24h]) / ignoring(notional) count_over_time(all_in_bps_tier{notional=1000}[24h]) per venue, averaged across all assets." - - "Oracle-priced venues (gains.trade, GMX v2): the fee is protocol-level and independent of orderbook depth. Every cycle succeeds, so fill rate = 100% by construction." - - "Orderbook venues: the harness samples the visible book depth at each measurement cycle. If ask-side depth is below $1M the tier is skipped. The fill rate equals the fraction of cycles where the book was deep enough." - - "Averaging: values are averaged across ETH, BTC and SOL markets. Use the per-asset bench (perp-cost-slope, bench 117) to see asset-level fill rates alongside the cost slope." - - "Failures and stale data: inherited from the perp-fees harness. A venue that errors keeps its last gauge and its health drops to 0." - -findings: - - "{{best_name}} leads the $1M fill rate at {{best_p50}} (24h avg). Oracle-priced venues always score 100% by construction." - - "gains.trade and GMX v2 score 100% because they use oracle pricing with no orderbook. Every trade is filled at the oracle price regardless of size." - - "Orderbook venues with deep ETH and BTC books (Hyperliquid, dYdX, Lighter) typically maintain near-100% fill rates on majors but may drop on SOL where books are thinner." - - "A fill rate below 100% is a liquidity signal stronger than any cost figure: a venue that cannot absorb $1M cannot be reliably used for institutional-size execution." - -source: https://github.com/ChainBench/OpenChainBench/tree/main/harnesses/perp-fees - -prometheus: - window: 24h - expected_freshness_seconds: 1800 - freshness_metric: perp_fees_all_in_bps_tier - -faq: - - q: "What does 100% fill rate mean?" - a: "Every 5-minute measurement cycle found sufficient ask-side depth to fill a $1M buy. Oracle-priced venues always score 100%: gains.trade and GMX v2 use a flat fee on notional with no orderbook, so every order succeeds regardless of size." - - q: "What does a low fill rate mean for a trader?" - a: "A fill rate below 100% means the venue's book was too thin to absorb $1M during some measurement cycles. In practice this means large orders either fail, execute in fragments, or suffer unexpected slippage. A fill rate of 50% means you can expect the book to be deep enough only half the time." - - q: "Why are oracle-priced venues always 100%?" - a: "gains.trade and GMX v2 charge a flat percentage of position size with no orderbook. There is no book to deplete, so every order at any size is filled at the oracle price. The $1M tier is always measurable, giving a fill rate of 100% by construction." - - q: "How does this differ from the perp-cost-slope bench?" - a: "The cost-slope bench (bench 117) shows how much a venue's all-in cost grows between $1k and $1M. The fill-rate bench shows whether the $1M tier can be measured at all. A venue with a missing slope on the cost-slope board typically has a low fill rate here." - -providers: - - slug: gains - name: gains.trade - tag: "Synthetic perps on Base, oracle-priced, always fills" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100. Oracle-priced: always 100%." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="gains"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gains",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="gains",notional="1000000"} - - - slug: gmx - name: GMX v2 - tag: "Synthetics on Arbitrum, oracle-priced, always fills" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100. Oracle-priced: always 100%." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="gmx"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="gmx",notional="1000000"} - - - slug: hyperliquid - name: Hyperliquid - tag: "HyperBFT L1 perp DEX, deepest onchain book" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="hyperliquid"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="hyperliquid",notional="1000000"} - - - slug: lighter - name: Lighter - tag: "zk-rollup, zero taker fee, onchain orderbook" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="lighter"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="lighter",notional="1000000"} - - - slug: dydx - name: dYdX v4 - tag: "Cosmos appchain orderbook" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="dydx"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="dydx",notional="1000000"} - - - slug: paradex - name: Paradex - tag: "Starknet appchain perps, 100-level book" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="paradex"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="paradex",notional="1000000"} - - - slug: extended - name: Extended - tag: "Starknet perps, full public book" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="extended"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="extended",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="extended",notional="1000000"} - - - slug: polymarket - name: Polymarket - tag: "Polygon perps, up to 500 book levels" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="polymarket"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="polymarket",notional="1000000"} - - - slug: aster - name: Aster - tag: "BNB Chain perp DEX, Binance-style REST book" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="aster"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="aster",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="aster",notional="1000000"} - - - slug: edgex - name: edgeX - tag: "Offchain CLOB perp DEX" - formula: "count_over_time(perp_fees_all_in_bps_tier{notional=1000000}[24h]) / count_over_time(perp_fees_all_in_bps_tier{notional=1000}[24h]) * 100." - queries: - p50: avg(count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])) * 100 - p90: avg(count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])) * 100 - p99: avg(count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])) * 100 - mean: avg(count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) / ignoring(notional) count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000"}[24h])) * 100 - success: avg_over_time(perp_fees_health{venue="edgex"}[24h]) - sample_size: count_over_time(perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"}[24h]) - series: perp_fees_all_in_bps_tier{venue="edgex",notional="1000000"} diff --git a/harnesses/perp-protocol-longevity/cmd/script/defillama.go b/harnesses/perp-protocol-longevity/cmd/script/defillama.go new file mode 100644 index 00000000..e54b5962 --- /dev/null +++ b/harnesses/perp-protocol-longevity/cmd/script/defillama.go @@ -0,0 +1,127 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" +) + +type dlHack struct { + Date int64 `json:"date"` + Name string `json:"name"` + Classification string `json:"classification"` + Amount float64 `json:"amount"` + Source string `json:"source"` + ReturnedFunds *float64 `json:"returnedFunds"` +} + +func fetchDefiLlamaHacks() ([]dlHack, error) { + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get("https://api.llama.fi/hacks") + if err != nil { + return nil, fmt.Errorf("fetch: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != 200 { + return nil, fmt.Errorf("status_%d", resp.StatusCode) + } + var hacks []dlHack + if err := json.Unmarshal(body, &hacks); err != nil { + return nil, fmt.Errorf("parse: %w", err) + } + return hacks, nil +} + +// enrichFromDefiLlama fetches the DeFiLlama hacks feed and appends newly-discovered +// incidents into the registry. Rules per the bench methodology: +// - Classification "Infrastructure" = front-end/phishing, skip +// - Incident date before venue launch = skip (filters pre-launch false positives) +// - Net-zero incidents (returnedFunds >= amount) = skip (no permanent loss) +// - Already-recorded incidents (±48h match) = skip +func enrichFromDefiLlama(reg []venueRecord) []venueRecord { + hacks, err := fetchDefiLlamaHacks() + if err != nil { + fmt.Printf("[LONGEVITY] DeFiLlama fetch failed: %v — using embedded registry\n", err) + return reg + } + fmt.Printf("[LONGEVITY] DeFiLlama: fetched %d incidents\n", len(hacks)) + + for i := range reg { + if len(reg[i].DefiLlamaNames) == 0 { + continue + } + for _, h := range hacks { + if !dlMatchesVenue(h, reg[i].DefiLlamaNames) { + continue + } + if h.Classification == "Infrastructure" { + continue + } + t := time.Unix(h.Date, 0).UTC() + if t.Before(reg[i].Launched) { + continue + } + if h.ReturnedFunds != nil && *h.ReturnedFunds >= h.Amount { + continue + } + if dlAlreadyRecorded(reg[i].Incidents, t) { + continue + } + net := h.Amount + if h.ReturnedFunds != nil { + net -= *h.ReturnedFunds + } + src := h.Source + if src == "" { + src = "https://defillama.com/hacks" + } + reg[i].Incidents = append(reg[i].Incidents, incidentRecord{ + Date: t, + AmountUSD: net, + Kind: dlKind(h.Classification), + Source: src, + }) + fmt.Printf("[LONGEVITY] DeFiLlama: +incident %s on %s ($%.0f net)\n", + reg[i].Slug, t.Format("2006-01-02"), net) + } + } + return reg +} + +func dlMatchesVenue(h dlHack, names []string) bool { + lower := strings.ToLower(h.Name) + for _, n := range names { + if strings.Contains(lower, strings.ToLower(n)) { + return true + } + } + return false +} + +func dlAlreadyRecorded(incidents []incidentRecord, t time.Time) bool { + for _, inc := range incidents { + diff := inc.Date.Sub(t) + if diff < 0 { + diff = -diff + } + if diff < 48*time.Hour { + return true + } + } + return false +} + +func dlKind(classification string) string { + switch classification { + case "Oracle": + return "oracle-manipulation" + case "Access Control": + return "admin-key" + default: + return "exploit" + } +} diff --git a/harnesses/perp-protocol-longevity/cmd/script/main.go b/harnesses/perp-protocol-longevity/cmd/script/main.go index c899b708..35905c3c 100644 --- a/harnesses/perp-protocol-longevity/cmd/script/main.go +++ b/harnesses/perp-protocol-longevity/cmd/script/main.go @@ -1,16 +1,18 @@ // perp-protocol-longevity -- Bench 119 // // Computes a live "days clean" counter for each perp DEX based on a -// versioned incident registry sourced from rekt.news and public post-mortems. -// No external API calls: all data is embedded in the binary at build time. +// versioned incident registry seeded from rekt.news / DeFiLlama post-mortems. +// The registry is enriched daily by polling https://api.llama.fi/hacks so new +// incidents are picked up without a harness redeploy. // // Metrics exposed on :2112/metrics: -// perp_protocol_days_clean{venue} -- primary, higher is better -// perp_protocol_incidents_total{venue} -// perp_protocol_incident_amount_usd{venue} -// perp_protocol_launch_timestamp_seconds{venue} -// perp_protocol_registry_last_updated_timestamp_seconds -// perp_protocol_health{venue} +// +// perp_protocol_days_clean{venue} -- primary, higher is better +// perp_protocol_incidents_total{venue} +// perp_protocol_incident_amount_usd{venue} +// perp_protocol_launch_timestamp_seconds{venue} +// perp_protocol_registry_last_updated_timestamp_seconds +// perp_protocol_health{venue} package main import ( @@ -23,7 +25,11 @@ import ( func main() { fmt.Println("=== perp-protocol-longevity harness ===") - fmt.Println("Bench 119 -- days clean counter from embedded incident registry.") + fmt.Println("Bench 119 -- days clean counter, enriched daily from DeFiLlama.") + + registry = enrichFromDefiLlama(registry) + lastEnriched := time.Now() + fmt.Printf("Registry date: %s. Venues: %d.\n", registryUpdatedAt.Format("2006-01-02"), len(registry)) fmt.Println("Exposes /metrics, /health on :2112.") @@ -37,7 +43,6 @@ func main() { } }() - // Publish immediately, then refresh the day counter every hour. publishAll() logCurrentCounts() @@ -50,6 +55,10 @@ func main() { fmt.Println("shutting down") return case <-tick.C: + if time.Since(lastEnriched) >= 24*time.Hour { + registry = enrichFromDefiLlama(registry) + lastEnriched = time.Now() + } publishAll() logCurrentCounts() } @@ -65,7 +74,7 @@ func logCurrentCounts() { fmt.Printf("[LONGEVITY][%s] %.1f days clean since launch (%s), 0 incidents\n", vr.Slug, days, vr.Launched.Format("2006-01-02")) } else { - fmt.Printf("[LONGEVITY][%s] %.1f days since last incident, %d total incident(s), $%.0f lost\n", + fmt.Printf("[LONGEVITY][%s] %.1f days since last incident, %d total incident(s), $%.0f net lost\n", vr.Slug, days, len(vr.Incidents), totalIncidentAmount(vr)) } } diff --git a/harnesses/perp-protocol-longevity/cmd/script/metrics.go b/harnesses/perp-protocol-longevity/cmd/script/metrics.go index 45879582..35329abd 100644 --- a/harnesses/perp-protocol-longevity/cmd/script/metrics.go +++ b/harnesses/perp-protocol-longevity/cmd/script/metrics.go @@ -50,13 +50,13 @@ func init() { healthGauge = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "perp_protocol_health", - Help: "Always 1 for this harness (no external calls, computed from embedded registry).", + Help: "Always 1 for this harness (computed from embedded registry, enriched daily from DeFiLlama).", }, []string{"venue"}) prometheus.MustRegister(healthGauge) } // registryUpdatedAt is the date the embedded registry was last reviewed. -var registryUpdatedAt = time.Date(2026, 8, 2, 0, 0, 0, 0, time.UTC) +var registryUpdatedAt = time.Date(2026, 8, 3, 0, 0, 0, 0, time.UTC) func publishAll() { registryTimestampGauge.Set(float64(registryUpdatedAt.Unix())) diff --git a/harnesses/perp-protocol-longevity/cmd/script/registry.go b/harnesses/perp-protocol-longevity/cmd/script/registry.go index 548f2847..7136464c 100644 --- a/harnesses/perp-protocol-longevity/cmd/script/registry.go +++ b/harnesses/perp-protocol-longevity/cmd/script/registry.go @@ -4,82 +4,124 @@ import "time" // incidentRecord is one confirmed security incident for a venue. // Only events resulting in direct theft or permanent loss of user funds -// via a protocol vulnerability are included. +// via a protocol vulnerability are included. Net loss is stored (gross minus returned). type incidentRecord struct { Date time.Time AmountUSD float64 Kind string // "exploit", "oracle-manipulation", "admin-key" - Source string // public post-mortem URL + Source string // public post-mortem or DeFiLlama URL } // venueRecord holds all known incidents for a venue plus its launch date. +// DefiLlamaNames: substring matches against DeFiLlama hack.Name (case-insensitive). +// Empty slice = no auto-enrichment (used when DeFiLlama would pull wrong-version incidents). type venueRecord struct { - Slug string - Name string - Launched time.Time - Incidents []incidentRecord + Slug string + Name string + Launched time.Time + Incidents []incidentRecord + DefiLlamaNames []string } -// Registry version: 2026-08-02. Updated from rekt.news and public post-mortems. -// A venue with no incidents uses its launch date as the clean-streak start. -// Incidents excluded per methodology: front-end phishing, market-structure -// events (liquidation cascades, forced close, governance disputes), oracle -// price manipulation without a smart contract vulnerability. +// Registry seeded from rekt.news / DeFiLlama as of 2026-08-03. +// Known incidents are pre-seeded so the registry works without network access. +// enrichFromDefiLlama() supplements this daily with newly-discovered incidents. var registry = []venueRecord{ { Slug: "gains", Name: "gains.trade", Launched: time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC), - // No incidents recorded in rekt.news or public post-mortems as of 2026-08-02. - // The synthetic architecture (oracle pricing, no custodial vault drainable - // without position netting, no privileged admin key on critical paths) - // reduces the attack surface relative to AMM-based vault protocols. - Incidents: []incidentRecord{}, + // No incidents recorded in DeFiLlama or public post-mortems as of 2026-08-03. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"gains.trade", "gains network"}, }, { Slug: "gmx", - Name: "GMX v2", - Launched: time.Date(2023, 8, 1, 0, 0, 0, 0, time.UTC), + Name: "GMX", + Launched: time.Date(2021, 9, 1, 0, 0, 0, 0, time.UTC), Incidents: []incidentRecord{ + // V1 oracle/price attack on Arbitrum, Sept 2022. + { + Date: time.Date(2022, 9, 18, 0, 0, 0, 0, time.UTC), + AmountUSD: 565_000, + Kind: "oracle-manipulation", + Source: "https://defillama.com/hacks", + }, + // V1 re-entrancy exploit July 2025. $42M taken; $40M returned by attacker. + // Net permanent loss: $2M. Resets the clean-streak counter. { Date: time.Date(2025, 7, 9, 0, 0, 0, 0, time.UTC), - AmountUSD: 42_000_000, + AmountUSD: 2_000_000, Kind: "exploit", - Source: "https://coinperps.xyz/gmx-v2-exploit-july-2025", + Source: "https://defillama.com/hacks", }, }, + DefiLlamaNames: []string{"gmx"}, }, { Slug: "hyperliquid", Name: "Hyperliquid", Launched: time.Date(2023, 11, 1, 0, 0, 0, 0, time.UTC), // No protocol exploit recorded. The March 2025 JellyJelly event was a - // market-structure incident (large-position liquidation cascade triggering - // a validator committee emergency close). No smart contract was compromised - // and no user funds were stolen via a protocol vulnerability. Excluded per - // the methodology's incident criteria. - Incidents: []incidentRecord{}, + // market-structure incident (liquidation cascade + validator committee + // emergency close). No smart contract compromised, no funds stolen via a + // protocol vulnerability. Excluded per methodology. + // DeFiLlama has a pre-launch "Hyperliquid" entry (June 2023, $37K) that + // predates mainnet by 5 months — filtered by the Launched date check. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"hyperliquid"}, }, { Slug: "dydx", Name: "dYdX v4", Launched: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), - // No exploit on the v4 Cosmos appchain deployment. dYdX v1/v2 on Ethereum - // had oracle manipulation events in 2020-2021 but those ran on a different - // contract and are out of scope for the v4 deployment tracked here. - Incidents: []incidentRecord{}, + // The Nov 2023 $9M incident was on dYdX V3 (Ethereum). Out of scope for + // the V4 Cosmos appchain deployment tracked here. No V4 exploit recorded. + // DefiLlamaNames intentionally empty: DeFiLlama tracks dYdX as one entity + // and would pull V3 incidents into V4. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{}, + }, + { + Slug: "lighter", + Name: "Lighter", + Launched: time.Date(2023, 7, 1, 0, 0, 0, 0, time.UTC), + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"lighter"}, + }, + { + Slug: "paradex", + Name: "Paradex", + Launched: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"paradex"}, + }, + { + Slug: "aster", + Name: "Aster", + Launched: time.Date(2024, 12, 1, 0, 0, 0, 0, time.UTC), + // Aster DEX went live under this brand after the APX Finance merger in Dec 2024. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"asterdex", "aster dex"}, }, { - Slug: "lighter", - Name: "Lighter", - Launched: time.Date(2023, 7, 1, 0, 0, 0, 0, time.UTC), - Incidents: []incidentRecord{}, + Slug: "edgex", + Name: "EdgeX", + Launched: time.Date(2024, 10, 3, 0, 0, 0, 0, time.UTC), + // StarkEx-powered non-custodial perp DEX, mainnet launch Oct 3, 2024. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{"edgex"}, }, { - Slug: "paradex", - Name: "Paradex", - Launched: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), - Incidents: []incidentRecord{}, + Slug: "polymarket", + Name: "Polymarket Perps", + Launched: time.Date(2026, 7, 8, 0, 0, 0, 0, time.UTC), + // Perps product launched July 8, 2026. Two DeFiLlama incidents for "Polymarket" + // (May-June 2026) are on the prediction market product, not perps, and predate + // the perps launch — filtered by the Launched date check. + // DefiLlamaNames intentionally empty to avoid false-positive prediction-market matches. + Incidents: []incidentRecord{}, + DefiLlamaNames: []string{}, }, } @@ -98,7 +140,7 @@ func cleanStreakStart(vr venueRecord) int64 { return last.Unix() } -// totalIncidentAmount returns the sum of USD losses across all recorded incidents. +// totalIncidentAmount returns the sum of net USD losses across all recorded incidents. func totalIncidentAmount(vr venueRecord) float64 { var total float64 for _, inc := range vr.Incidents { diff --git a/harnesses/perp-protocol-longevity/go.mod b/harnesses/perp-protocol-longevity/go.mod index 00a10d93..9c4e8096 100644 --- a/harnesses/perp-protocol-longevity/go.mod +++ b/harnesses/perp-protocol-longevity/go.mod @@ -3,3 +3,16 @@ module perp-protocol-longevity go 1.24.0 require github.com/prometheus/client_golang v1.23.2 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/sys v0.35.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect +) diff --git a/harnesses/perp-protocol-longevity/go.sum b/harnesses/perp-protocol-longevity/go.sum new file mode 100644 index 00000000..d6b8ca98 --- /dev/null +++ b/harnesses/perp-protocol-longevity/go.sum @@ -0,0 +1,46 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/src/app/perp/[slug]/page.tsx b/src/app/perp/[slug]/page.tsx index bbcc982a..e0f2b2b1 100644 --- a/src/app/perp/[slug]/page.tsx +++ b/src/app/perp/[slug]/page.tsx @@ -18,7 +18,7 @@ import { PerpBarChart } from "@/components/perp-bar-chart"; import { logoPath } from "@/lib/logo-manifest"; import { buildBreadcrumbJsonLd, safeJsonLd } from "@/lib/jsonld"; -export const revalidate = 60; +export const dynamic = "force-dynamic"; export const maxDuration = 60; type Params = { slug: string };