From 18571f9c0877f23456c271531aa31e0c1426dcae Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Fri, 7 Aug 2026 21:40:08 +0200 Subject: [PATCH 01/26] refactor(keeper): consume unified order aggregates Use the published struct-returning venue ABIs so predictive snapshots no longer depend on the removed value-only compatibility getter. Co-authored-by: Cursor --- keeper/package.json | 4 ++-- keeper/pnpm-lock.yaml | 20 ++++++++-------- keeper/src/predict/snapshot.ts | 24 +++++++++---------- keeper/tests/predict/coordinator.test.ts | 4 ++-- .../tests/predict/coordinatorAlerts.test.ts | 4 ++-- keeper/tests/predict/snapshot.test.ts | 11 ++++++--- keeper/tests/venues/reduceToTarget.test.ts | 2 +- portfolio-margin/src/types.ts | 6 ++--- 8 files changed, 40 insertions(+), 35 deletions(-) diff --git a/keeper/package.json b/keeper/package.json index ea42073..cd88e6a 100644 --- a/keeper/package.json +++ b/keeper/package.json @@ -24,8 +24,8 @@ "@hashpower/portfolio-margin": "github:Lumerin-protocol/collateral-margin#c34b4a360d6616d017b157a4a9e27e1a8e60079c&path:/portfolio-margin", "amaro": "^1.1.9", "collateral-margin-abi": "github:Lumerin-protocol/collateral-margin#c34b4a360d6616d017b157a4a9e27e1a8e60079c&path:/contracts/abi", - "derivatives-marketplace-abi": "github:Lumerin-protocol/derivatives-marketplace#f7e219f704646ab654a2a8d0286c0f477148e299&path:/contracts/abi", - "futures-marketplace-abi": "github:Lumerin-protocol/futures-marketplace#547ade13eb944b0e86bbef325749ed627cc886b4&path:/contracts/abi", + "derivatives-marketplace-abi": "github:Lumerin-protocol/derivatives-marketplace#8b7ed0f3572d0ea8039a11757b7c1b963be75535&path:/contracts/abi", + "futures-marketplace-abi": "github:Lumerin-protocol/futures-marketplace#05e97deb86a59e6be45fa0aa2ac10b8cb070c303&path:/contracts/abi", "pino": "^10.3.1", "viem": "^2.48.8" }, diff --git a/keeper/pnpm-lock.yaml b/keeper/pnpm-lock.yaml index d96ef49..def7113 100644 --- a/keeper/pnpm-lock.yaml +++ b/keeper/pnpm-lock.yaml @@ -18,11 +18,11 @@ importers: specifier: github:Lumerin-protocol/collateral-margin#c34b4a360d6616d017b157a4a9e27e1a8e60079c&path:/contracts/abi version: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/contracts/abi derivatives-marketplace-abi: - specifier: github:Lumerin-protocol/derivatives-marketplace#f7e219f704646ab654a2a8d0286c0f477148e299&path:/contracts/abi - version: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299#path:/contracts/abi + specifier: github:Lumerin-protocol/derivatives-marketplace#8b7ed0f3572d0ea8039a11757b7c1b963be75535&path:/contracts/abi + version: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi futures-marketplace-abi: - specifier: github:Lumerin-protocol/futures-marketplace#547ade13eb944b0e86bbef325749ed627cc886b4&path:/contracts/abi - version: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts/abi + specifier: github:Lumerin-protocol/futures-marketplace#05e97deb86a59e6be45fa0aa2ac10b8cb070c303&path:/contracts/abi + version: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi pino: specifier: ^10.3.1 version: 10.3.1 @@ -213,8 +213,8 @@ packages: dateformat@4.6.3: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} - derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299#path:/contracts/abi: - resolution: {gitHosted: true, integrity: sha512-WoZc9oXcWv0waw6+iZLtWfVyG7mZ6ngTWE5YOi0Nu8+dZ9rsnetk+044w3BHRNMqp0TvX3YuMHMSf6TAIaxtdw==, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299} + derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi: + resolution: {gitHosted: true, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535} version: 0.0.0 end-of-stream@1.4.5: @@ -229,8 +229,8 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts/abi: - resolution: {gitHosted: true, integrity: sha512-LQiUPSYCFvY86dYZEk61cpMWwy3VvhAaYgnn/NUdY+RoifWHKV3PIrpx0Am9x27U840+KyMhkdOMDoGjPJq9MQ==, tarball: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4} + futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi: + resolution: {gitHosted: true, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303} version: 0.0.0 help-me@5.0.0: @@ -452,7 +452,7 @@ snapshots: dateformat@4.6.3: {} - derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299#path:/contracts/abi: {} + derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi: {} end-of-stream@1.4.5: dependencies: @@ -464,7 +464,7 @@ snapshots: fast-safe-stringify@2.1.1: {} - futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts/abi: {} + futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi: {} help-me@5.0.0: {} diff --git a/keeper/src/predict/snapshot.ts b/keeper/src/predict/snapshot.ts index 718e9ad..db630ca 100644 --- a/keeper/src/predict/snapshot.ts +++ b/keeper/src/predict/snapshot.ts @@ -63,7 +63,7 @@ export async function readMMParams( * Read everything needed to evaluate `mmSurplus(P)` for a single user as a * function of price. Two RPC round-trips: * - * 1. Bulk multicall: balance, both venues' `getRiskView` / `getOrderValues`, + * 1. Bulk multicall: balance, both venues' `getRiskView` / `getOrderAggregate`, * the perp position, futures activeExpirationAts. * 2. Per-expiry multicall: hydrate each aggregate via `getUserPosition`, plus * its `settlementPrice` — an expiry that has settled but not yet been swept @@ -75,7 +75,7 @@ export async function readMMParams( * * `getRiskView` carries the per-side order delta but reports fill loss only at the * current mark, and the clamp makes that non-invertible once it reads zero — so the - * per-side limit-price totals come from `getOrderValues` and the predictor derives + * per-side limit-price totals come from `getOrderAggregate` and the predictor derives * fill loss at whatever price it is evaluating. Pending funding also rides in * `getRiskView`, replacing the separate `getPendingFunding` read. */ @@ -88,9 +88,9 @@ export async function readAccountSnapshot( balance, perpPosition, perpRisk, - perpOrderValues, + perpOrderAggregate, futuresRisk, - futuresOrderValues, + futuresOrderAggregate, activeExpirationAts, ] = await chain.publicClient.multicall({ contracts: [ @@ -115,7 +115,7 @@ export async function readAccountSnapshot( { address: config.perps.address, abi: HashPowerPerpsDEXAbi, - functionName: "getOrderValues" as const, + functionName: "getOrderAggregate" as const, args: [user] as const, }, { @@ -127,7 +127,7 @@ export async function readAccountSnapshot( { address: config.futures.address, abi: FuturesAbi, - functionName: "getOrderValues" as const, + functionName: "getOrderAggregate" as const, args: [user] as const, }, { @@ -182,26 +182,26 @@ export async function readAccountSnapshot( perp: { netQty: perpPosition.netQuantity, entryPrice: perpPosition.aggregatedEntryPrice, - orders: restingOrders(perpRisk, perpOrderValues), + orders: restingOrders(perpRisk, perpOrderAggregate), // PME uses `max(0, pendingFunding)` — only what the user owes. fundingOwed: funding > 0n ? funding : 0n, }, futures: { positions: futuresPositions, - orders: restingOrders(futuresRisk, futuresOrderValues), + orders: restingOrders(futuresRisk, futuresOrderAggregate), }, }; } -/** Pair a venue's `getRiskView` deltas with its `getOrderValues` limit-price totals. */ +/** Pair a venue's risk deltas with its cached order aggregate. */ function restingOrders( risk: { buyOrderDelta: bigint; sellOrderDelta: bigint }, - values: readonly [bigint, bigint], + aggregate: { buyValue: bigint; sellValue: bigint }, ): AccountSnapshot["perp"]["orders"] { return { buyDelta: risk.buyOrderDelta, sellDelta: risk.sellOrderDelta, - buyValue: values[0], - sellValue: values[1], + buyValue: aggregate.buyValue, + sellValue: aggregate.sellValue, }; } diff --git a/keeper/tests/predict/coordinator.test.ts b/keeper/tests/predict/coordinator.test.ts index be7f4f3..53c0a63 100644 --- a/keeper/tests/predict/coordinator.test.ts +++ b/keeper/tests/predict/coordinator.test.ts @@ -129,8 +129,8 @@ function buildHarness({ buyOrderFillLoss: 0n, sellOrderFillLoss: 0n, }; - case "getOrderValues": - return [0n, 0n]; + case "getOrderAggregate": + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; case "getActiveExpirationDates": return []; case "computePortfolioIM": diff --git a/keeper/tests/predict/coordinatorAlerts.test.ts b/keeper/tests/predict/coordinatorAlerts.test.ts index bfaad08..10529c8 100644 --- a/keeper/tests/predict/coordinatorAlerts.test.ts +++ b/keeper/tests/predict/coordinatorAlerts.test.ts @@ -104,8 +104,8 @@ function buildHarness({ balance, perpEntry }: { balance: bigint; perpEntry: bigi buyOrderFillLoss: 0n, sellOrderFillLoss: 0n, }; - case "getOrderValues": - return [0n, 0n]; + case "getOrderAggregate": + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; case "getActiveExpirationDates": return []; case "computePortfolioIM": diff --git a/keeper/tests/predict/snapshot.test.ts b/keeper/tests/predict/snapshot.test.ts index 243fc6a..efe87df 100644 --- a/keeper/tests/predict/snapshot.test.ts +++ b/keeper/tests/predict/snapshot.test.ts @@ -90,10 +90,15 @@ function makeChain(scripted: { sellOrderFillLoss: 0n, }; } - case "getOrderValues": { + case "getOrderAggregate": { const orders = (c.address === PERPS ? scripted.perpOrders : scripted.futuresOrders) ?? NO_ORDERS; - return [orders.buyValue, orders.sellValue]; + return { + buyQty: 0n, + sellQty: 0n, + buyValue: orders.buyValue, + sellValue: orders.sellValue, + }; } case "getActiveExpirationDates": return scripted.activeExpirationAts ?? []; @@ -137,7 +142,7 @@ describe("predict/snapshot: readAccountSnapshot", () => { assert.deepEqual(snap.futures.orders, NO_ORDERS); }); - it("pairs each venue's getRiskView deltas with its getOrderValues totals", async () => { + it("pairs each venue's risk deltas with its order aggregate totals", async () => { const perpOrders: RestingOrders = { buyDelta: 2_000_000n, sellDelta: 500_000n, diff --git a/keeper/tests/venues/reduceToTarget.test.ts b/keeper/tests/venues/reduceToTarget.test.ts index bc70084..911cac4 100644 --- a/keeper/tests/venues/reduceToTarget.test.ts +++ b/keeper/tests/venues/reduceToTarget.test.ts @@ -31,7 +31,7 @@ const EMPTY_RISK_VIEW = { /** * The bulk read `readAccountSnapshot` issues, in order: balance, the perp position, - * then each venue's `getRiskView` / `getOrderValues` pair, then the active futures + * then each venue's `getRiskView` / `getOrderAggregate` pair, then the active futures * expiries. Only the expiry list varies between these cases. */ function snapshotMulticall(balance: bigint, expiries: readonly bigint[]) { diff --git a/portfolio-margin/src/types.ts b/portfolio-margin/src/types.ts index 472e406..15016a2 100644 --- a/portfolio-margin/src/types.ts +++ b/portfolio-margin/src/types.ts @@ -8,7 +8,7 @@ export type Address = `0x${string}`; /** * A venue's resting book reduced to what the margin math needs, as reported by - * `ILinearMarket.getRiskView` plus the venue's `getOrderValues`. + * `ILinearMarket.getRiskView` plus the venue's `getOrderAggregate`. * * Nothing here is constant in P. The engine stresses order delta as part of net * delta, and the fill-loss terms are `max(0, value − P × delta / 10^tokenDecimals)` @@ -37,8 +37,8 @@ export interface RestingOrders { * predictor can re-evaluate at any new price without further RPC reads. * * Shapes deliberately mirror the on-chain getters: - * - perps: `getRiskView` + `getOrderValues` + `getUserPosition` - * - futures: `getRiskView` + `getOrderValues` + `getActiveExpirationDates`/`getUserPosition` + * - perps: `getRiskView` + `getOrderAggregate` + `getUserPosition` + * - futures: `getRiskView` + `getOrderAggregate` + `getActiveExpirationDates`/`getUserPosition` * * Bigints throughout because PME math is performed in token-decimal units * (typically USDC = 6 decimals) with intermediate WAD scaling. JS numbers From 4f1a1c531f59c83d08c86a4caaaa4f80e436dcbc Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Fri, 7 Aug 2026 23:40:20 +0200 Subject: [PATCH 02/26] refactor: use typed contract batch methods Route keeper maintenance and settlement through dedicated contract methods and remove unused market-maker multicall wrappers. Co-authored-by: Cursor --- keeper/src/config.ts | 8 +- keeper/src/delivery/coordinator.ts | 38 ++--- keeper/src/runtime/outdatedOrderSweeper.ts | 114 ++++----------- keeper/tests/delivery/coordinator.test.ts | 21 ++- keeper/tests/integration/buildKeeper.ts | 2 +- .../runtime/outdatedOrderSweeper.test.ts | 133 ++---------------- market-maker/src/adapters/futures/venue.ts | 30 ---- market-maker/src/adapters/perps/venue.ts | 30 ---- market-maker/src/core/adapter.ts | 11 +- market-maker/tests/core/errSerializer.test.ts | 4 +- 10 files changed, 78 insertions(+), 313 deletions(-) diff --git a/keeper/src/config.ts b/keeper/src/config.ts index 71eb100..520afcc 100644 --- a/keeper/src/config.ts +++ b/keeper/src/config.ts @@ -177,8 +177,8 @@ export interface Config { */ sweepIntervalMs: number; /** - * Maximum number of `removeOutdatedOrder` calls bundled into a - * single `Futures.multicall(bytes[])` tx. Each call is roughly + * Maximum number of ids passed to one + * `Futures.removeOutdatedOrders(bytes32[])` tx. Each cleanup is roughly * 50-80k gas (one `_closeOrder` traversal); 50 keeps us well under * Base's 30M block-gas limit (~4M worst case). Larger user-side * fan-outs split across multiple sequential txs. @@ -221,8 +221,8 @@ export interface Config { */ bootstrapUsers: readonly Address[]; /** - * Maximum number of `settlePosition` calls bundled into a single - * `Futures.multicall(bytes[])` transaction. Trades a single nonce per + * Maximum number of position pairs passed to a single + * `Futures.settlePositions(address[],uint256[])` transaction. Trades a single nonce per * sweep tick (no replacement-underpriced races) for one bigger tx. * Capped to keep gas usage well under the block limit — Base has 30M * block gas, each `settlePosition` is roughly 200-300k gas, so 50 is diff --git a/keeper/src/delivery/coordinator.ts b/keeper/src/delivery/coordinator.ts index ceaa3ae..bba247b 100644 --- a/keeper/src/delivery/coordinator.ts +++ b/keeper/src/delivery/coordinator.ts @@ -1,7 +1,6 @@ import { BaseError, ContractFunctionRevertedError, - encodeFunctionData, getAddress, type Address, type Hex, @@ -431,31 +430,14 @@ export class DeliveryCoordinator { if (this.config.keeper.dryRun) { this.logger.info( { batchSize: settleable.length }, - "[dryRun] would call Futures.multicall(settlePosition × N)", + "[dryRun] would call Futures.settlePositions", ); for (const pos of settleable) this.dropTracked(pos.user, pos.expirationAt); return; } - const calldatas: Hex[] = []; - const encodable: TrackedPosition[] = []; - for (const pos of settleable) { - try { - const data = encodeFunctionData({ - abi: FuturesAbi, - functionName: "settlePosition", - args: [pos.user, pos.expirationAt], - }); - calldatas.push(data); - encodable.push(pos); - } catch (err) { - this.logger.error( - { err, user: pos.user, expirationAt: pos.expirationAt.toString() }, - "delivery: encodeFunctionData threw — dropping malformed entry from batch", - ); - } - } - if (calldatas.length === 0) return; + const users = settleable.map((pos) => pos.user); + const expirationAts = settleable.map((pos) => pos.expirationAt); type WriteParams = Parameters< typeof this.chain.walletClient.writeContract @@ -466,8 +448,8 @@ export class DeliveryCoordinator { this.chain.walletClient.writeContract({ address: this.config.futures.address, abi: FuturesAbi, - functionName: "multicall", - args: [calldatas], + functionName: "settlePositions", + args: [users, expirationAts], account: this.chain.account, chain: this.chain.walletClient.chain ?? null, } as unknown as WriteParams), @@ -481,10 +463,10 @@ export class DeliveryCoordinator { return; } this.logger.warn( - { err, batchSize: encodable.length }, + { err, batchSize: settleable.length }, "delivery batch: write reverted — falling back to per-position retries", ); - for (const pos of encodable) { + for (const pos of settleable) { try { await this.attemptSettle(pos); } catch (innerErr) { @@ -505,13 +487,13 @@ export class DeliveryCoordinator { { hash, blockNumber: receipt.blockNumber.toString(), - batchSize: encodable.length, + batchSize: settleable.length, ...formatGasCost(receipt, this.ethUsdFeed), }, - "delivery batch: multicall confirmed", + "delivery batch: settlePositions confirmed", ); - for (const pos of encodable) { + for (const pos of settleable) { this.dropTracked(pos.user, pos.expirationAt); } } diff --git a/keeper/src/runtime/outdatedOrderSweeper.ts b/keeper/src/runtime/outdatedOrderSweeper.ts index 8043fe2..126060c 100644 --- a/keeper/src/runtime/outdatedOrderSweeper.ts +++ b/keeper/src/runtime/outdatedOrderSweeper.ts @@ -1,10 +1,4 @@ -import { - BaseError, - ContractFunctionRevertedError, - encodeFunctionData, - type Address, - type Hex, -} from "viem"; +import { type Address, type Hex } from "viem"; import type pino from "pino"; import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; import { withUnstickRetry } from "../tx/unstick.ts"; @@ -39,20 +33,26 @@ import type { ParticipantTracker } from "../discovery/tracker.ts"; * tick → for each tracked user: * 1. readContract `getUserOrders(user)` — empty? skip * 2. multicall `getOrder(id)` for each id → filter expired - * 3. one `Futures.multicall([removeOutdatedOrder(id1), ...])` write + * 3. one `Futures.removeOutdatedOrders([id1, ...])` write * (capped at `outdatedOrders.maxBatchSize`; larger user-side * fan-outs are split into N batches, each its own tx). * - * Recoverable reverts during simulation (`OrderNotExists`, `OrderNotExpired`) - * just drop the id from the batch — they happen when an id closed between - * our read and our write (user cancel, match, prior keeper instance won the - * race). We log them at debug because they're entirely benign. + * The typed batch skips stale and not-yet-expired ids on-chain, so a user + * cancellation or competing keeper cannot revert unrelated cleanup work. * * Non-futures venues (perps) don't have order expiry so this module is * Futures-only by design. */ -const RECOVERABLE_REVERTS = new Set(["OrderNotExists", "OrderNotExpired"]); +const FUTURES_REMOVE_OUTDATED_ORDERS_ABI = [ + { + type: "function", + name: "removeOutdatedOrders", + stateMutability: "nonpayable", + inputs: [{ name: "_orderIds", type: "bytes32[]" }], + outputs: [{ name: "removed", type: "uint256" }], + }, +] as const; interface ExpiredOrder { user: Address; @@ -251,72 +251,21 @@ export class OutdatedOrderSweeper { } /** - * Simulates each `removeOutdatedOrder(id)` to filter stale entries - * (`OrderNotExists` / `OrderNotExpired` — usually a race against a user - * cancel or a prior keeper run), then encodes the survivors into one - * `Futures.multicall(bytes[])` write. - * - * Returns the number of orders actually broadcast for closure (zero on - * dry-run or empty-batch-after-filter — both are normal). Throws only on - * unexpected reverts during the write phase; transient RPC failures are - * caught and logged so the next sweep retries. + * Sends one race-tolerant `removeOutdatedOrders(ids)` write. The contract + * skips stale/live ids and preserves every valid cleanup in the batch. */ private async closeBatch(batch: readonly ExpiredOrder[]): Promise { - type SimParams = Parameters< - typeof this.chain.publicClient.simulateContract - >[0]; - - const simResults = await Promise.allSettled( - batch.map((entry) => - this.chain.publicClient.simulateContract({ - address: this.config.futures.address, - abi: FuturesAbi, - functionName: "removeOutdatedOrder", - args: [entry.orderId], - account: this.chain.account, - } as unknown as SimParams), - ), - ); - - const survivors: ExpiredOrder[] = []; - for (let i = 0; i < batch.length; i++) { - const entry = batch[i] as ExpiredOrder; - const r = simResults[i] as PromiseSettledResult; - if (r.status === "fulfilled") { - survivors.push(entry); - continue; - } - const decoded = decodeRecoverableRevert(r.reason); - if (decoded !== undefined) { - this.logger.debug( - { orderId: entry.orderId, user: entry.user, revert: decoded }, - "skipping stale candidate (state moved between read and simulate)", - ); - continue; - } - this.logger.warn( - { err: r.reason, orderId: entry.orderId, user: entry.user }, - "simulate failed with non-recoverable error — dropping from batch", - ); - } - - if (survivors.length === 0) return 0; + if (batch.length === 0) return 0; if (this.config.keeper.dryRun) { this.logger.info( - { batchSize: survivors.length }, - "[dryRun] would call Futures.multicall(removeOutdatedOrder × N)", + { batchSize: batch.length }, + "[dryRun] would call Futures.removeOutdatedOrders", ); return 0; } - const calldatas: Hex[] = survivors.map((entry) => - encodeFunctionData({ - abi: FuturesAbi, - functionName: "removeOutdatedOrder", - args: [entry.orderId], - }), - ); + const orderIds = batch.map((entry) => entry.orderId); type WriteParams = Parameters< typeof this.chain.walletClient.writeContract @@ -330,9 +279,9 @@ export class OutdatedOrderSweeper { hash = await withUnstickRetry(this.chain, this.logger, () => this.chain.walletClient.writeContract({ address: this.config.futures.address, - abi: FuturesAbi, - functionName: "multicall", - args: [calldatas], + abi: FUTURES_REMOVE_OUTDATED_ORDERS_ABI, + functionName: "removeOutdatedOrders", + args: [orderIds], account: this.chain.account, chain: this.chain.walletClient.chain ?? null, } as unknown as WriteParams), @@ -342,7 +291,7 @@ export class OutdatedOrderSweeper { // want unhandled rejection on the setInterval-fired path to crash // the keeper, so always swallow and log. this.logger.warn( - { err, batchSize: survivors.length }, + { err, batchSize: batch.length }, "tx submission failed — sweep will retry", ); return 0; @@ -356,22 +305,11 @@ export class OutdatedOrderSweeper { { hash, blockNumber: receipt.blockNumber.toString(), - batchSize: survivors.length, + batchSize: batch.length, ...formatGasCost(receipt, this.ethUsdFeed), }, - "multicall(removeOutdatedOrder × N) confirmed", + "removeOutdatedOrders confirmed", ); - return survivors.length; + return batch.length; } } - -function decodeRecoverableRevert(err: unknown): string | undefined { - if (!(err instanceof BaseError)) return undefined; - const revert = err.walk((e) => e instanceof ContractFunctionRevertedError); - if (!(revert instanceof ContractFunctionRevertedError)) return undefined; - const name = revert.data?.errorName; - if (typeof name !== "string") return undefined; - return RECOVERABLE_REVERTS.has(name) ? name : undefined; -} - -export const __testing = { decodeRecoverableRevert }; diff --git a/keeper/tests/delivery/coordinator.test.ts b/keeper/tests/delivery/coordinator.test.ts index 97f5890..3e447f8 100644 --- a/keeper/tests/delivery/coordinator.test.ts +++ b/keeper/tests/delivery/coordinator.test.ts @@ -95,6 +95,7 @@ interface ChainStubOptions { activeDatesByUser?: Record; positionsByUserDate?: Record; readContractError?: (functionName: string) => Error | undefined; + writes?: Array<{ functionName: string; args: readonly unknown[] }>; } function posKey(user: Address, expirationAt: bigint): string { @@ -176,7 +177,16 @@ function makeChain(opts: ChainStubOptions = {}): Chain { }, walletClient: { chain: null, - writeContract: async () => writeHash, + writeContract: async ({ + functionName, + args, + }: { + functionName: string; + args: readonly unknown[]; + }) => { + opts.writes?.push({ functionName, args }); + return writeHash; + }, }, } as unknown as Chain; } @@ -285,6 +295,7 @@ describe("delivery/coordinator: bootstrap + settle", () => { it("settleBatch simulates settlePosition(user, expirationAt) and drops on success", async () => { const simulated: unknown[][] = []; + const writes: Array<{ functionName: string; args: readonly unknown[] }> = []; const chain = makeChain({ // Far-future timestamp so bootstrap's trailing sweep is a no-op. blockTimestamp: 1n, @@ -296,6 +307,7 @@ describe("delivery/coordinator: bootstrap + settle", () => { positionsByUserDate: { [posKey(USER_A, DELIVERY_A)]: { netQuantity: 1n, netEntryValue: 50n }, }, + writes, }); const coord = new DeliveryCoordinator(chain, makeConfig({ settleDelayMs: 0 }), silentLogger); await coord.bootstrapFromUsers([USER_A]); @@ -307,6 +319,13 @@ describe("delivery/coordinator: bootstrap + settle", () => { USER_A.toLowerCase(), ); assert.equal(simulated[0]?.[1], DELIVERY_A); + assert.equal(writes.length, 1); + assert.equal(writes[0]?.functionName, "settlePositions"); + assert.equal( + ((writes[0]?.args[0] as Address[])[0] as string).toLowerCase(), + USER_A.toLowerCase(), + ); + assert.deepEqual(writes[0]?.args[1], [DELIVERY_A]); assert.equal(coord.has(USER_A, DELIVERY_A), false); }); diff --git a/keeper/tests/integration/buildKeeper.ts b/keeper/tests/integration/buildKeeper.ts index 2da307e..ae84766 100644 --- a/keeper/tests/integration/buildKeeper.ts +++ b/keeper/tests/integration/buildKeeper.ts @@ -86,7 +86,7 @@ export interface BuildKeeperOverrides { */ deliveryBootstrapUsers?: readonly Address[]; /** - * Maximum settlePosition calls bundled into one Futures.multicall tx by + * Maximum position pairs passed to one Futures.settlePositions tx by * the delivery coordinator. Defaults to 50 for parity with production. * Override to a small value to assert batching behaviour explicitly * (e.g. set to 1 to force per-id calls, or 2 to assert chunked sweeps). diff --git a/keeper/tests/runtime/outdatedOrderSweeper.test.ts b/keeper/tests/runtime/outdatedOrderSweeper.test.ts index ebdb98d..a051584 100644 --- a/keeper/tests/runtime/outdatedOrderSweeper.test.ts +++ b/keeper/tests/runtime/outdatedOrderSweeper.test.ts @@ -1,14 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { - BaseError, - ContractFunctionRevertedError, - decodeFunctionData, - type Address, - type Hex, -} from "viem"; +import { type Address, type Hex } from "viem"; import pino from "pino"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; import { OutdatedOrderSweeper } from "../../src/runtime/outdatedOrderSweeper.ts"; import type { Chain } from "../../src/chain.ts"; import type { Config } from "../../src/config.ts"; @@ -60,22 +53,18 @@ interface FakeChainOpts { blockTimestamp: bigint; orderIdsByUser: Map; orders: Map; - /** Simulate failure: returns an `errorName` for the given orderId, else undefined. */ - simulateRevert?: (orderId: Hex) => string | undefined; } interface Recorded { readContractCalls: number; multicallReadCalls: number; - simulateCalls: Hex[]; - writeCalls: Array<{ functionName: string; calldatas: Hex[] }>; + writeCalls: Array<{ functionName: string; orderIds: Hex[] }>; } function makeChain(opts: FakeChainOpts): { chain: Chain; recorded: Recorded } { const recorded: Recorded = { readContractCalls: 0, multicallReadCalls: 0, - simulateCalls: [], writeCalls: [], }; @@ -121,24 +110,6 @@ function makeChain(opts: FakeChainOpts): { chain: Chain; recorded: Recorded } { }; }); }, - simulateContract: async ({ - functionName, - args, - }: { - functionName: string; - args: unknown[]; - }) => { - if (functionName !== "removeOutdatedOrder") { - throw new Error(`unexpected simulate fn: ${functionName}`); - } - const orderId = args[0] as Hex; - recorded.simulateCalls.push(orderId); - const errorName = opts.simulateRevert?.(orderId); - if (errorName !== undefined) { - throw new MockRevertError(errorName); - } - return { request: { functionName, args } }; - }, waitForTransactionReceipt: async () => ({ blockNumber: 1n, gasUsed: 200_000n, @@ -155,10 +126,10 @@ function makeChain(opts: FakeChainOpts): { chain: Chain; recorded: Recorded } { functionName: string; args: unknown[]; }) => { - if (functionName !== "multicall") { + if (functionName !== "removeOutdatedOrders") { throw new Error(`unexpected write fn: ${functionName}`); } - recorded.writeCalls.push({ functionName, calldatas: args[0] as Hex[] }); + recorded.writeCalls.push({ functionName, orderIds: args[0] as Hex[] }); return "0xabc" as Hex; }, }; @@ -171,20 +142,6 @@ function makeChain(opts: FakeChainOpts): { chain: Chain; recorded: Recorded } { return { chain, recorded }; } -/** Mimics a viem ContractFunctionRevertedError so `BaseError.walk` finds it. */ -class MockRevertError extends BaseError { - override name = "ContractFunctionExecutionError"; - constructor(errorName: string) { - const inner = new ContractFunctionRevertedError({ - abi: FuturesAbi, - data: undefined, - functionName: "removeOutdatedOrder", - }); - (inner as unknown as { data: { errorName: string } }).data = { errorName }; - super("simulated revert", { cause: inner }); - } -} - function makeConfig(overrides: Partial = {}): Config { return { futures: { address: FUTURES }, @@ -260,11 +217,10 @@ describe("OutdatedOrderSweeper", () => { ); const closed = await sweeper.runSweep(); assert.equal(closed, 0); - assert.equal(recorded.simulateCalls.length, 0); assert.equal(recorded.writeCalls.length, 0); }); - it("batches all expired orders for a user into a single multicall write", async () => { + it("batches all expired orders for a user into one typed write", async () => { const { logger, calls } = makeRecordingLogger(); const id1 = ("0x" + "11".repeat(32)) as Hex; const id2 = ("0x" + "22".repeat(32)) as Hex; @@ -289,21 +245,12 @@ describe("OutdatedOrderSweeper", () => { assert.equal(closed, 2); assert.equal(recorded.writeCalls.length, 1); - assert.deepEqual(recorded.simulateCalls, [id1, id2]); - - // Decode each calldata to confirm both are `removeOutdatedOrder()`. - const decoded = recorded.writeCalls[0]!.calldatas.map((cd) => - decodeFunctionData({ abi: FuturesAbi, data: cd }), - ); - assert.equal(decoded.length, 2); - assert.equal(decoded[0]?.functionName, "removeOutdatedOrder"); - assert.equal(decoded[0]?.args?.[0], id1); - assert.equal(decoded[1]?.functionName, "removeOutdatedOrder"); - assert.equal(decoded[1]?.args?.[0], id2); + assert.equal(recorded.writeCalls[0]!.functionName, "removeOutdatedOrders"); + assert.deepEqual(recorded.writeCalls[0]!.orderIds, [id1, id2]); assert.ok( calls.some((c) => c.level === "info" && c.msg.includes("confirmed")), - "expected an INFO log when the multicall write confirms", + "expected an INFO log when the batch write confirms", ); }); @@ -333,9 +280,9 @@ describe("OutdatedOrderSweeper", () => { assert.equal( recorded.writeCalls.length, 1, - "one multicall write for cross-user batch", + "one typed write for cross-user batch", ); - assert.equal(recorded.writeCalls[0]!.calldatas.length, 2); + assert.deepEqual(recorded.writeCalls[0]!.orderIds, [idA, idB]); }); it("splits across multiple writes when batch size cap is exceeded", async () => { @@ -363,54 +310,12 @@ describe("OutdatedOrderSweeper", () => { // 5 expired / batch of 2 → ceil(5/2) = 3 writes assert.equal(recorded.writeCalls.length, 3); assert.deepEqual( - recorded.writeCalls.map((c) => c.calldatas.length), + recorded.writeCalls.map((c) => c.orderIds.length), [2, 2, 1], ); }); - it("drops stale-state candidates flagged by simulate (OrderNotExists / OrderNotExpired)", async () => { - // Race scenario: between our `getOrder` read and our simulate, the - // user (or a concurrent keeper) closed orderId1, and orderId2 had its - // expirationAt bumped. The sweeper must skip them silently and still - // broadcast a write for the survivor (orderId3). - const { logger, calls } = makeRecordingLogger(); - const id1 = ("0x" + "11".repeat(32)) as Hex; - const id2 = ("0x" + "22".repeat(32)) as Hex; - const id3 = ("0x" + "33".repeat(32)) as Hex; - const { chain, recorded } = makeChain({ - blockTimestamp: 10_000n, - orderIdsByUser: new Map([[USER_A, [id1, id2, id3]]]), - orders: new Map([ - [id1, { participant: USER_A, expirationAt: 5_000n }], - [id2, { participant: USER_A, expirationAt: 6_000n }], - [id3, { participant: USER_A, expirationAt: 7_000n }], - ]), - simulateRevert: (id) => { - if (id === id1) return "OrderNotExists"; - if (id === id2) return "OrderNotExpired"; - return undefined; - }, - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 1); - assert.equal(recorded.writeCalls.length, 1); - assert.equal(recorded.writeCalls[0]!.calldatas.length, 1); - // Stale-state skips are debug — they're benign and shouldn't pollute INFO. - assert.equal( - calls.filter( - (c) => c.level === "warn" && c.msg.includes("non-recoverable"), - ).length, - 0, - ); - }); - - it("skips the write entirely on dry-run but still simulates", async () => { + it("skips the write entirely on dry-run", async () => { const { logger, calls } = makeRecordingLogger(); const id1 = ("0x" + "11".repeat(32)) as Hex; const { chain, recorded } = makeChain({ @@ -429,11 +334,6 @@ describe("OutdatedOrderSweeper", () => { const closed = await sweeper.runSweep(); assert.equal(closed, 0); assert.equal(recorded.writeCalls.length, 0); - assert.equal( - recorded.simulateCalls.length, - 1, - "simulate runs so dry-run still surfaces reverts", - ); assert.ok(calls.some((c) => c.msg.startsWith("[dryRun]"))); }); @@ -451,7 +351,6 @@ describe("OutdatedOrderSweeper", () => { const recorded: Recorded = { readContractCalls: 0, multicallReadCalls: 0, - simulateCalls: [], writeCalls: [], }; const publicClient = { @@ -479,10 +378,6 @@ describe("OutdatedOrderSweeper", () => { }; }); }, - simulateContract: async ({ args }: { args: unknown[] }) => { - recorded.simulateCalls.push(args[0] as Hex); - return { request: {} }; - }, waitForTransactionReceipt: async () => ({ blockNumber: 1n, gasUsed: 0n, @@ -493,8 +388,8 @@ describe("OutdatedOrderSweeper", () => { chain: null, writeContract: async ({ args }: { args: unknown[] }) => { recorded.writeCalls.push({ - functionName: "multicall", - calldatas: args[0] as Hex[], + functionName: "removeOutdatedOrders", + orderIds: args[0] as Hex[], }); return "0xabc" as Hex; }, diff --git a/market-maker/src/adapters/futures/venue.ts b/market-maker/src/adapters/futures/venue.ts index f232b50..ec4c86a 100644 --- a/market-maker/src/adapters/futures/venue.ts +++ b/market-maker/src/adapters/futures/venue.ts @@ -260,36 +260,6 @@ export class FuturesVenueAdapter implements VenueAdapter { } } - /** @deprecated Prefer {@link sendCall} with a single `updateOrders` encoding. */ - async multicall( - calls: `0x${string}`[], - opts: { maxFeePerGas?: bigint; nonce?: number } = {}, - ): Promise<`0x${string}`> { - try { - return await this.wallet.walletClient.writeContract({ - address: this.address, - abi: FuturesAbi, - functionName: "multicall", - args: [calls], - account: this.wallet.account, - chain: this.chain, - maxFeePerGas: opts.maxFeePerGas, - nonce: opts.nonce, - }); - } catch (err) { - throw attachTenderlyUrl(err, { - chainId: this.chain.id, - from: this.wallet.account.address, - to: this.address, - data: encodeFunctionData({ - abi: FuturesAbi, - functionName: "multicall", - args: [calls], - }), - }); - } - } - // ── Internal helpers ──────────────────────────────────────────────────── async resolveAddresses(): Promise<{ diff --git a/market-maker/src/adapters/perps/venue.ts b/market-maker/src/adapters/perps/venue.ts index c1586f3..60873ab 100644 --- a/market-maker/src/adapters/perps/venue.ts +++ b/market-maker/src/adapters/perps/venue.ts @@ -168,36 +168,6 @@ export class PerpsVenueAdapter implements VenueAdapter { } } - /** @deprecated Prefer {@link sendCall} with a single `updateOrders` encoding. */ - async multicall( - calls: `0x${string}`[], - opts: { maxFeePerGas?: bigint; nonce?: number } = {}, - ): Promise<`0x${string}`> { - try { - return await this.wallet.walletClient.writeContract({ - address: this.address, - abi: HashPowerPerpsDEXAbi, - functionName: "multicall", - args: [calls], - account: this.wallet.account, - chain: this.chain, - maxFeePerGas: opts.maxFeePerGas, - nonce: opts.nonce, - }); - } catch (err) { - throw attachTenderlyUrl(err, { - chainId: this.chain.id, - from: this.wallet.account.address, - to: this.address, - data: encodeFunctionData({ - abi: HashPowerPerpsDEXAbi, - functionName: "multicall", - args: [calls], - }), - }); - } - } - // ── Internal helpers used by the collateral account & instrument ───────── async resolveAddresses(): Promise<{ diff --git a/market-maker/src/core/adapter.ts b/market-maker/src/core/adapter.ts index 70f7c27..71bfb68 100644 --- a/market-maker/src/core/adapter.ts +++ b/market-maker/src/core/adapter.ts @@ -385,7 +385,7 @@ export interface InstrumentAdapter { export type VenueKind = "perps" | "futures"; /** - * Per-venue interface. One per process; owns the wallet, the multicall route, + * Per-venue interface. One per process; owns the wallet, read-batching route, * the venue-events stream, and the collateral account. Single-instrument * venues (perps, futures) expose `getInstrument()` directly; a future * multi-instrument venue (options) would expose `listInstruments()` instead. @@ -427,13 +427,4 @@ export interface VenueAdapter { data: `0x${string}`, opts: { maxFeePerGas?: bigint; nonce?: number }, ): Promise<`0x${string}`>; - - /** - * @deprecated Prefer {@link sendCall} with a single `updateOrders` encoding. - * Multicall wrapping is no longer used by the portfolio coordinator. - */ - multicall( - calls: `0x${string}`[], - opts: { maxFeePerGas?: bigint; nonce?: number }, - ): Promise<`0x${string}`>; } diff --git a/market-maker/tests/core/errSerializer.test.ts b/market-maker/tests/core/errSerializer.test.ts index 58c6ebb..d2458d6 100644 --- a/market-maker/tests/core/errSerializer.test.ts +++ b/market-maker/tests/core/errSerializer.test.ts @@ -71,12 +71,12 @@ describe("serializeError", () => { const err = Object.assign(new Error("contract reverted"), { shortMessage: "reverted", contractAddress: "0xabc", - functionName: "multicall", + functionName: "updateOrders", sender: "0xdef", }); const out = serializeError(err); assert.equal(out.contractAddress, "0xabc"); - assert.equal(out.functionName, "multicall"); + assert.equal(out.functionName, "updateOrders"); assert.equal(out.sender, "0xdef"); }); From f807ff72fe7ad2f5e372a83a43e40e872907e3fc Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sat, 8 Aug 2026 00:46:46 +0200 Subject: [PATCH 03/26] refactor: retire expired-order sweeper Expired futures orders no longer consume global limits, so remove scheduled cleanup and scope market-maker reads to each delivery. Co-authored-by: Cursor --- keeper/src/config.ts | 23 - keeper/src/index.ts | 18 - keeper/src/runtime/outdatedOrderSweeper.ts | 315 ----------- keeper/tests/integration/buildKeeper.ts | 7 - keeper/tests/runtime/healthcheck.test.ts | 4 - .../runtime/outdatedOrderSweeper.test.ts | 510 ------------------ .../src/adapters/futures/ownOrders.ts | 28 +- 7 files changed, 19 insertions(+), 886 deletions(-) delete mode 100644 keeper/src/runtime/outdatedOrderSweeper.ts delete mode 100644 keeper/tests/runtime/outdatedOrderSweeper.test.ts diff --git a/keeper/src/config.ts b/keeper/src/config.ts index 520afcc..c3b214e 100644 --- a/keeper/src/config.ts +++ b/keeper/src/config.ts @@ -166,25 +166,6 @@ export interface Config { */ balanceCriticalWei: bigint; }; - outdatedOrders: { - /** - * Cadence of the futures expired-order sweep in ms. Default 5 min — - * expired orders aren't time-critical (they just pin a slot under - * `MAX_ORDERS_PER_PARTICIPANT` and leave a dead level on the book), - * so we don't need the sub-minute cadence used by liquidations. Set - * to 0 to disable the sweep entirely (e.g. when another keeper is - * the designated cleaner). - */ - sweepIntervalMs: number; - /** - * Maximum number of ids passed to one - * `Futures.removeOutdatedOrders(bytes32[])` tx. Each cleanup is roughly - * 50-80k gas (one `_closeOrder` traversal); 50 keeps us well under - * Base's 30M block-gas limit (~4M worst case). Larger user-side - * fan-outs split across multiple sequential txs. - */ - maxBatchSize: number; - }; delivery: { /** * Opt-in: when true, the keeper permissionlessly calls @@ -412,10 +393,6 @@ export function loadConfig(): Config { balanceLowWei: BigInt(process.env.BALANCE_LOW_WEI ?? "10000000000000000"), balanceCriticalWei: BigInt(process.env.BALANCE_CRITICAL_WEI ?? "1000000000000000"), }, - outdatedOrders: { - sweepIntervalMs: Number(process.env.OUTDATED_ORDERS_SWEEP_INTERVAL_MS ?? "300000"), - maxBatchSize: Number(process.env.OUTDATED_ORDERS_MAX_BATCH_SIZE ?? "50"), - }, delivery: { enabled: process.env.DELIVERY_KEEPER_ENABLED === "true", sweepIntervalMs: Number(process.env.DELIVERY_SWEEP_INTERVAL_MS ?? "60000"), diff --git a/keeper/src/index.ts b/keeper/src/index.ts index 1ed6ebd..f3241c2 100644 --- a/keeper/src/index.ts +++ b/keeper/src/index.ts @@ -12,7 +12,6 @@ import { Notifier } from "./alert/notifier.ts"; import { Healthcheck } from "./runtime/healthcheck.ts"; import { Scheduler } from "./runtime/scheduler.ts"; import { BalanceMonitor } from "./runtime/balanceMonitor.ts"; -import { OutdatedOrderSweeper } from "./runtime/outdatedOrderSweeper.ts"; import { PerpsVenue } from "./venues/perps.ts"; import { FuturesVenue } from "./venues/futures.ts"; import { PriceFeed } from "./oracle/priceFeed.ts"; @@ -162,18 +161,6 @@ async function main(): Promise { // concern, not specific to any one venue. const balanceMonitor = new BalanceMonitor(chain, config, logger); - // Futures-specific maintenance: walks tracked participants and closes - // any of their orders past `expirationAt` via the permissionless - // `Futures.removeOutdatedOrder` entrypoint (Futures v2.11.0+ no longer - // auto-sweeps on `createOrder`). Cheap, off the hot path — see - // `runtime/outdatedOrderSweeper.ts` for the gas-trade reasoning. Set - // OUTDATED_ORDERS_SWEEP_INTERVAL_MS=0 to disable when another keeper - // owns this responsibility for the deployment. - const outdatedOrderSweeper = - config.outdatedOrders.sweepIntervalMs > 0 - ? new OutdatedOrderSweeper(chain, config, tracker, logger, ethUsdFeed) - : undefined; - // Newly-tracked users should not wait for the next sweep tick. Kicking the // executor wakes any idle workers so they can pick up the new user as soon // as the next sweep enriches the queue. (We can't enqueue here without an @@ -206,7 +193,6 @@ async function main(): Promise { priceFeed.stop(); balanceMonitor.stop(); ethUsdFeed?.stop(); - outdatedOrderSweeper?.stop(); deliveryCoordinator?.stop(); await executor.stop(); if (webhookIngester !== undefined) await webhookIngester.stop(); @@ -301,10 +287,6 @@ async function main(): Promise { await deliveryCoordinator.bootstrapFromUsers(seedUsers); } await scheduler.runSweep(); - // Start the expired-order sweeper after backfill so its eager first - // tick sees the populated tracker, not an empty one. Skipped entirely - // when `outdatedOrders.sweepIntervalMs` is 0 (operator opt-out). - if (outdatedOrderSweeper !== undefined) await outdatedOrderSweeper.start(); // Backfill fires `tracker.onAdded` for every existing user, which the // predictor consumes via `rebuild`. Those rebuilds are fire-and-forget, // so we wait until `inflightRebuilds` drains before claiming "running" diff --git a/keeper/src/runtime/outdatedOrderSweeper.ts b/keeper/src/runtime/outdatedOrderSweeper.ts deleted file mode 100644 index 126060c..0000000 --- a/keeper/src/runtime/outdatedOrderSweeper.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { type Address, type Hex } from "viem"; -import type pino from "pino"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; -import { withUnstickRetry } from "../tx/unstick.ts"; -import { formatGasCost } from "../tx/gasCost.ts"; -import type { Chain } from "../chain.ts"; -import type { Config } from "../config.ts"; -import type { EthUsdFeed } from "../oracle/ethUsdFeed.ts"; -import type { ParticipantTracker } from "../discovery/tracker.ts"; - -/** - * Periodic sweep that closes expired Futures orders via the permissionless - * `Futures.removeOutdatedOrder(orderId)` entrypoint. - * - * Why this lives in the keeper at all: as of Futures v2.11.0 `createOrder` / - * `createOrders` no longer auto-sweep the caller's stale orders on the hot - * path (it was costing ~50-100k gas per placement just to walk an empty - * expired list). Cleanup is now an explicit, permissionless cron job — and - * the keeper is the natural operator for it because: - * - * 1. It already discovers participants (`ParticipantTracker`). - * 2. It already has the signer + tx-retry plumbing (`withUnstickRetry`). - * 3. Expired orders pin the owner against `MAX_ORDERS_PER_PARTICIPANT` - * and leave dead price levels on the book. Letting them rot makes - * every health probe and book read slightly slower forever. - * - * Today the keeper eats the gas with no on-chain reward — see the - * `TODO(keeper-incentive)` block in `Futures.sol` next to `removeOutdatedOrder` - * for a sketch of a maker-fee-escrow bounty that could pay for this work. - * - * Hot path: - * - * tick → for each tracked user: - * 1. readContract `getUserOrders(user)` — empty? skip - * 2. multicall `getOrder(id)` for each id → filter expired - * 3. one `Futures.removeOutdatedOrders([id1, ...])` write - * (capped at `outdatedOrders.maxBatchSize`; larger user-side - * fan-outs are split into N batches, each its own tx). - * - * The typed batch skips stale and not-yet-expired ids on-chain, so a user - * cancellation or competing keeper cannot revert unrelated cleanup work. - * - * Non-futures venues (perps) don't have order expiry so this module is - * Futures-only by design. - */ - -const FUTURES_REMOVE_OUTDATED_ORDERS_ABI = [ - { - type: "function", - name: "removeOutdatedOrders", - stateMutability: "nonpayable", - inputs: [{ name: "_orderIds", type: "bytes32[]" }], - outputs: [{ name: "removed", type: "uint256" }], - }, -] as const; - -interface ExpiredOrder { - user: Address; - orderId: Hex; - expirationAt: bigint; -} - -export class OutdatedOrderSweeper { - private timer: NodeJS.Timeout | undefined; - private running = false; - private inflightSweep = false; - - private readonly chain: Chain; - private readonly config: Config; - private readonly tracker: ParticipantTracker; - private readonly logger: pino.Logger; - private readonly ethUsdFeed: EthUsdFeed | undefined; - - constructor( - chain: Chain, - config: Config, - tracker: ParticipantTracker, - logger: pino.Logger, - ethUsdFeed?: EthUsdFeed, - ) { - this.chain = chain; - this.config = config; - this.tracker = tracker; - this.logger = logger.child({ component: "outdatedOrderSweeper" }); - // Optional ETH/USD source for `gasCostUsd` on confirmed-tx logs. - this.ethUsdFeed = ethUsdFeed; - } - - /** - * Run a single sweep cycle to completion. Public for tests. Idempotent - * across concurrent calls — a second invocation while one is in flight - * is dropped (we don't want overlapping sweeps racing on the same nonce). - */ - async runSweep(): Promise { - if (this.inflightSweep) { - this.logger.debug("sweep skipped — previous sweep still running"); - return 0; - } - this.inflightSweep = true; - try { - const users = this.tracker.list(); - if (users.length === 0) return 0; - - // Pull the chain's view of "now" rather than `Date.now()`. Block - // timestamps lag wall clock by up to a slot (~2s on Base), and the - // contract's `OrderNotExpired` guard uses `block.timestamp` — using - // the same clock here keeps us from broadcasting txs that'll just - // revert during the brief window around expiry. - const blockTimestamp = await this.readBlockTimestamp(); - if (blockTimestamp === undefined) return 0; - - const expired = await this.discoverExpired(users, blockTimestamp); - if (expired.length === 0) { - this.logger.debug( - { tracked: users.length }, - "sweep clean — no expired orders", - ); - return 0; - } - - this.logger.info( - { tracked: users.length, expired: expired.length }, - "sweep: closing expired orders", - ); - - const max = Math.max(1, this.config.outdatedOrders.maxBatchSize); - let closed = 0; - for (let i = 0; i < expired.length; i += max) { - const slice = expired.slice(i, i + max); - try { - closed += await this.closeBatch(slice); - } catch (err) { - this.logger.error( - { err, batchSize: slice.length }, - "sweep: batch threw — continuing with next batch", - ); - } - } - return closed; - } catch (err) { - this.logger.warn({ err }, "sweep failed — will retry next tick"); - return 0; - } finally { - this.inflightSweep = false; - } - } - - /** - * Immediate sweep at boot (catches stale orders that built up while the - * keeper was down), then periodic polls at - * `outdatedOrders.sweepIntervalMs`. Idempotent — repeated calls are a - * no-op so the standard wiring sequence in `index.ts` doesn't need - * special-cased guards. - */ - async start(): Promise { - if (this.running) return; - this.running = true; - // Fire one eager sweep so an operator deploying after a long outage - // doesn't have to wait a full interval to see the backlog drained. - await this.runSweep(); - this.timer = setInterval(() => { - void this.runSweep(); - }, this.config.outdatedOrders.sweepIntervalMs); - if (typeof this.timer.unref === "function") this.timer.unref(); - } - - stop(): void { - if (!this.running) return; - this.running = false; - if (this.timer !== undefined) { - clearInterval(this.timer); - this.timer = undefined; - } - } - - private async readBlockTimestamp(): Promise { - try { - const block = await this.chain.publicClient.getBlock({ - blockTag: "latest", - }); - return block.timestamp; - } catch (err) { - this.logger.warn({ err }, "getBlock(latest) failed — skipping sweep"); - return undefined; - } - } - - /** - * For each tracked user, read its order ids and hydrate to find - * `expirationAt < blockTimestamp`. Per-user RPC failure is logged and - * skipped — one bad address (e.g. recently dropped from the tracker) - * shouldn't block the rest of the sweep. - */ - private async discoverExpired( - users: readonly Address[], - blockTimestamp: bigint, - ): Promise { - const expired: ExpiredOrder[] = []; - - for (const user of users) { - let orderIds: readonly Hex[]; - try { - orderIds = (await this.chain.publicClient.readContract({ - address: this.config.futures.address, - abi: FuturesAbi, - functionName: "getUserOrders", - args: [user], - })) as readonly Hex[]; - } catch (err) { - this.logger.warn( - { err, user }, - "getUserOrders failed — skipping user this sweep", - ); - continue; - } - if (orderIds.length === 0) continue; - - let orders: ReadonlyArray<{ expirationAt: bigint }>; - try { - orders = (await this.chain.publicClient.multicall({ - contracts: orderIds.map((id) => ({ - address: this.config.futures.address, - abi: FuturesAbi, - functionName: "getOrder" as const, - args: [id] as const, - })), - allowFailure: false, - })) as ReadonlyArray<{ expirationAt: bigint }>; - } catch (err) { - this.logger.warn( - { err, user, orderCount: orderIds.length }, - "multicall(getOrder) failed — skipping user this sweep", - ); - continue; - } - - for (let i = 0; i < orderIds.length; i++) { - const order = orders[i]; - const orderId = orderIds[i] as Hex; - if (order === undefined) continue; - // Matches the contract guard: `expirationAt >= block.timestamp` reverts - // `OrderNotExpired`. Use strict-less-than here so we don't broadcast - // a tx in the very-edge case `expirationAt == blockTimestamp` (next - // block will satisfy it cleanly). - if (order.expirationAt < blockTimestamp) { - expired.push({ user, orderId, expirationAt: order.expirationAt }); - } - } - } - return expired; - } - - /** - * Sends one race-tolerant `removeOutdatedOrders(ids)` write. The contract - * skips stale/live ids and preserves every valid cleanup in the batch. - */ - private async closeBatch(batch: readonly ExpiredOrder[]): Promise { - if (batch.length === 0) return 0; - - if (this.config.keeper.dryRun) { - this.logger.info( - { batchSize: batch.length }, - "[dryRun] would call Futures.removeOutdatedOrders", - ); - return 0; - } - - const orderIds = batch.map((entry) => entry.orderId); - - type WriteParams = Parameters< - typeof this.chain.walletClient.writeContract - >[0]; - let hash: Hex; - try { - // Same wallet that liquidates / settles — if a previous run left a - // stuck pending tx in the mempool we need to clear it before this - // sweep can broadcast. `withUnstickRetry` handles the common case - // automatically; anything still broken after that surfaces normally. - hash = await withUnstickRetry(this.chain, this.logger, () => - this.chain.walletClient.writeContract({ - address: this.config.futures.address, - abi: FUTURES_REMOVE_OUTDATED_ORDERS_ABI, - functionName: "removeOutdatedOrders", - args: [orderIds], - account: this.chain.account, - chain: this.chain.walletClient.chain ?? null, - } as unknown as WriteParams), - ); - } catch (err) { - // Transient tx-submission failure → next sweep retries. We don't - // want unhandled rejection on the setInterval-fired path to crash - // the keeper, so always swallow and log. - this.logger.warn( - { err, batchSize: batch.length }, - "tx submission failed — sweep will retry", - ); - return 0; - } - - const receipt = await this.chain.publicClient.waitForTransactionReceipt({ - hash, - confirmations: this.config.coordinator.confirmationBlocks, - }); - this.logger.info( - { - hash, - blockNumber: receipt.blockNumber.toString(), - batchSize: batch.length, - ...formatGasCost(receipt, this.ethUsdFeed), - }, - "removeOutdatedOrders confirmed", - ); - return batch.length; - } -} diff --git a/keeper/tests/integration/buildKeeper.ts b/keeper/tests/integration/buildKeeper.ts index ae84766..26983cb 100644 --- a/keeper/tests/integration/buildKeeper.ts +++ b/keeper/tests/integration/buildKeeper.ts @@ -252,13 +252,6 @@ function buildConfig( balanceLowWei: 10_000_000_000_000_000n, balanceCriticalWei: 1_000_000_000_000_000n, }, - outdatedOrders: { - // Disabled by default in integration tests — they cover liquidation - // and delivery flows; expired-order sweep has its own unit tests. - // Tests that want to exercise it can override via a future flag. - sweepIntervalMs: 0, - maxBatchSize: 50, - }, delivery: { enabled: overrides.delivery === true, // Tighter than production so tests don't have to wait a minute for diff --git a/keeper/tests/runtime/healthcheck.test.ts b/keeper/tests/runtime/healthcheck.test.ts index ba0c9bf..0236ba2 100644 --- a/keeper/tests/runtime/healthcheck.test.ts +++ b/keeper/tests/runtime/healthcheck.test.ts @@ -50,10 +50,6 @@ const STUB_CONFIG: Config = { balanceLowWei: 10_000_000_000_000_000n, balanceCriticalWei: 1_000_000_000_000_000n, }, - outdatedOrders: { - sweepIntervalMs: 0, - maxBatchSize: 50, - }, delivery: { enabled: false, sweepIntervalMs: 60_000, diff --git a/keeper/tests/runtime/outdatedOrderSweeper.test.ts b/keeper/tests/runtime/outdatedOrderSweeper.test.ts deleted file mode 100644 index a051584..0000000 --- a/keeper/tests/runtime/outdatedOrderSweeper.test.ts +++ /dev/null @@ -1,510 +0,0 @@ -import { describe, it } from "node:test"; -import assert from "node:assert/strict"; -import { type Address, type Hex } from "viem"; -import pino from "pino"; -import { OutdatedOrderSweeper } from "../../src/runtime/outdatedOrderSweeper.ts"; -import type { Chain } from "../../src/chain.ts"; -import type { Config } from "../../src/config.ts"; -import type { ParticipantTracker } from "../../src/discovery/tracker.ts"; - -const FUTURES: Address = "0x00000000000000000000000000000000000000F1"; -const USER_A: Address = "0x000000000000000000000000000000000000000a"; -const USER_B: Address = "0x000000000000000000000000000000000000000B"; -const SIGNER: Address = "0x00000000000000000000000000000000000000A1"; - -interface LogCall { - level: "info" | "warn" | "error" | "debug"; - msg: string; - ctx: Record; -} - -function makeRecordingLogger(): { logger: pino.Logger; calls: LogCall[] } { - const calls: LogCall[] = []; - const record = - (level: LogCall["level"]) => (ctxOrMsg: unknown, msg?: string) => { - if (typeof ctxOrMsg === "string") { - calls.push({ level, msg: ctxOrMsg, ctx: {} }); - } else { - calls.push({ - level, - msg: msg ?? "", - ctx: ctxOrMsg as Record, - }); - } - }; - const logger = { - info: record("info"), - warn: record("warn"), - error: record("error"), - debug: record("debug"), - trace: () => undefined, - fatal: () => undefined, - child: () => logger, - } as unknown as pino.Logger; - return { logger, calls }; -} - -interface FakeOrder { - participant: Address; - expirationAt: bigint; -} - -interface FakeChainOpts { - blockTimestamp: bigint; - orderIdsByUser: Map; - orders: Map; -} - -interface Recorded { - readContractCalls: number; - multicallReadCalls: number; - writeCalls: Array<{ functionName: string; orderIds: Hex[] }>; -} - -function makeChain(opts: FakeChainOpts): { chain: Chain; recorded: Recorded } { - const recorded: Recorded = { - readContractCalls: 0, - multicallReadCalls: 0, - writeCalls: [], - }; - - const publicClient = { - getBlock: async () => ({ timestamp: opts.blockTimestamp }), - readContract: async ({ - functionName, - args, - }: { - functionName: string; - args: unknown[]; - }) => { - recorded.readContractCalls++; - if (functionName !== "getUserOrders") { - throw new Error(`unexpected readContract: ${functionName}`); - } - const user = args[0] as Address; - return opts.orderIdsByUser.get(user) ?? []; - }, - multicall: async ({ - contracts, - }: { - contracts: Array<{ functionName: string; args: unknown[] }>; - }) => { - recorded.multicallReadCalls++; - return contracts.map((c) => { - if (c.functionName !== "getOrder") { - throw new Error(`unexpected multicall fn: ${c.functionName}`); - } - const id = c.args[0] as Hex; - const order = opts.orders.get(id); - if (order === undefined) { - throw new Error(`order not found in fake state: ${id}`); - } - // Return shape matches the on-chain Order struct; sweeper only - // reads `expirationAt` but include the other fields so tests - // stay close to the real ABI. - return { - participant: order.participant, - price: 0n, - quantity: 1n, - expirationAt: order.expirationAt, - }; - }); - }, - waitForTransactionReceipt: async () => ({ - blockNumber: 1n, - gasUsed: 200_000n, - logs: [], - }), - }; - - const walletClient = { - chain: null, - writeContract: async ({ - functionName, - args, - }: { - functionName: string; - args: unknown[]; - }) => { - if (functionName !== "removeOutdatedOrders") { - throw new Error(`unexpected write fn: ${functionName}`); - } - recorded.writeCalls.push({ functionName, orderIds: args[0] as Hex[] }); - return "0xabc" as Hex; - }, - }; - - const chain: Chain = { - publicClient, - walletClient, - account: { address: SIGNER }, - } as unknown as Chain; - return { chain, recorded }; -} - -function makeConfig(overrides: Partial = {}): Config { - return { - futures: { address: FUTURES }, - keeper: { dryRun: false }, - coordinator: { confirmationBlocks: 0 }, - outdatedOrders: { - sweepIntervalMs: 1_000_000, // intervals never auto-fire in tests - maxBatchSize: 50, - ...overrides, - }, - } as unknown as Config; -} - -function makeTracker(users: Address[]): ParticipantTracker { - return { list: () => users } as unknown as ParticipantTracker; -} - -describe("OutdatedOrderSweeper", () => { - it("is a no-op when the tracker is empty", async () => { - const { logger, calls } = makeRecordingLogger(); - const { chain, recorded } = makeChain({ - blockTimestamp: 1_000n, - orderIdsByUser: new Map(), - orders: new Map(), - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 0); - assert.equal(recorded.writeCalls.length, 0); - assert.equal(recorded.readContractCalls, 0); - assert.equal(calls.filter((c) => c.level === "warn").length, 0); - }); - - it("skips users with no orders without sending a write", async () => { - const { logger } = makeRecordingLogger(); - const { chain, recorded } = makeChain({ - blockTimestamp: 1_000n, - orderIdsByUser: new Map([[USER_A, []]]), - orders: new Map(), - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 0); - assert.equal(recorded.writeCalls.length, 0); - assert.equal(recorded.multicallReadCalls, 0); - }); - - it("ignores orders whose expirationAt is still in the future", async () => { - const { logger } = makeRecordingLogger(); - const orderId = ("0x" + "11".repeat(32)) as Hex; - const { chain, recorded } = makeChain({ - blockTimestamp: 1_000n, - orderIdsByUser: new Map([[USER_A, [orderId]]]), - orders: new Map([ - [orderId, { participant: USER_A, expirationAt: 5_000n }], // future - ]), - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 0); - assert.equal(recorded.writeCalls.length, 0); - }); - - it("batches all expired orders for a user into one typed write", async () => { - const { logger, calls } = makeRecordingLogger(); - const id1 = ("0x" + "11".repeat(32)) as Hex; - const id2 = ("0x" + "22".repeat(32)) as Hex; - const id3 = ("0x" + "33".repeat(32)) as Hex; - const { chain, recorded } = makeChain({ - blockTimestamp: 10_000n, - orderIdsByUser: new Map([[USER_A, [id1, id2, id3]]]), - orders: new Map([ - [id1, { participant: USER_A, expirationAt: 5_000n }], // expired - [id2, { participant: USER_A, expirationAt: 9_999n }], // expired - [id3, { participant: USER_A, expirationAt: 20_000n }], // future - ]), - }); - - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - - assert.equal(closed, 2); - assert.equal(recorded.writeCalls.length, 1); - assert.equal(recorded.writeCalls[0]!.functionName, "removeOutdatedOrders"); - assert.deepEqual(recorded.writeCalls[0]!.orderIds, [id1, id2]); - - assert.ok( - calls.some((c) => c.level === "info" && c.msg.includes("confirmed")), - "expected an INFO log when the batch write confirms", - ); - }); - - it("aggregates expired orders across multiple tracked users into one tx", async () => { - const { logger } = makeRecordingLogger(); - const idA = ("0x" + "aa".repeat(32)) as Hex; - const idB = ("0x" + "bb".repeat(32)) as Hex; - const { chain, recorded } = makeChain({ - blockTimestamp: 10_000n, - orderIdsByUser: new Map([ - [USER_A, [idA]], - [USER_B, [idB]], - ]), - orders: new Map([ - [idA, { participant: USER_A, expirationAt: 5_000n }], - [idB, { participant: USER_B, expirationAt: 5_000n }], - ]), - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A, USER_B]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 2); - assert.equal( - recorded.writeCalls.length, - 1, - "one typed write for cross-user batch", - ); - assert.deepEqual(recorded.writeCalls[0]!.orderIds, [idA, idB]); - }); - - it("splits across multiple writes when batch size cap is exceeded", async () => { - const { logger } = makeRecordingLogger(); - const ids: Hex[] = []; - const orders = new Map(); - for (let i = 0; i < 5; i++) { - const id = ("0x" + String(i).padStart(2, "0").repeat(32)) as Hex; - ids.push(id); - orders.set(id, { participant: USER_A, expirationAt: 1n }); - } - const { chain, recorded } = makeChain({ - blockTimestamp: 1_000n, - orderIdsByUser: new Map([[USER_A, ids]]), - orders, - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig({ maxBatchSize: 2 }), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 5); - // 5 expired / batch of 2 → ceil(5/2) = 3 writes - assert.equal(recorded.writeCalls.length, 3); - assert.deepEqual( - recorded.writeCalls.map((c) => c.orderIds.length), - [2, 2, 1], - ); - }); - - it("skips the write entirely on dry-run", async () => { - const { logger, calls } = makeRecordingLogger(); - const id1 = ("0x" + "11".repeat(32)) as Hex; - const { chain, recorded } = makeChain({ - blockTimestamp: 10_000n, - orderIdsByUser: new Map([[USER_A, [id1]]]), - orders: new Map([[id1, { participant: USER_A, expirationAt: 1n }]]), - }); - const config = makeConfig(); - (config as { keeper: { dryRun: boolean } }).keeper.dryRun = true; - const sweeper = new OutdatedOrderSweeper( - chain, - config, - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 0); - assert.equal(recorded.writeCalls.length, 0); - assert.ok(calls.some((c) => c.msg.startsWith("[dryRun]"))); - }); - - it("does not crash when one user's getUserOrders fails — continues with the next user", async () => { - // Per-user RPC blips shouldn't drop the whole sweep tick. - const { logger, calls } = makeRecordingLogger(); - const idB = ("0x" + "bb".repeat(32)) as Hex; - const orderIdsByUser = new Map([[USER_B, [idB]]]); - const orders = new Map([ - [idB, { participant: USER_B, expirationAt: 1n }], - ]); - const blockTimestamp = 10_000n; - - // Custom chain that fails getUserOrders(USER_A) only. - const recorded: Recorded = { - readContractCalls: 0, - multicallReadCalls: 0, - writeCalls: [], - }; - const publicClient = { - getBlock: async () => ({ timestamp: blockTimestamp }), - readContract: async ({ args }: { args: unknown[] }) => { - recorded.readContractCalls++; - const user = args[0] as Address; - if (user === USER_A) throw new Error("rpc 503"); - return orderIdsByUser.get(user) ?? []; - }, - multicall: async ({ - contracts, - }: { - contracts: Array<{ args: unknown[] }>; - }) => { - recorded.multicallReadCalls++; - return contracts.map((c) => { - const order = orders.get(c.args[0] as Hex); - if (order === undefined) throw new Error("missing"); - return { - participant: order.participant, - price: 0n, - quantity: 1n, - expirationAt: order.expirationAt, - }; - }); - }, - waitForTransactionReceipt: async () => ({ - blockNumber: 1n, - gasUsed: 0n, - logs: [], - }), - }; - const walletClient = { - chain: null, - writeContract: async ({ args }: { args: unknown[] }) => { - recorded.writeCalls.push({ - functionName: "removeOutdatedOrders", - orderIds: args[0] as Hex[], - }); - return "0xabc" as Hex; - }, - }; - const chain: Chain = { - publicClient, - walletClient, - account: { address: SIGNER }, - } as unknown as Chain; - - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A, USER_B]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal( - closed, - 1, - "USER_B's order still gets closed despite USER_A's RPC failure", - ); - assert.ok( - calls.some( - (c) => c.level === "warn" && c.msg.includes("getUserOrders failed"), - ), - "expected a warn log for the failed user", - ); - }); - - it("drops the sweep cleanly when getBlock fails (skip rather than guess at timestamp)", async () => { - const { logger, calls } = makeRecordingLogger(); - const chain: Chain = { - publicClient: { - getBlock: async () => { - throw new Error("rpc 503"); - }, - }, - walletClient: {}, - account: { address: SIGNER }, - } as unknown as Chain; - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - const closed = await sweeper.runSweep(); - assert.equal(closed, 0); - assert.ok( - calls.some((c) => c.level === "warn" && c.msg.includes("getBlock")), - ); - }); - - it("coalesces overlapping sweeps — second concurrent runSweep is dropped", async () => { - // setInterval can fire while a previous sweep is still in flight on - // slow RPCs. Overlapping sweeps would race on the same nonce, so the - // sweeper must drop the redundant call. - const { logger } = makeRecordingLogger(); - const id1 = ("0x" + "11".repeat(32)) as Hex; - let releaseFirstSweep: () => void = () => undefined; - const firstSweepBlocked = new Promise((resolve) => { - releaseFirstSweep = resolve; - }); - let getBlockCount = 0; - const chain: Chain = { - publicClient: { - getBlock: async () => { - getBlockCount++; - if (getBlockCount === 1) await firstSweepBlocked; - return { timestamp: 0n }; - }, - readContract: async () => [], - }, - walletClient: {}, - account: { address: SIGNER }, - } as unknown as Chain; - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([USER_A]), - logger, - ); - - const first = sweeper.runSweep(); - const second = sweeper.runSweep(); // Should bail immediately. - const secondResult = await second; - assert.equal( - secondResult, - 0, - "concurrent sweep returns 0 without doing work", - ); - releaseFirstSweep(); - await first; - assert.equal( - getBlockCount, - 1, - "block timestamp read once — second sweep was dropped", - ); - }); - - it("stop() is idempotent and clears the interval", () => { - const { logger } = makeRecordingLogger(); - const { chain } = makeChain({ - blockTimestamp: 0n, - orderIdsByUser: new Map(), - orders: new Map(), - }); - const sweeper = new OutdatedOrderSweeper( - chain, - makeConfig(), - makeTracker([]), - logger, - ); - sweeper.stop(); // no-op pre-start - sweeper.stop(); // no-op repeated - }); -}); diff --git a/market-maker/src/adapters/futures/ownOrders.ts b/market-maker/src/adapters/futures/ownOrders.ts index 9c31311..1ae7421 100644 --- a/market-maker/src/adapters/futures/ownOrders.ts +++ b/market-maker/src/adapters/futures/ownOrders.ts @@ -10,16 +10,26 @@ import type { FuturesVenueAdapter } from "./venue.ts"; import { futuresInstrumentId } from "./events.ts"; const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"; +const FUTURES_USER_ORDERS_AT_EXPIRATION_ABI = [ + { + type: "function", + name: "getUserOrdersAtExpiration", + stateMutability: "view", + inputs: [ + { name: "_user", type: "address" }, + { name: "_expirationAt", type: "uint256" }, + ], + outputs: [{ name: "orderIds", type: "bytes32[]" }], + }, +] as const; /** * Cache-backed own-order source for a single futures expiry. * - * The contract has no per-participant order view scoped by delivery date, so - * we read all of the wallet's orders and keep only those matching this - * instrument's `expirationAt`: + * The contract exposes a participant-order view scoped by delivery date: * - * 1. `bootstrap()` reads `getUserOrders(wallet)` + `getOrder(id)` and - * caches the orders whose `expirationAt === expirationAt`. + * 1. `bootstrap()` reads `getUserOrdersAtExpiration(wallet, expirationAt)` + * plus `getOrder(id)`. * 2. `subscribe()` listens to venue events. `order-created` is filtered by * participant AND instrumentId (which encodes the expiry). `order-cancelled` * carries no expiry, so we apply it only if the id is in *this* cache — @@ -73,9 +83,9 @@ export class FuturesOwnOrders implements OwnOrderSource { const orderIds = await this.venue.publicClient.readContract({ address: this.venue.address, - abi: FuturesAbi, - functionName: "getUserOrders", - args: [owner], + abi: FUTURES_USER_ORDERS_AT_EXPIRATION_ABI, + functionName: "getUserOrdersAtExpiration", + args: [owner, this.expirationAt], }); if (orderIds.length === 0) { @@ -110,7 +120,7 @@ export class FuturesOwnOrders implements OwnOrderSource { expirationAt: bigint; }; if (!o.participant || o.participant === ZERO_ADDRESS) continue; - // Keep only orders belonging to this expiry. + // Defensive against an inconsistent RPC response. if (o.expirationAt !== this.expirationAt) continue; if (o.quantity === 0n) continue; const absQty = o.quantity < 0n ? -o.quantity : o.quantity; From 890fc7c18206967aceb468a970d11e1fa6fe5c62 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sat, 8 Aug 2026 02:42:50 +0200 Subject: [PATCH 04/26] perf(pme): specialize resting order checks Let markets answer portfolio liquidation order gates from their aggregate indexes without computing full risk views or reading price oracles. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 7 +++---- contracts/contracts/interfaces/ILinearMarket.sol | 5 +++++ contracts/contracts/mocks/FuturesMock.sol | 4 ++++ contracts/contracts/mocks/PerpsDEXMock.sol | 10 ++++++++++ contracts/tests/portfolioMarginEngine.test.ts | 11 +++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index 6fac367..c01ecfc 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -310,13 +310,12 @@ contract PortfolioMarginEngine is /// /// Keyed on delta, not order count, so an order carrying no risk cannot deadlock /// liquidation — an expired futures order still occupies its participant index but - /// contributes nothing here. Short-circuits on the first market with exposure, so - /// the common case costs one `getRiskView`. + /// contributes nothing here. Markets answer this from their order indexes or + /// aggregate caches, without computing position PnL or reading an oracle. function hasRestingOrderDelta(address user) external view returns (bool) { uint256 len = linearMarkets.length(); for (uint256 i = 0; i < len; i++) { - ILinearMarket.RiskView memory account = ILinearMarket(linearMarkets.at(i)).getRiskView(user); - if (account.buyOrderDelta != 0 || account.sellOrderDelta != 0) return true; + if (ILinearMarket(linearMarkets.at(i)).hasRestingOrderDelta(user)) return true; } return false; } diff --git a/contracts/contracts/interfaces/ILinearMarket.sol b/contracts/contracts/interfaces/ILinearMarket.sol index da9eb6f..0dee54d 100644 --- a/contracts/contracts/interfaces/ILinearMarket.sol +++ b/contracts/contracts/interfaces/ILinearMarket.sol @@ -70,4 +70,9 @@ interface ILinearMarket { /// silently take `pendingFunding` as the old `orderMargin` instead of /// reverting. A fresh selector makes version skew fail loud. function getRiskView(address user) external view returns (RiskView memory); + + /// @notice Whether this market reports any currently margin-relevant resting-order delta. + /// @dev This narrow read keeps portfolio-wide orders-first liquidation checks off the + /// substantially more expensive position, oracle, and fill-loss path in `getRiskView`. + function hasRestingOrderDelta(address user) external view returns (bool); } diff --git a/contracts/contracts/mocks/FuturesMock.sol b/contracts/contracts/mocks/FuturesMock.sol index c220e12..25bd81a 100644 --- a/contracts/contracts/mocks/FuturesMock.sol +++ b/contracts/contracts/mocks/FuturesMock.sol @@ -74,4 +74,8 @@ contract FuturesMock is ILinearMarket { sellOrderFillLoss: _sellOrderFillLoss[user] }); } + + function hasRestingOrderDelta(address user) external view returns (bool) { + return _buyOrderDelta[user] != 0 || _sellOrderDelta[user] != 0; + } } diff --git a/contracts/contracts/mocks/PerpsDEXMock.sol b/contracts/contracts/mocks/PerpsDEXMock.sol index 1f89a35..a05b1c3 100644 --- a/contracts/contracts/mocks/PerpsDEXMock.sol +++ b/contracts/contracts/mocks/PerpsDEXMock.sol @@ -31,6 +31,7 @@ contract PerpsDEXMock is ILinearMarket { mapping(address => uint256) private _sellOrderDelta; mapping(address => uint256) private _buyOrderFillLoss; mapping(address => uint256) private _sellOrderFillLoss; + bool private _riskViewDisabled; function setUserPosition(address user, int256 qty, uint256 entryPrice) external { _positions[user] = Position(qty, entryPrice); @@ -60,6 +61,7 @@ contract PerpsDEXMock is ILinearMarket { } function getRiskView(address user) external view returns (RiskView memory) { + if (_riskViewDisabled) revert(); return RiskView({ netPositionDelta: _positions[user].netQuantity * 1e6 / int256(10 ** QUANTITY_DECIMALS), unrealizedPnl: _unrealizedPnl[user], @@ -95,6 +97,14 @@ contract PerpsDEXMock is ILinearMarket { _sellOrderFillLoss[user] = sellLoss; } + function setRiskViewDisabled(bool disabled) external { + _riskViewDisabled = disabled; + } + + function hasRestingOrderDelta(address user) external view returns (bool) { + return _buyOrderDelta[user] != 0 || _sellOrderDelta[user] != 0; + } + function isLiquidatable(address user) external view returns (bool) { if (_positions[user].netQuantity == 0) return false; return _balances[user] < _maintenanceMargin[user]; diff --git a/contracts/tests/portfolioMarginEngine.test.ts b/contracts/tests/portfolioMarginEngine.test.ts index ed2bb2b..46f98ed 100644 --- a/contracts/tests/portfolioMarginEngine.test.ts +++ b/contracts/tests/portfolioMarginEngine.test.ts @@ -279,6 +279,17 @@ describe("PortfolioMarginEngine", () => { assert.equal(await pme.read.hasRestingOrderDelta([user]), true); }); + it("does not compute full market risk views", async () => { + const { pme, perpsMock, futuresMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await perpsMock.write.setRiskViewDisabled([true]); + await futuresMock.write.setOrderDeltas([user, ONE_LOT_QTY, 0n]); + + assert.equal(await pme.read.hasRestingOrderDelta([user]), true); + }); + it("catches either side", async () => { const { pme, perpsMock, user } = await networkHelpers.loadFixture( deployPortfolioMarginEngineFixture, From 2bf1d055c27697c3dde6884ec57f629a37a9d2cf Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sat, 8 Aug 2026 02:50:38 +0200 Subject: [PATCH 05/26] perf(pme): compute liquidation margins together Price IM and MM from one market/options snapshot and oracle read so post-liquidation guards avoid duplicating every portfolio call. Co-authored-by: Cursor --- contracts/abi/IPortfolioMarginEngine.json | 24 ++++++ contracts/abi/IPortfolioMarginEngine.ts | 24 ++++++ contracts/abi/PortfolioMarginEngine.json | 24 ++++++ contracts/abi/PortfolioMarginEngine.ts | 24 ++++++ contracts/contracts/PortfolioMarginEngine.sol | 82 +++++++++++++------ .../interfaces/IPortfolioMarginEngine.sol | 3 + .../contracts/mocks/MarginEngineMock.sol | 4 + contracts/tests/portfolioMarginEngine.test.ts | 2 + 8 files changed, 161 insertions(+), 26 deletions(-) diff --git a/contracts/abi/IPortfolioMarginEngine.json b/contracts/abi/IPortfolioMarginEngine.json index bb87587..4be2dfc 100644 --- a/contracts/abi/IPortfolioMarginEngine.json +++ b/contracts/abi/IPortfolioMarginEngine.json @@ -37,6 +37,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "computePortfolioMargins", + "outputs": [ + { + "internalType": "uint256", + "name": "im", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mm", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/IPortfolioMarginEngine.ts b/contracts/abi/IPortfolioMarginEngine.ts index 2b93977..a9a1c57 100644 --- a/contracts/abi/IPortfolioMarginEngine.ts +++ b/contracts/abi/IPortfolioMarginEngine.ts @@ -37,6 +37,30 @@ export const IPortfolioMarginEngineAbi = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "computePortfolioMargins", + "outputs": [ + { + "internalType": "uint256", + "name": "im", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mm", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/PortfolioMarginEngine.json b/contracts/abi/PortfolioMarginEngine.json index 54d4ff2..a5dc6b5 100644 --- a/contracts/abi/PortfolioMarginEngine.json +++ b/contracts/abi/PortfolioMarginEngine.json @@ -361,6 +361,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "computePortfolioMargins", + "outputs": [ + { + "internalType": "uint256", + "name": "im", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mm", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getLinearMarkets", diff --git a/contracts/abi/PortfolioMarginEngine.ts b/contracts/abi/PortfolioMarginEngine.ts index 3bb317b..1461c08 100644 --- a/contracts/abi/PortfolioMarginEngine.ts +++ b/contracts/abi/PortfolioMarginEngine.ts @@ -361,6 +361,30 @@ export const PortfolioMarginEngineAbi = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "computePortfolioMargins", + "outputs": [ + { + "internalType": "uint256", + "name": "im", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "mm", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getLinearMarkets", diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index c01ecfc..1633592 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -252,6 +252,14 @@ contract PortfolioMarginEngine is return _computeMargin(user, false); } + /// @notice Compute IM and MM from one market/options snapshot and one oracle read. + function computePortfolioMargins(address user) external view returns (uint256 im, uint256 mm) { + MarginInputs memory inputs = _marginInputs(user, _linearAggregate(user)); + uint256 spotPrice = _getSpotPriceWad(); + im = _marginFromInputs(inputs, true, spotPrice); + mm = _marginFromInputs(inputs, false, spotPrice); + } + /// @notice Margin charged against a delta-one resting order's notional (both token /// decimals). /// @dev The IM spot shock is the single knob sizing unmatched linear exposure across @@ -291,12 +299,14 @@ contract PortfolioMarginEngine is /// wanting a per-order gate want `linearOrderMargin` instead. function orderMarginOf(address user) external view returns (uint256) { LinearAggregate memory agg = _linearAggregate(user); - uint256 withOrders = _marginFromAggregate(user, agg, true); + MarginInputs memory inputs = _marginInputs(user, agg); + uint256 spotPrice = _getSpotPriceWad(); + uint256 withOrders = _marginFromInputs(inputs, true, spotPrice); - agg.buyOrderDelta = 0; - agg.sellOrderDelta = 0; - agg.fillLoss = 0; - uint256 withoutOrders = _marginFromAggregate(user, agg, true); + inputs.linear.buyOrderDelta = 0; + inputs.linear.sellOrderDelta = 0; + inputs.linear.fillLoss = 0; + uint256 withoutOrders = _marginFromInputs(inputs, true, spotPrice); return withOrders > withoutOrders ? withOrders - withoutOrders : 0; } @@ -335,37 +345,58 @@ contract PortfolioMarginEngine is uint256 fundingOwed; } + struct MarginInputs { + LinearAggregate linear; + int256 netDelta; + uint256 netGamma; + uint256 netVega; + uint256 optionsReserved; + } + function _computeMargin(address user, bool isIM) private view returns (uint256) { return _marginFromAggregate(user, _linearAggregate(user), isIM); } - /// @dev Folds options greeks into the linear aggregate and prices it. Split out from - /// `_computeMargin` so `orderMarginOf` can re-price the same aggregate with the - /// order fields zeroed without a second round of external reads. - function _marginFromAggregate(address user, LinearAggregate memory agg, bool isIM) + /// @dev Fold options into an already-collected linear snapshot. + function _marginInputs(address user, LinearAggregate memory agg) private view - returns (uint256) + returns (MarginInputs memory inputs) { - // 1. Options Greeks — WAD-scaled signed delta, unsigned gamma/vega (optional) - int256 netDelta = agg.netDelta; - uint256 netGamma = 0; - uint256 netVega = 0; - uint256 optReservedTokens = 0; + inputs.linear = agg; + inputs.netDelta = agg.netDelta; if (address(optionsEngine) != address(0)) { (int256 optDelta, uint256 optGamma, uint256 optVega) = optionsEngine.getNetGreeks(user); - netDelta += optDelta; - netGamma = optGamma; - netVega = optVega; - optReservedTokens = M.fromWad(optionsEngine.getOptionsReservedMargin(user), collateralDecimals); + inputs.netDelta += optDelta; + inputs.netGamma = optGamma; + inputs.netVega = optVega; + inputs.optionsReserved = M.fromWad(optionsEngine.getOptionsReservedMargin(user), collateralDecimals); } + } + + /// @dev Price one shared account snapshot at either IM or MM shocks. + function _marginFromAggregate(address user, LinearAggregate memory agg, bool isIM) + private + view + returns (uint256) + { + return _marginFromInputs(_marginInputs(user, agg), isIM, _getSpotPriceWad()); + } + function _marginFromInputs(MarginInputs memory inputs, bool isIM, uint256 spotPrice) + private + view + returns (uint256) + { + LinearAggregate memory agg = inputs.linear; // 2. Stress both fill legs (WAD-scaled) and keep the worse. Gamma and vega ride // along unchanged in both — only delta moves with the orders. - uint256 worstLoss = - _worstStressLoss(netDelta + int256(agg.buyOrderDelta), netGamma, netVega, isIM); - uint256 sellLoss = - _worstStressLoss(netDelta - int256(agg.sellOrderDelta), netGamma, netVega, isIM); + uint256 worstLoss = _worstStressLoss( + inputs.netDelta + int256(agg.buyOrderDelta), inputs.netGamma, inputs.netVega, isIM, spotPrice + ); + uint256 sellLoss = _worstStressLoss( + inputs.netDelta - int256(agg.sellOrderDelta), inputs.netGamma, inputs.netVega, isIM, spotPrice + ); if (sellLoss > worstLoss) worstLoss = sellLoss; // Convert stress loss from WAD to token decimals @@ -378,7 +409,7 @@ contract PortfolioMarginEngine is ? agg.unrealizedLossPerMarket : (agg.netUnrealizedPnl < 0 ? uint256(-agg.netUnrealizedPnl) : 0); - return stressTokens + agg.fillLoss + optReservedTokens + pnlTokens + agg.fundingOwed; + return stressTokens + agg.fillLoss + inputs.optionsReserved + pnlTokens + agg.fundingOwed; } /// @dev One batched getRiskView call per registered linear market: sums the WAD-lifted @@ -408,7 +439,7 @@ contract PortfolioMarginEngine is /// @dev Evaluate 4 stress scenarios and return the worst-case loss (WAD). /// Scenarios: (±Δs, ±Δσ) where Δs = spotShock * spotPrice (dollar move) /// PnL ≈ delta·Δs + ½·gamma·Δs² + vega·Δσ - function _worstStressLoss(int256 netDelta, uint256 netGamma, uint256 netVega, bool isIM) + function _worstStressLoss(int256 netDelta, uint256 netGamma, uint256 netVega, bool isIM, uint256 spotPrice) private view returns (uint256 worst) @@ -417,7 +448,6 @@ contract PortfolioMarginEngine is uint256 volShock = isIM ? imVolShock : mmVolShock; // Convert percentage shock → dollar move (WAD) - uint256 spotPrice = _getSpotPriceWad(); uint256 deltaS = spotShockFrac * spotPrice / WAD; // Pre-compute gamma term: ½ · gamma · Δs² diff --git a/contracts/contracts/interfaces/IPortfolioMarginEngine.sol b/contracts/contracts/interfaces/IPortfolioMarginEngine.sol index 4cc73cf..55a822f 100644 --- a/contracts/contracts/interfaces/IPortfolioMarginEngine.sol +++ b/contracts/contracts/interfaces/IPortfolioMarginEngine.sol @@ -19,6 +19,9 @@ interface IPortfolioMarginEngine { /// @notice Portfolio Maintenance Margin in token decimals. function computePortfolioMM(address user) external view returns (uint256); + /// @notice Portfolio Initial and Maintenance Margin from one shared market snapshot. + function computePortfolioMargins(address user) external view returns (uint256 im, uint256 mm); + /// @notice Margin charged against a delta-one resting order's notional (both token /// decimals). /// @dev Lets a market size order margin from the engine's risk knob without importing diff --git a/contracts/contracts/mocks/MarginEngineMock.sol b/contracts/contracts/mocks/MarginEngineMock.sol index c66df2d..ef888d2 100644 --- a/contracts/contracts/mocks/MarginEngineMock.sol +++ b/contracts/contracts/mocks/MarginEngineMock.sol @@ -30,6 +30,10 @@ contract MarginEngineMock is IPortfolioMarginEngine { return 0; } + function computePortfolioMargins(address user) external view returns (uint256 im, uint256 mm) { + return (_im[user], 0); + } + /// @dev Consistent with the zero shock below: this mock never charges order margin. function linearOrderMargin(uint256) external pure returns (uint256) { return 0; diff --git a/contracts/tests/portfolioMarginEngine.test.ts b/contracts/tests/portfolioMarginEngine.test.ts index 46f98ed..d2507f0 100644 --- a/contracts/tests/portfolioMarginEngine.test.ts +++ b/contracts/tests/portfolioMarginEngine.test.ts @@ -398,8 +398,10 @@ describe("PortfolioMarginEngine", () => { await perpsMock.write.setUserPosition([user, ONE_LOT_QTY, DEFAULT_MARKET_PRICE]); const im = await pme.read.computePortfolioIM([user]); const mm = await pme.read.computePortfolioMM([user]); + const [combinedIm, combinedMm] = await pme.read.computePortfolioMargins([user]); assert.ok(im > mm, "IM > MM for same position"); + assert.deepEqual([combinedIm, combinedMm], [im, mm], "combined read matches standalone margins"); }); }); From c0ecd98f555f0405bf23f32b87becd4e26b39112 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sat, 8 Aug 2026 23:44:44 +0200 Subject: [PATCH 06/26] fix(pme): fail closed on invalid spot prices Prevent stale or non-positive oracle answers from erasing portfolio stress requirements during critical margin checks. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 10 +++--- contracts/tests/fixtures.ts | 4 +-- .../tests/gas-portfolioMarginEngine.test.ts | 32 +++++++++++++++++++ contracts/tests/portfolioMarginEngine.test.ts | 31 ++++++++++++++++++ 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 contracts/tests/gas-portfolioMarginEngine.test.ts diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index 1633592..0badda9 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -119,6 +119,7 @@ contract PortfolioMarginEngine is error LinearMarketNotRegistered(); error OracleNotSet(); error InvalidOracle(); + error OracleStale(); error VaultMismatch(); /// @dev A dependency did not answer a call the engine depends on: no code at the /// address, or the call reverted. Covers every dependency; which one is bad is @@ -524,14 +525,13 @@ contract PortfolioMarginEngine is } } - /// @dev Read the index oracle and scale to WAD. Reverts when no oracle is - /// configured — an unset oracle must not silently zero out the delta/gamma - /// stress loss. Returns 0 on a stale/non-positive answer (zero stress, same - /// degradation semantics as the products' own oracle reads). + /// @dev Read the index oracle and scale to WAD. Missing, invalid, or stale + /// prices must fail closed: returning zero would erase delta/gamma stress. function _getSpotPriceWad() private view returns (uint256) { if (address(priceOracle) == address(0)) revert OracleNotSet(); (, int256 answer,, uint256 updatedAt,) = priceOracle.latestRoundData(); - if (answer <= 0 || block.timestamp - updatedAt > MAX_ORACLE_STALENESS) return 0; + if (answer <= 0 || updatedAt == 0 || updatedAt > block.timestamp) revert InvalidOracle(); + if (block.timestamp - updatedAt > MAX_ORACLE_STALENESS) revert OracleStale(); return M.toWad(uint256(answer), oracleDecimals); } diff --git a/contracts/tests/fixtures.ts b/contracts/tests/fixtures.ts index 4bb4ef1..83e7445 100644 --- a/contracts/tests/fixtures.ts +++ b/contracts/tests/fixtures.ts @@ -98,7 +98,7 @@ export async function deployPortfolioMarginEngineFixture(conn: NetworkConnection const { viem } = conn; const [owner] = await viem.getWalletClients(); const { usdc, vault } = await deployCollateralVaultProxy(conn); - const { perpsMock, optionsMock, futuresMock, pme } = await deployPortfolioMarginEngineStack( + const { perpsMock, optionsMock, futuresMock, oracleMock, pme } = await deployPortfolioMarginEngineStack( conn, vault.address, ); @@ -108,7 +108,7 @@ export async function deployPortfolioMarginEngineFixture(conn: NetworkConnection await vault.write.deposit([PME_OWNER_DEPOSIT], { account: owner.account }); await vault.write.setMarginEngine([pme.address], { account: owner.account }); - return { vault, perpsMock, optionsMock, futuresMock, pme, usdc, user, owner }; + return { vault, perpsMock, optionsMock, futuresMock, oracleMock, pme, usdc, user, owner }; } /** End-to-end: vault + PME + product mocks, Alice funded and deposited. */ diff --git a/contracts/tests/gas-portfolioMarginEngine.test.ts b/contracts/tests/gas-portfolioMarginEngine.test.ts new file mode 100644 index 0000000..a50f579 --- /dev/null +++ b/contracts/tests/gas-portfolioMarginEngine.test.ts @@ -0,0 +1,32 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { network } from "hardhat"; +import { encodeFunctionData } from "viem"; +import { DEFAULT_MARKET_PRICE, deployPortfolioMarginEngineFixture } from "./fixtures.js"; + +const { networkHelpers, viem } = await network.connect(); + +describe("Gas: PortfolioMarginEngine", () => { + it("computePortfolioIM representative portfolio", async () => { + const { pme, perpsMock, optionsMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await perpsMock.write.setUserPosition([user, 1_000_000n, DEFAULT_MARKET_PRICE]); + await perpsMock.write.setOrderDeltas([user, 500_000n, 250_000n]); + await optionsMock.write.setNetGreeks([user, 100_000_000_000_000_000n, 0n, 0n]); + + const publicClient = await viem.getPublicClient(); + const gas = await publicClient.estimateGas({ + account: user, + to: pme.address, + data: encodeFunctionData({ + abi: pme.abi, + functionName: "computePortfolioIM", + args: [user], + }), + }); + console.log(` computePortfolioIM representative: ${gas.toLocaleString()} gas`); + assert.ok(gas > 0n); + }); +}); diff --git a/contracts/tests/portfolioMarginEngine.test.ts b/contracts/tests/portfolioMarginEngine.test.ts index d2507f0..7d12a48 100644 --- a/contracts/tests/portfolioMarginEngine.test.ts +++ b/contracts/tests/portfolioMarginEngine.test.ts @@ -632,6 +632,37 @@ describe("PortfolioMarginEngine", () => { }); + describe("oracle freshness", () => { + it("reverts margin reads when the oracle is stale", async () => { + const { pme, oracleMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await oracleMock.write.freezeTimestamp(); + await networkHelpers.time.increase(3601); + + await viem.assertions.revertWithCustomError( + pme.read.computePortfolioIM([user]), + pme, + "OracleStale", + ); + }); + + it("reverts margin reads when the oracle answer is non-positive", async () => { + const { pme, oracleMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await oracleMock.write.setPrice([0n, 6]); + + await viem.assertions.revertWithCustomError( + pme.read.computePortfolioIM([user]), + pme, + "InvalidOracle", + ); + }); + }); + describe("gamma and vega", () => { it("gamma reduces stress loss for long gamma position", async () => { const { pme, optionsMock, user } = await networkHelpers.loadFixture( From b445276ae4de4141ec63e2e61e1da3cf954aafb2 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sat, 8 Aug 2026 23:47:46 +0200 Subject: [PATCH 07/26] fix(pme): preserve signed option convexity Stress signed net gamma and vega so short option exposure cannot be interpreted as protective long convexity. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 21 +++++++++---------- .../IOptionsEnginePortfolioView.sol | 2 +- .../contracts/mocks/OptionsEngineMock.sol | 8 +++---- .../tests/gas-portfolioMarginEngine.test.ts | 7 ++++++- contracts/tests/portfolioMarginEngine.test.ts | 18 ++++++++++++---- 5 files changed, 35 insertions(+), 21 deletions(-) diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index 0badda9..dcbf9ad 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -349,8 +349,8 @@ contract PortfolioMarginEngine is struct MarginInputs { LinearAggregate linear; int256 netDelta; - uint256 netGamma; - uint256 netVega; + int256 netGamma; + int256 netVega; uint256 optionsReserved; } @@ -367,7 +367,7 @@ contract PortfolioMarginEngine is inputs.linear = agg; inputs.netDelta = agg.netDelta; if (address(optionsEngine) != address(0)) { - (int256 optDelta, uint256 optGamma, uint256 optVega) = optionsEngine.getNetGreeks(user); + (int256 optDelta, int256 optGamma, int256 optVega) = optionsEngine.getNetGreeks(user); inputs.netDelta += optDelta; inputs.netGamma = optGamma; inputs.netVega = optVega; @@ -440,7 +440,7 @@ contract PortfolioMarginEngine is /// @dev Evaluate 4 stress scenarios and return the worst-case loss (WAD). /// Scenarios: (±Δs, ±Δσ) where Δs = spotShock * spotPrice (dollar move) /// PnL ≈ delta·Δs + ½·gamma·Δs² + vega·Δσ - function _worstStressLoss(int256 netDelta, uint256 netGamma, uint256 netVega, bool isIM, uint256 spotPrice) + function _worstStressLoss(int256 netDelta, int256 netGamma, int256 netVega, bool isIM, uint256 spotPrice) private view returns (uint256 worst) @@ -452,7 +452,7 @@ contract PortfolioMarginEngine is uint256 deltaS = spotShockFrac * spotPrice / WAD; // Pre-compute gamma term: ½ · gamma · Δs² - uint256 gammaTerm = netGamma * deltaS / WAD * deltaS / (2 * WAD); + int256 gammaTerm = netGamma * int256(deltaS) / int256(WAD) * int256(deltaS) / int256(2 * WAD); // Scenario 1: spot +, vol + worst = _scenarioLoss(netDelta, gammaTerm, netVega, int256(deltaS), int256(volShock)); @@ -474,16 +474,15 @@ contract PortfolioMarginEngine is /// PnL = delta·Δs/WAD + gammaTerm + vega·Δσ/WAD /// Note: gammaTerm is pre-computed and always the same magnitude across ±spotShock /// (quadratic in |Δs|), so we always ADD it regardless of direction. - function _scenarioLoss(int256 netDelta, uint256 gammaTerm, uint256 netVega, int256 deltaS, int256 deltaVol) + function _scenarioLoss(int256 netDelta, int256 gammaTerm, int256 netVega, int256 deltaS, int256 deltaVol) private pure returns (uint256) { int256 deltaPnl = netDelta * deltaS / int256(WAD); - int256 vegaPnl = int256(netVega) * deltaVol / int256(WAD); - // Gamma term is ½γ(Δs)² — always non-negative, always adds to P&L - // (positive gamma profits from moves, negative gamma loses) - int256 pnl = deltaPnl + int256(gammaTerm) + vegaPnl; + int256 vegaPnl = netVega * deltaVol / int256(WAD); + // Gamma term is ½γ(Δs)²: positive gamma profits from moves, negative gamma loses. + int256 pnl = deltaPnl + gammaTerm + vegaPnl; return pnl < 0 ? uint256(-pnl) : 0; } @@ -577,7 +576,7 @@ contract PortfolioMarginEngine is function _validateOptionsContract(address _optionsEngine)private view{ try IOptionsEnginePortfolioView(_optionsEngine).getNetGreeks(address(this)) returns ( - int256, uint256, uint256 + int256, int256, int256 ) { } catch { revert InvalidDependency(); } diff --git a/contracts/contracts/interfaces/IOptionsEnginePortfolioView.sol b/contracts/contracts/interfaces/IOptionsEnginePortfolioView.sol index 6af0dd9..b2686a6 100644 --- a/contracts/contracts/interfaces/IOptionsEnginePortfolioView.sol +++ b/contracts/contracts/interfaces/IOptionsEnginePortfolioView.sol @@ -12,7 +12,7 @@ interface IOptionsEnginePortfolioView { /// own vault. See `ILinearMarket.vault`. function vault() external view returns (ICollateralVault); - function getNetGreeks(address user) external view returns (int256 netDelta, uint256 netGamma, uint256 netVega); + function getNetGreeks(address user) external view returns (int256 netDelta, int256 netGamma, int256 netVega); function getOptionsReservedMargin(address user) external view returns (uint256); } diff --git a/contracts/contracts/mocks/OptionsEngineMock.sol b/contracts/contracts/mocks/OptionsEngineMock.sol index c2edb49..a175e3b 100644 --- a/contracts/contracts/mocks/OptionsEngineMock.sol +++ b/contracts/contracts/mocks/OptionsEngineMock.sol @@ -8,8 +8,8 @@ import { IOptionsEnginePortfolioView } from "../interfaces/IOptionsEnginePortfol contract OptionsEngineMock is IOptionsEnginePortfolioView { struct Greeks { int256 netDelta; - uint256 netGamma; - uint256 netVega; + int256 netGamma; + int256 netVega; } mapping(address => Greeks) private _greeks; @@ -22,7 +22,7 @@ contract OptionsEngineMock is IOptionsEnginePortfolioView { vault = _vault; } - function setNetGreeks(address user, int256 delta, uint256 gamma, uint256 vega) external { + function setNetGreeks(address user, int256 delta, int256 gamma, int256 vega) external { _greeks[user] = Greeks(delta, gamma, vega); } @@ -30,7 +30,7 @@ contract OptionsEngineMock is IOptionsEnginePortfolioView { _reserved[user] = amount; } - function getNetGreeks(address user) external view returns (int256, uint256, uint256) { + function getNetGreeks(address user) external view returns (int256, int256, int256) { Greeks memory g = _greeks[user]; return (g.netDelta, g.netGamma, g.netVega); } diff --git a/contracts/tests/gas-portfolioMarginEngine.test.ts b/contracts/tests/gas-portfolioMarginEngine.test.ts index a50f579..f1d6f49 100644 --- a/contracts/tests/gas-portfolioMarginEngine.test.ts +++ b/contracts/tests/gas-portfolioMarginEngine.test.ts @@ -14,7 +14,12 @@ describe("Gas: PortfolioMarginEngine", () => { await perpsMock.write.setUserPosition([user, 1_000_000n, DEFAULT_MARKET_PRICE]); await perpsMock.write.setOrderDeltas([user, 500_000n, 250_000n]); - await optionsMock.write.setNetGreeks([user, 100_000_000_000_000_000n, 0n, 0n]); + await optionsMock.write.setNetGreeks([ + user, + 100_000_000_000_000_000n, + 1_000_000_000_000_000_000n, + 1_000_000_000_000_000_000n, + ]); const publicClient = await viem.getPublicClient(); const gas = await publicClient.estimateGas({ diff --git a/contracts/tests/portfolioMarginEngine.test.ts b/contracts/tests/portfolioMarginEngine.test.ts index 7d12a48..964a8a9 100644 --- a/contracts/tests/portfolioMarginEngine.test.ts +++ b/contracts/tests/portfolioMarginEngine.test.ts @@ -676,14 +676,13 @@ describe("PortfolioMarginEngine", () => { }); it("short gamma increases stress loss", async () => { - const { pme, perpsMock, optionsMock, user } = await networkHelpers.loadFixture( + const { pme, optionsMock, user } = await networkHelpers.loadFixture( deployPortfolioMarginEngineFixture, ); - await perpsMock.write.setUserPosition([user, 0n, 0n]); - await optionsMock.write.setNetGreeks([user, 0n, 0n, 0n]); + await optionsMock.write.setNetGreeks([user, 0n, -WAD, 0n]); const im = await pme.read.computePortfolioIM([user]); - assert.equal(im, 0n, "delta-neutral, no gamma/vega → 0 margin"); + assert.ok(im > 0n, "negative gamma loses under either spot move"); }); it("vega exposure adds to margin", async () => { @@ -697,5 +696,16 @@ describe("PortfolioMarginEngine", () => { assert.ok(im > 0n, "pure vega position has positive stress margin"); assert.equal(im, 100_000n, "vega stress = vega * volShock in token decimals"); }); + + it("short vega is stressed in the opposite volatility scenario", async () => { + const { pme, optionsMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await optionsMock.write.setNetGreeks([user, 0n, 0n, -WAD]); + const im = await pme.read.computePortfolioIM([user]); + + assert.equal(im, 100_000n, "negative vega loses under the positive vol shock"); + }); }); }); From 8661576df4081fe37415e6f7a6511a09f705c160 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sun, 9 Aug 2026 01:32:50 +0200 Subject: [PATCH 08/26] perf(pme): fast-return zero order margin Skip options and oracle stress reads when linear markets report no resting-order exposure. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 1 + .../tests/gas-portfolioMarginEngine.test.ts | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index dcbf9ad..22cc9e8 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -300,6 +300,7 @@ contract PortfolioMarginEngine is /// wanting a per-order gate want `linearOrderMargin` instead. function orderMarginOf(address user) external view returns (uint256) { LinearAggregate memory agg = _linearAggregate(user); + if (agg.buyOrderDelta == 0 && agg.sellOrderDelta == 0 && agg.fillLoss == 0) return 0; MarginInputs memory inputs = _marginInputs(user, agg); uint256 spotPrice = _getSpotPriceWad(); uint256 withOrders = _marginFromInputs(inputs, true, spotPrice); diff --git a/contracts/tests/gas-portfolioMarginEngine.test.ts b/contracts/tests/gas-portfolioMarginEngine.test.ts index f1d6f49..eb68752 100644 --- a/contracts/tests/gas-portfolioMarginEngine.test.ts +++ b/contracts/tests/gas-portfolioMarginEngine.test.ts @@ -34,4 +34,31 @@ describe("Gas: PortfolioMarginEngine", () => { console.log(` computePortfolioIM representative: ${gas.toLocaleString()} gas`); assert.ok(gas > 0n); }); + + it("orderMarginOf no resting orders", async () => { + const { pme, perpsMock, optionsMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await perpsMock.write.setUserPosition([user, 1_000_000n, DEFAULT_MARKET_PRICE]); + await optionsMock.write.setNetGreeks([ + user, + 100_000_000_000_000_000n, + 1_000_000_000_000_000_000n, + 1_000_000_000_000_000_000n, + ]); + + const publicClient = await viem.getPublicClient(); + const gas = await publicClient.estimateGas({ + account: user, + to: pme.address, + data: encodeFunctionData({ + abi: pme.abi, + functionName: "orderMarginOf", + args: [user], + }), + }); + console.log(` orderMarginOf no orders: ${gas.toLocaleString()} gas`); + assert.equal(await pme.read.orderMarginOf([user]), 0n); + }); }); From 863a4e66e9ca929e8a8e434c1c436c1880256e75 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Sun, 9 Aug 2026 01:34:43 +0200 Subject: [PATCH 09/26] perf(pme): collapse equivalent stress scenarios Compute the minimum signed delta and vega endpoints directly now that both option Greeks preserve direction. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index 22cc9e8..8776533 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -438,8 +438,8 @@ contract PortfolioMarginEngine is } } - /// @dev Evaluate 4 stress scenarios and return the worst-case loss (WAD). - /// Scenarios: (±Δs, ±Δσ) where Δs = spotShock * spotPrice (dollar move) + /// @dev Return the worst loss over (±Δs, ±Δσ), where each linear term is minimized + /// independently and the gamma term is unchanged across spot directions. /// PnL ≈ delta·Δs + ½·gamma·Δs² + vega·Δσ function _worstStressLoss(int256 netDelta, int256 netGamma, int256 netVega, bool isIM, uint256 spotPrice) private @@ -455,36 +455,12 @@ contract PortfolioMarginEngine is // Pre-compute gamma term: ½ · gamma · Δs² int256 gammaTerm = netGamma * int256(deltaS) / int256(WAD) * int256(deltaS) / int256(2 * WAD); - // Scenario 1: spot +, vol + - worst = _scenarioLoss(netDelta, gammaTerm, netVega, int256(deltaS), int256(volShock)); - - // Scenario 2: spot +, vol - - uint256 loss = _scenarioLoss(netDelta, gammaTerm, netVega, int256(deltaS), -int256(volShock)); - if (loss > worst) worst = loss; - - // Scenario 3: spot -, vol + - loss = _scenarioLoss(netDelta, gammaTerm, netVega, -int256(deltaS), int256(volShock)); - if (loss > worst) worst = loss; - - // Scenario 4: spot -, vol - - loss = _scenarioLoss(netDelta, gammaTerm, netVega, -int256(deltaS), -int256(volShock)); - if (loss > worst) worst = loss; - } - - /// @dev Compute loss for a single scenario. Returns max(0, -PnL) in WAD. - /// PnL = delta·Δs/WAD + gammaTerm + vega·Δσ/WAD - /// Note: gammaTerm is pre-computed and always the same magnitude across ±spotShock - /// (quadratic in |Δs|), so we always ADD it regardless of direction. - function _scenarioLoss(int256 netDelta, int256 gammaTerm, int256 netVega, int256 deltaS, int256 deltaVol) - private - pure - returns (uint256) - { - int256 deltaPnl = netDelta * deltaS / int256(WAD); - int256 vegaPnl = netVega * deltaVol / int256(WAD); - // Gamma term is ½γ(Δs)²: positive gamma profits from moves, negative gamma loses. - int256 pnl = deltaPnl + gammaTerm + vegaPnl; - return pnl < 0 ? uint256(-pnl) : 0; + int256 deltaPnl = netDelta * int256(deltaS) / int256(WAD); + int256 vegaPnl = netVega * int256(volShock) / int256(WAD); + uint256 deltaLoss = deltaPnl < 0 ? uint256(-deltaPnl) : uint256(deltaPnl); + uint256 vegaLoss = vegaPnl < 0 ? uint256(-vegaPnl) : uint256(vegaPnl); + int256 worstPnl = gammaTerm - int256(deltaLoss) - int256(vegaLoss); + worst = worstPnl < 0 ? uint256(-worstPnl) : 0; } From 559f3af0e7662106f7b8a2b02f87a27e74b9fdc2 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 12:18:42 +0200 Subject: [PATCH 10/26] fix(contracts): mirror signed perps entry value Align the perps mock position tuple with production semantics while preserving the price-based test setter. Co-authored-by: Cursor --- contracts/contracts/mocks/PerpsDEXMock.sol | 5 +++-- contracts/tests/portfolioMarginEngine.test.ts | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/contracts/contracts/mocks/PerpsDEXMock.sol b/contracts/contracts/mocks/PerpsDEXMock.sol index a05b1c3..f8fdb0b 100644 --- a/contracts/contracts/mocks/PerpsDEXMock.sol +++ b/contracts/contracts/mocks/PerpsDEXMock.sol @@ -8,7 +8,7 @@ import { ILinearMarket } from "../interfaces/ILinearMarket.sol"; contract PerpsDEXMock is ILinearMarket { struct Position { int256 netQuantity; - uint256 aggregatedEntryPrice; + int256 netEntryValue; } uint8 public constant QUANTITY_DECIMALS = 6; @@ -34,7 +34,8 @@ contract PerpsDEXMock is ILinearMarket { bool private _riskViewDisabled; function setUserPosition(address user, int256 qty, uint256 entryPrice) external { - _positions[user] = Position(qty, entryPrice); + int256 netEntryValue = qty * int256(entryPrice) / int256(10 ** QUANTITY_DECIMALS); + _positions[user] = Position(qty, netEntryValue); } function setBalance(address user, uint256 bal) external { diff --git a/contracts/tests/portfolioMarginEngine.test.ts b/contracts/tests/portfolioMarginEngine.test.ts index 964a8a9..5dba1ef 100644 --- a/contracts/tests/portfolioMarginEngine.test.ts +++ b/contracts/tests/portfolioMarginEngine.test.ts @@ -33,6 +33,19 @@ describe("PortfolioMarginEngine", () => { }); describe("perps-only position", () => { + it("exposes signed net entry value", async () => { + const { perpsMock, user } = await networkHelpers.loadFixture( + deployPortfolioMarginEngineFixture, + ); + + await perpsMock.write.setUserPosition([user, -ONE_LOT_QTY, DEFAULT_MARKET_PRICE]); + + assert.deepEqual(await perpsMock.read.getUserPosition([user]), { + netQuantity: -ONE_LOT_QTY, + netEntryValue: -DEFAULT_MARKET_PRICE, + }); + }); + it("computes margin from perps delta stress", async () => { const { pme, perpsMock, user } = await networkHelpers.loadFixture( deployPortfolioMarginEngineFixture, From 60aed6affd9cc8083be66d07daef68f951d14d70 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 12:28:01 +0200 Subject: [PATCH 11/26] fix(keeper): decode signed perps entry value Use an exact local getter ABI so snapshots and venue PnL match the deployed signed position tuple without waiting on an external ABI release. Co-authored-by: Cursor --- keeper/src/predict/snapshot.ts | 12 +++++++-- keeper/src/venues/perps.ts | 12 ++++----- keeper/src/venues/perpsPositionAbi.ts | 19 ++++++++++++++ keeper/tests/integration/helpers.ts | 5 ++-- keeper/tests/predict/coordinator.test.ts | 5 +++- .../tests/predict/coordinatorAlerts.test.ts | 2 +- keeper/tests/predict/snapshot.test.ts | 15 +++++++++-- keeper/tests/venues/perps.test.ts | 25 +++++++++++++++---- keeper/tests/venues/reduceToTarget.test.ts | 2 +- 9 files changed, 77 insertions(+), 20 deletions(-) create mode 100644 keeper/src/venues/perpsPositionAbi.ts diff --git a/keeper/src/predict/snapshot.ts b/keeper/src/predict/snapshot.ts index db630ca..0b18096 100644 --- a/keeper/src/predict/snapshot.ts +++ b/keeper/src/predict/snapshot.ts @@ -6,6 +6,7 @@ import { PortfolioMarginEngineAbi } from "collateral-margin-abi/PortfolioMarginE import { HashPowerPerpsDEXAbi } from "derivatives-marketplace-abi/HashPowerPerpsDEX.ts"; import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; import type { AccountSnapshot, MMParams } from "@hashpower/portfolio-margin"; +import { PerpsPositionAbi } from "../venues/perpsPositionAbi.ts"; /** * Read the engine-wide constants once. They only change on PME admin @@ -102,7 +103,7 @@ export async function readAccountSnapshot( }, { address: config.perps.address, - abi: HashPowerPerpsDEXAbi, + abi: PerpsPositionAbi, functionName: "getUserPosition" as const, args: [user] as const, }, @@ -181,7 +182,10 @@ export async function readAccountSnapshot( balance: balance as bigint, perp: { netQty: perpPosition.netQuantity, - entryPrice: perpPosition.aggregatedEntryPrice, + entryPrice: + perpPosition.netQuantity === 0n + ? 0n + : (abs(perpPosition.netEntryValue) * 1_000_000n) / abs(perpPosition.netQuantity), orders: restingOrders(perpRisk, perpOrderAggregate), // PME uses `max(0, pendingFunding)` — only what the user owes. fundingOwed: funding > 0n ? funding : 0n, @@ -205,3 +209,7 @@ function restingOrders( sellValue: aggregate.sellValue, }; } + +function abs(value: bigint): bigint { + return value < 0n ? -value : value; +} diff --git a/keeper/src/venues/perps.ts b/keeper/src/venues/perps.ts index 228387f..0689acb 100644 --- a/keeper/src/venues/perps.ts +++ b/keeper/src/venues/perps.ts @@ -7,6 +7,7 @@ import { sendLiquidate } from "../tx/liquidate.ts"; import { readAccountSnapshot, readMMParams } from "../predict/snapshot.ts"; import { type MMParams, solvePerpCloseToTarget } from "@hashpower/portfolio-margin"; import type { EthUsdFeed } from "../oracle/ethUsdFeed.ts"; +import { PerpsPositionAbi } from "./perpsPositionAbi.ts"; import type { LiquidateOrdersOutcome, MarketId, @@ -81,13 +82,13 @@ export class PerpsVenue implements Venue { } async readPositions(user: Address): Promise { - // Single-market netted position. We need entryPrice + qty + market price - // to derive `unrealizedLoss` and `notional`. + // Single-market netted position. The signed entry value lets us derive PnL + // directly without reconstructing a rounded average entry price. const [position, marketPrice] = await this.chain.publicClient.multicall({ contracts: [ { address: this.config.perps.address, - abi: HashPowerPerpsDEXAbi, + abi: PerpsPositionAbi, functionName: "getUserPosition" as const, args: [user] as const, }, @@ -104,9 +105,8 @@ export class PerpsVenue implements Venue { const absQty = abs(position.netQuantity); const isLong = position.netQuantity > 0n; - // PnL in token decimals: priceDiff * netQty / 10^QUANTITY_DECIMALS - const priceDiff = marketPrice - position.aggregatedEntryPrice; - const pnl = (priceDiff * position.netQuantity) / QUANTITY_SCALE; + // PnL in token decimals: mark value minus the signed entry value. + const pnl = (marketPrice * position.netQuantity) / QUANTITY_SCALE - position.netEntryValue; const unrealizedLoss = pnl < 0n ? -pnl : 0n; const notional = (marketPrice * absQty) / QUANTITY_SCALE; diff --git a/keeper/src/venues/perpsPositionAbi.ts b/keeper/src/venues/perpsPositionAbi.ts new file mode 100644 index 0000000..6baf9bd --- /dev/null +++ b/keeper/src/venues/perpsPositionAbi.ts @@ -0,0 +1,19 @@ +/** Exact local fragment while the pinned perps ABI still exposes the legacy position tuple. */ +export const PerpsPositionAbi = [ + { + type: "function", + name: "getUserPosition", + stateMutability: "view", + inputs: [{ name: "_user", type: "address" }], + outputs: [ + { + name: "", + type: "tuple", + components: [ + { name: "netQuantity", type: "int256" }, + { name: "netEntryValue", type: "int256" }, + ], + }, + ], + }, +] as const; diff --git a/keeper/tests/integration/helpers.ts b/keeper/tests/integration/helpers.ts index 1d845c6..37be777 100644 --- a/keeper/tests/integration/helpers.ts +++ b/keeper/tests/integration/helpers.ts @@ -3,6 +3,7 @@ import type { Address, Hex } from "viem"; import type { PlanOutcome } from "../../src/coordinator/planner.ts"; import type { KeeperHarness } from "./buildKeeper.ts"; import type { DeployedStack } from "./deployStack.ts"; +import { PerpsPositionAbi } from "../../src/venues/perpsPositionAbi.ts"; /** * Integration-test helpers. @@ -73,7 +74,7 @@ export async function runOneSweep(keeper: KeeperHarness, user: Address): Promise export interface PerpsPosition { /** Signed; positive = long, negative = short, zero = flat. */ netQuantity: bigint; - aggregatedEntryPrice: bigint; + netEntryValue: bigint; } export async function readPerpsPosition( @@ -82,7 +83,7 @@ export async function readPerpsPosition( ): Promise { return (await stack.publicClient.readContract({ address: stack.addresses.perps, - abi: stack.abis.perps, + abi: PerpsPositionAbi, functionName: "getUserPosition", args: [user], })) as PerpsPosition; diff --git a/keeper/tests/predict/coordinator.test.ts b/keeper/tests/predict/coordinator.test.ts index 53c0a63..45211b6 100644 --- a/keeper/tests/predict/coordinator.test.ts +++ b/keeper/tests/predict/coordinator.test.ts @@ -118,7 +118,10 @@ function buildHarness({ case "balanceOf": return balance; case "getUserPosition": - return { netQuantity: perpNetQty, aggregatedEntryPrice: perpEntry }; + return { + netQuantity: perpNetQty, + netEntryValue: (perpNetQty * perpEntry) / 1_000_000n, + }; case "getRiskView": return { netPositionDelta: 0n, diff --git a/keeper/tests/predict/coordinatorAlerts.test.ts b/keeper/tests/predict/coordinatorAlerts.test.ts index 10529c8..91ec8cc 100644 --- a/keeper/tests/predict/coordinatorAlerts.test.ts +++ b/keeper/tests/predict/coordinatorAlerts.test.ts @@ -93,7 +93,7 @@ function buildHarness({ balance, perpEntry }: { balance: bigint; perpEntry: bigi case "balanceOf": return balance; case "getUserPosition": - return { netQuantity: 1_000_000n, aggregatedEntryPrice: perpEntry }; + return { netQuantity: 1_000_000n, netEntryValue: perpEntry }; case "getRiskView": return { netPositionDelta: 0n, diff --git a/keeper/tests/predict/snapshot.test.ts b/keeper/tests/predict/snapshot.test.ts index efe87df..0d892c2 100644 --- a/keeper/tests/predict/snapshot.test.ts +++ b/keeper/tests/predict/snapshot.test.ts @@ -34,7 +34,7 @@ function makeChain(scripted: { /** Keyed by expiry; absent means the expiry has not settled. */ settlementPrices?: Record; perpNetQty?: bigint; - perpEntry?: bigint; + perpNetEntryValue?: bigint; perpFunding?: bigint; perpOrders?: RestingOrders; futuresOrders?: RestingOrders; @@ -69,7 +69,7 @@ function makeChain(scripted: { } return { netQuantity: scripted.perpNetQty ?? 0n, - aggregatedEntryPrice: scripted.perpEntry ?? 0n, + netEntryValue: scripted.perpNetEntryValue ?? 0n, }; } case "settlementPrice": { @@ -136,6 +136,7 @@ describe("predict/snapshot: readAccountSnapshot", () => { assert.equal(snap.user, USER); assert.equal(snap.balance, 0n); assert.equal(snap.perp.netQty, 0n); + assert.equal(snap.perp.entryPrice, 0n); assert.equal(snap.perp.fundingOwed, 0n); assert.equal(snap.futures.positions.length, 0); assert.deepEqual(snap.perp.orders, NO_ORDERS); @@ -176,6 +177,16 @@ describe("predict/snapshot: readAccountSnapshot", () => { assert.equal(snap.perp.fundingOwed, 1_000n); }); + it("derives the perps average entry price from signed entry value", async () => { + const chain = makeChain({ + perpNetQty: -2_000_000n, + perpNetEntryValue: -240_000_000n, + }); + const snap = await readAccountSnapshot(chain, makeConfig(), USER); + + assert.equal(snap.perp.entryPrice, 120_000_000n); + }); + it("hydrates futures aggregates from active delivery dates", async () => { const chain = makeChain({ activeExpirationAts: [EXPIRY_A, EXPIRY_B], diff --git a/keeper/tests/venues/perps.test.ts b/keeper/tests/venues/perps.test.ts index aed92ca..9e556b5 100644 --- a/keeper/tests/venues/perps.test.ts +++ b/keeper/tests/venues/perps.test.ts @@ -95,7 +95,7 @@ describe("perps venue: readPositions", () => { it("returns no position when netQuantity is 0", async () => { const chain = makeChainStub({ multicall: () => [ - { netQuantity: 0n, aggregatedEntryPrice: 50n }, + { netQuantity: 0n, netEntryValue: 0n }, 100n, // marketPrice ], }); @@ -110,7 +110,7 @@ describe("perps venue: readPositions", () => { const marketPrice = 150n; // up → long is in profit, no loss const chain = makeChainStub({ multicall: () => [ - { netQuantity: qty, aggregatedEntryPrice: entryPrice }, + { netQuantity: qty, netEntryValue: (qty * entryPrice) / QTY_SCALE }, marketPrice, ], }); @@ -127,7 +127,7 @@ describe("perps venue: readPositions", () => { const marketPrice = 150n; // -50 per contract × 3 contracts = 150 loss const chain = makeChainStub({ multicall: () => [ - { netQuantity: qty, aggregatedEntryPrice: entryPrice }, + { netQuantity: qty, netEntryValue: (qty * entryPrice) / QTY_SCALE }, marketPrice, ], }); @@ -144,7 +144,7 @@ describe("perps venue: readPositions", () => { const marketPrice = 130n; // +30 against the short × 4 = 120 loss const chain = makeChainStub({ multicall: () => [ - { netQuantity: qty, aggregatedEntryPrice: entryPrice }, + { netQuantity: qty, netEntryValue: (qty * entryPrice) / QTY_SCALE }, marketPrice, ], }); @@ -155,10 +155,25 @@ describe("perps venue: readPositions", () => { assert.equal(pos.notional, marketPrice * 4n); }); + it("computes PnL directly from net entry value without average-price rounding", async () => { + const qty = 1_500_000n; + const chain = makeChainStub({ + multicall: () => [ + { netQuantity: qty, netEntryValue: 151n }, + 100n, + ], + }); + const venue = new PerpsVenue(chain, makeConfigStub(), silentLogger); + const [pos] = await venue.readPositions(USER); + + assert.ok(pos); + assert.equal(pos.unrealizedLoss, 1n); + }); + it("synthesises a deterministic positionId from the user address (bytes32(user))", async () => { const chain = makeChainStub({ multicall: () => [ - { netQuantity: 1n * QTY_SCALE, aggregatedEntryPrice: 100n }, + { netQuantity: 1n * QTY_SCALE, netEntryValue: 100n }, 100n, ], }); diff --git a/keeper/tests/venues/reduceToTarget.test.ts b/keeper/tests/venues/reduceToTarget.test.ts index 911cac4..e88e930 100644 --- a/keeper/tests/venues/reduceToTarget.test.ts +++ b/keeper/tests/venues/reduceToTarget.test.ts @@ -37,7 +37,7 @@ const EMPTY_RISK_VIEW = { function snapshotMulticall(balance: bigint, expiries: readonly bigint[]) { return [ balance, - { netQuantity: 0n, aggregatedEntryPrice: 0n }, + { netQuantity: 0n, netEntryValue: 0n }, EMPTY_RISK_VIEW, [0n, 0n], EMPTY_RISK_VIEW, From 9e24a5105744f3589a1cd38db93e57eb16336667 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 12:30:13 +0200 Subject: [PATCH 12/26] fix(market-maker): decode signed perps entry value Read the exact local position tuple and derive average entry prices for both long and short inventory without depending on an unpublished ABI. Co-authored-by: Cursor --- market-maker/src/adapters/perps/instrument.ts | 8 +- .../src/adapters/perps/positionAbi.ts | 19 +++++ .../tests/core/perpsInstrument.test.ts | 81 +++++++++++++++++++ 3 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 market-maker/src/adapters/perps/positionAbi.ts create mode 100644 market-maker/tests/core/perpsInstrument.test.ts diff --git a/market-maker/src/adapters/perps/instrument.ts b/market-maker/src/adapters/perps/instrument.ts index 3486895..d7e1703 100644 --- a/market-maker/src/adapters/perps/instrument.ts +++ b/market-maker/src/adapters/perps/instrument.ts @@ -21,6 +21,7 @@ import { TimeInForce } from "../../core/adapter.ts"; import { HashPowerPerpsDEXAbi } from "perps-contracts/abi/HashPowerPerpsDEX.ts"; import { calculateNotional, fillLossFromNotionals } from "../../core/math.ts"; import type { PerpsVenueAdapter } from "./venue.ts"; +import { PerpsPositionAbi } from "./positionAbi.ts"; const PERPS_INSTRUMENT_ID = "perps"; @@ -51,13 +52,16 @@ export class PerpsInstrumentAdapter implements InstrumentAdapter { const owner = this.venue.wallet.account.address; const pos = await this.venue.publicClient.readContract({ address: this.venue.address, - abi: HashPowerPerpsDEXAbi, + abi: PerpsPositionAbi, functionName: "getUserPosition", args: [owner], }); + const absQuantity = pos.netQuantity < 0n ? -pos.netQuantity : pos.netQuantity; + const absEntryValue = pos.netEntryValue < 0n ? -pos.netEntryValue : pos.netEntryValue; return { netQuantity: pos.netQuantity, - entryPrice: pos.aggregatedEntryPrice, + entryPrice: + absQuantity === 0n ? 0n : (absEntryValue * 1_000_000n) / absQuantity, }; } diff --git a/market-maker/src/adapters/perps/positionAbi.ts b/market-maker/src/adapters/perps/positionAbi.ts new file mode 100644 index 0000000..6baf9bd --- /dev/null +++ b/market-maker/src/adapters/perps/positionAbi.ts @@ -0,0 +1,19 @@ +/** Exact local fragment while the pinned perps ABI still exposes the legacy position tuple. */ +export const PerpsPositionAbi = [ + { + type: "function", + name: "getUserPosition", + stateMutability: "view", + inputs: [{ name: "_user", type: "address" }], + outputs: [ + { + name: "", + type: "tuple", + components: [ + { name: "netQuantity", type: "int256" }, + { name: "netEntryValue", type: "int256" }, + ], + }, + ], + }, +] as const; diff --git a/market-maker/tests/core/perpsInstrument.test.ts b/market-maker/tests/core/perpsInstrument.test.ts new file mode 100644 index 0000000..2577647 --- /dev/null +++ b/market-maker/tests/core/perpsInstrument.test.ts @@ -0,0 +1,81 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import type { Address } from "viem"; +import { PerpsInstrumentAdapter } from "../../src/adapters/perps/instrument.ts"; +import type { PerpsVenueAdapter } from "../../src/adapters/perps/venue.ts"; + +const OWNER = "0x1111111111111111111111111111111111111111" as Address; +const PERPS = "0x2222222222222222222222222222222222222222" as Address; + +function makeInstrument(position: { netQuantity: bigint; netEntryValue: bigint }) { + const venue = { + address: PERPS, + wallet: { account: { address: OWNER } }, + publicClient: { + readContract: async (call: { + functionName: string; + args: readonly unknown[]; + abi: readonly unknown[]; + }) => { + assert.equal(call.functionName, "getUserPosition"); + assert.deepEqual(call.args, [OWNER]); + assert.deepEqual(call.abi, [ + { + type: "function", + name: "getUserPosition", + stateMutability: "view", + inputs: [{ name: "_user", type: "address" }], + outputs: [ + { + name: "", + type: "tuple", + components: [ + { name: "netQuantity", type: "int256" }, + { name: "netEntryValue", type: "int256" }, + ], + }, + ], + }, + ]); + return position; + }, + }, + } as unknown as PerpsVenueAdapter; + + return new PerpsInstrumentAdapter(venue); +} + +describe("perps instrument position", () => { + it("derives a long average entry price from net entry value", async () => { + const position = await makeInstrument({ + netQuantity: 2_000_000n, + netEntryValue: 241_000_000n, + }).getPosition(); + + assert.deepEqual(position, { + netQuantity: 2_000_000n, + entryPrice: 120_500_000n, + }); + }); + + it("derives a positive average entry price for a short", async () => { + const position = await makeInstrument({ + netQuantity: -2_500_000n, + netEntryValue: -300_000_000n, + }).getPosition(); + + assert.deepEqual(position, { + netQuantity: -2_500_000n, + entryPrice: 120_000_000n, + }); + }); + + it("uses zero entry price when flat", async () => { + const position = await makeInstrument({ + netQuantity: 0n, + netEntryValue: 0n, + }).getPosition(); + + assert.deepEqual(position, { netQuantity: 0n, entryPrice: 0n }); + }); +}); From e642d9b33bdc77e23d61f2af8636b9e331e87ef0 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 12:33:43 +0200 Subject: [PATCH 13/26] fix(keeper): align integration harness with siblings Run the keeper test node with integration-only size limits disabled and deploy Futures through its current initializer surface. Co-authored-by: Cursor --- keeper/tests/integration/deployStack.ts | 6 ++-- keeper/tests/integration/hardhat.config.ts | 15 +++++++++ keeper/tests/integration/nodeProcess.ts | 39 +++++++++++++--------- 3 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 keeper/tests/integration/hardhat.config.ts diff --git a/keeper/tests/integration/deployStack.ts b/keeper/tests/integration/deployStack.ts index a29da42..d6a25ea 100644 --- a/keeper/tests/integration/deployStack.ts +++ b/keeper/tests/integration/deployStack.ts @@ -248,8 +248,8 @@ export async function deployStack(rpcUrl: string): Promise { // hashpower settles per-day, so only the expiry spacing schedules the book). const firstExpirationAt = latestBlock.timestamp + BigInt(FUTURES_EXPIRATION_INTERVAL_DAYS * 24 * 3600); - // initialize(hashrateOracle, liquidationMarginPercent, minimumPriceIncrement, - // expirationIntervalDays, futureExpirationDatesCount, firstFutureExpirationDate) + // initialize(hashrateOracle, liquidationMarginPercent, + // futureExpirationDatesCount, firstFutureExpirationDate) const futures = await deployProxy( publicClient, owner.client, @@ -259,8 +259,6 @@ export async function deployStack(rpcUrl: string): Promise { [ hashpriceOracle, FUTURES_LIQUIDATION_MARGIN_PCT, - MIN_PRICE_INCREMENT, - FUTURES_EXPIRATION_INTERVAL_DAYS, FUTURES_FUTURE_DELIVERY_DATES_COUNT, firstExpirationAt, ], diff --git a/keeper/tests/integration/hardhat.config.ts b/keeper/tests/integration/hardhat.config.ts new file mode 100644 index 0000000..0f3a8cb --- /dev/null +++ b/keeper/tests/integration/hardhat.config.ts @@ -0,0 +1,15 @@ +/** + * Keeper integration-node configuration only. + * + * Sibling implementations can exceed EIP-170 while under active development; + * the integration suite exercises their behavior, not deployability. + */ +export default { + networks: { + hardhat: { + type: "edr-simulated", + chainType: "l1", + allowUnlimitedContractSize: true, + }, + }, +}; diff --git a/keeper/tests/integration/nodeProcess.ts b/keeper/tests/integration/nodeProcess.ts index 852d98b..b0df3e7 100644 --- a/keeper/tests/integration/nodeProcess.ts +++ b/keeper/tests/integration/nodeProcess.ts @@ -3,10 +3,10 @@ import { resolve } from "node:path"; import { createPublicClient, http } from "viem"; /** - * Spawn `pnpm exec hardhat node` from `collateral-margin/contracts/`, the only - * package in this repo that already has Hardhat 3 + viem wired up. The node - * is shared across every integration test — scenarios use `evm_snapshot` / - * `evm_revert` to isolate themselves (see `loadFixture.ts`). + * Spawn a Hardhat node from `collateral-margin/contracts/`, the only package + * in this repo that already has Hardhat 3 + viem wired up. The keeper-specific + * config disables the contract-size limit for sibling implementation artifacts + * without changing any production network configuration. * * We deliberately do NOT spin up Hardhat in `keeper/` itself: the sibling * perps and futures repos each have a deep Solidity dep tree (OZ, OZ @@ -29,8 +29,8 @@ const POLL_INTERVAL_MS = 200; export interface StartHardhatNodeOptions { /** - * Absolute path to the directory whose `hardhat.config.ts` we should run - * `pnpm exec hardhat node` from. Defaults to the workspace's + * Absolute path to the directory whose Hardhat installation should run + * the integration config. Defaults to the workspace's * `collateral-margin/contracts/` (`../../contracts` relative to this file). */ hardhatProjectDir?: string; @@ -53,18 +53,27 @@ export async function startHardhatNode( options: StartHardhatNodeOptions = {}, ): Promise { const cwd = options.hardhatProjectDir ?? resolve(import.meta.dirname, "../../../contracts"); + const config = resolve(import.meta.dirname, "hardhat.config.ts"); const rpcUrl = options.rpcUrl ?? DEFAULT_RPC_URL; const readyTimeoutMs = options.readyTimeoutMs ?? READY_TIMEOUT_MS; - const proc = spawn("pnpm", ["exec", "hardhat", "node"], { - cwd, - // `detached: true` puts the child in its own process group so we can - // kill the whole tree on shutdown — Hardhat spawns helpers (the EDR - // worker, the JSON-RPC server) that would otherwise outlive SIGTERM. - detached: true, - env: { ...process.env, FORCE_COLOR: "0" }, - stdio: ["ignore", options.verbose ? "inherit" : "ignore", options.verbose ? "inherit" : "pipe"], - }); + const proc = spawn( + "pnpm", + ["exec", "hardhat", "--config", config, "--network", "hardhat", "node"], + { + cwd, + // `detached: true` puts the child in its own process group so we can + // kill the whole tree on shutdown — Hardhat spawns helpers (the EDR + // worker, the JSON-RPC server) that would otherwise outlive SIGTERM. + detached: true, + env: { ...process.env, FORCE_COLOR: "0" }, + stdio: [ + "ignore", + options.verbose ? "inherit" : "ignore", + options.verbose ? "inherit" : "pipe", + ], + }, + ); // Even when stderr is piped silently we still want to surface crashes: // attach a one-shot handler that captures the first ~256 chars so the From ed089e7e2c05263fc4d71bd2d53c7644632e5e28 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 13:26:52 +0200 Subject: [PATCH 14/26] refactor(futures): consume HashPowerFutures artifact Vendor the renamed generated ABI so keeper and market-maker remain independently buildable before the upstream packages publish it. Co-authored-by: Cursor --- .github/workflows/keeper-test.yml | 40 +- keeper/package.json | 1 - keeper/pnpm-lock.yaml | 9 - keeper/scripts/audit-indexer-sync.ts | 4 +- keeper/scripts/debug-delivery-bootstrap.ts | 6 +- keeper/src/abi/HashPowerFutures.ts | 2128 +++++++++++++++++ keeper/src/delivery/coordinator.ts | 20 +- keeper/src/discovery/tracker.ts | 2 +- keeper/src/predict/snapshot.ts | 12 +- keeper/src/venues/futures.ts | 20 +- keeper/tests/integration/artifacts.ts | 7 +- market-maker/package.json | 1 - market-maker/pnpm-lock.yaml | 150 +- market-maker/src/abi/HashPowerFutures.ts | 2128 +++++++++++++++++ market-maker/src/adapters/futures/events.ts | 6 +- .../src/adapters/futures/instrument.ts | 16 +- .../src/adapters/futures/ownOrders.ts | 4 +- market-maker/src/adapters/futures/venue.ts | 19 +- 18 files changed, 4336 insertions(+), 237 deletions(-) create mode 100644 keeper/src/abi/HashPowerFutures.ts create mode 100644 market-maker/src/abi/HashPowerFutures.ts diff --git a/.github/workflows/keeper-test.yml b/.github/workflows/keeper-test.yml index 7a74c37..54cc92e 100644 --- a/.github/workflows/keeper-test.yml +++ b/.github/workflows/keeper-test.yml @@ -51,44 +51,22 @@ jobs: working-directory: ./keeper run: pnpm install --frozen-lockfile - # pnpm git path deps must keep `path: /contracts/abi` in the lockfile - # resolution. If that field is dropped (integrity-only), a cold CI store - # unpacks the whole futures-marketplace repo and TypeCheck fails with - # TS2307 on futures-marketplace-abi/Futures.ts. - - name: Verify futures ABI package layout + # Keep the generated HashPowerFutures artifact vendored with the keeper + # until the upstream ABI package publishes the renamed module. + - name: Verify vendored HashPowerFutures ABI working-directory: ./keeper run: | set -euo pipefail - pkg="node_modules/futures-marketplace-abi" - if [[ ! -e "$pkg" ]]; then - echo "::error::$pkg is missing after pnpm install" - ls -la node_modules | head -40 + abi="src/abi/HashPowerFutures.ts" + if [[ ! -f "$abi" ]]; then + echo "::error::$abi is missing" exit 1 fi - if [[ ! -f "$pkg/Futures.ts" ]]; then - echo "::error::$pkg/Futures.ts missing; lockfile likely dropped path:/contracts/abi (full repo unpacked)" - ls -la "$pkg" | head -40 - if [[ -f "$pkg/contracts/abi/Futures.ts" ]]; then - echo "::error::Found Futures.ts under $pkg/contracts/abi — restore path: /contracts/abi in pnpm-lock.yaml resolution" - fi + if ! grep -q 'export const HashPowerFuturesAbi' "$abi"; then + echo "::error::$abi does not export HashPowerFuturesAbi" exit 1 fi - if [[ ! -f "$pkg/package.json" ]]; then - echo "::error::$pkg/package.json missing" - ls -la "$pkg" | head -40 - exit 1 - fi - if grep -q '_pnpmPlaceholder' "$pkg/package.json"; then - echo "::error::$pkg/package.json is a pnpm placeholder; lockfile likely lost path:/contracts/abi" - exit 1 - fi - if ! grep -q '"name": "futures-marketplace-abi"' "$pkg/package.json"; then - echo "::error::$pkg/package.json is not the abi package manifest" - cat "$pkg/package.json" - exit 1 - fi - echo "futures-marketplace-abi layout ok:" - ls -la "$pkg" | head -20 + echo "Vendored HashPowerFutures ABI present" - name: Lint working-directory: ./keeper diff --git a/keeper/package.json b/keeper/package.json index cd88e6a..bb7d632 100644 --- a/keeper/package.json +++ b/keeper/package.json @@ -25,7 +25,6 @@ "amaro": "^1.1.9", "collateral-margin-abi": "github:Lumerin-protocol/collateral-margin#c34b4a360d6616d017b157a4a9e27e1a8e60079c&path:/contracts/abi", "derivatives-marketplace-abi": "github:Lumerin-protocol/derivatives-marketplace#8b7ed0f3572d0ea8039a11757b7c1b963be75535&path:/contracts/abi", - "futures-marketplace-abi": "github:Lumerin-protocol/futures-marketplace#05e97deb86a59e6be45fa0aa2ac10b8cb070c303&path:/contracts/abi", "pino": "^10.3.1", "viem": "^2.48.8" }, diff --git a/keeper/pnpm-lock.yaml b/keeper/pnpm-lock.yaml index def7113..fc7b5d8 100644 --- a/keeper/pnpm-lock.yaml +++ b/keeper/pnpm-lock.yaml @@ -20,9 +20,6 @@ importers: derivatives-marketplace-abi: specifier: github:Lumerin-protocol/derivatives-marketplace#8b7ed0f3572d0ea8039a11757b7c1b963be75535&path:/contracts/abi version: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi - futures-marketplace-abi: - specifier: github:Lumerin-protocol/futures-marketplace#05e97deb86a59e6be45fa0aa2ac10b8cb070c303&path:/contracts/abi - version: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi pino: specifier: ^10.3.1 version: 10.3.1 @@ -229,10 +226,6 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} - futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi: - resolution: {gitHosted: true, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303} - version: 0.0.0 - help-me@5.0.0: resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} @@ -464,8 +457,6 @@ snapshots: fast-safe-stringify@2.1.1: {} - futures-marketplace-abi@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/05e97deb86a59e6be45fa0aa2ac10b8cb070c303#path:/contracts/abi: {} - help-me@5.0.0: {} isows@1.0.7(ws@8.21.0): diff --git a/keeper/scripts/audit-indexer-sync.ts b/keeper/scripts/audit-indexer-sync.ts index 34c8ccd..431457f 100644 --- a/keeper/scripts/audit-indexer-sync.ts +++ b/keeper/scripts/audit-indexer-sync.ts @@ -7,7 +7,7 @@ */ import { createPublicClient, http, type Address, type Hex } from "viem"; import { baseSepolia } from "viem/chains"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi } from "../src/abi/HashPowerFutures.ts"; const ENDPOINT = "https://api.goldsky.com/api/public/project_cmmz59uoa7b5201wthnkxbuqy/subgraphs/hpow-futures/dev-latest/gn"; @@ -76,7 +76,7 @@ async function fetchTrades(): Promise { async function getChainPositionCount(blockNumber: number): Promise { const ids = await client.readContract({ address: FUT, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getActiveExpirationDates", args: [USER as Address], blockNumber: BigInt(blockNumber), diff --git a/keeper/scripts/debug-delivery-bootstrap.ts b/keeper/scripts/debug-delivery-bootstrap.ts index 7f81582..d03977f 100644 --- a/keeper/scripts/debug-delivery-bootstrap.ts +++ b/keeper/scripts/debug-delivery-bootstrap.ts @@ -7,7 +7,7 @@ */ import { createPublicClient, http, type Address } from "viem"; import { baseSepolia, base, hardhat } from "viem/chains"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi } from "../src/abi/HashPowerFutures.ts"; const FUTURES = process.env.FUTURES_ADDRESS as Address; const NETWORK = process.env.NETWORK ?? "base-sepolia"; @@ -32,7 +32,7 @@ console.log("\n--- Stage 1: getActiveExpirationDates via multicall ---"); const dateLists = await client.multicall({ contracts: USERS.map((u) => ({ address: FUTURES, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getActiveExpirationDates" as const, args: [u] as const, })), @@ -60,7 +60,7 @@ console.log(`\n--- Stage 2: getUserPosition for ${pairs.length} aggregates ---`) const positions = await client.multicall({ contracts: pairs.map((p) => ({ address: FUTURES, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserPosition" as const, args: [p.user, p.expirationAt] as const, })), diff --git a/keeper/src/abi/HashPowerFutures.ts b/keeper/src/abi/HashPowerFutures.ts new file mode 100644 index 0000000..8e40a77 --- /dev/null +++ b/keeper/src/abi/HashPowerFutures.ts @@ -0,0 +1,2128 @@ +export const HashPowerFuturesAbi = [ + { + "inputs": [ + { + "internalType": "contract ICollateralVault", + "name": "_vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyBatch", + "type": "error" + }, + { + "inputs": [], + "name": "ExpirationDateNotAvailable", + "type": "error" + }, + { + "inputs": [], + "name": "ExpirationDateShouldBeInTheFuture", + "type": "error" + }, + { + "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientMarginBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsuranceFundNotConfigured", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidDependency", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFee", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracle", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPrice", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQty", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidReduceQuantity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTimeInForce", + "type": "error" + }, + { + "inputs": [], + "name": "MaxOrdersPerParticipantPerExpirationReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPriceLevelsReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPriceLevelsReached", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "NotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotBelongToSender", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotBelongToUser", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "OrdersStillOpen", + "type": "error" + }, + { + "inputs": [], + "name": "OverLiquidation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "PositionExpirationNotStartedYet", + "type": "error" + }, + { + "inputs": [], + "name": "PositionNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "SettlementDateNotReached", + "type": "error" + }, + { + "inputs": [], + "name": "TimeInForceNotFilled", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "inputs": [], + "name": "UnsupportedTokenDecimals", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "min", + "type": "int256" + }, + { + "internalType": "int256", + "name": "max", + "type": "int256" + } + ], + "name": "ValueOutOfRange", + "type": "error" + }, + { + "inputs": [], + "name": "VaultMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "BadDebt", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "newFutureExpirationDatesCount", + "type": "uint8" + } + ], + "name": "FutureExpirationDatesCountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "hook", + "type": "address" + } + ], + "name": "HookUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newLiquidationFeeBps", + "type": "uint16" + } + ], + "name": "LiquidationFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "newLiquidationMarginPercent", + "type": "uint8" + } + ], + "name": "LiquidationMarginPercentUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newLiquidatorShareBps", + "type": "uint16" + } + ], + "name": "LiquidatorShareBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int16", + "name": "newMakerFeeBps", + "type": "int16" + } + ], + "name": "MakerFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "OracleUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + } + ], + "name": "OrderCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "name": "OrderCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OrderLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "makerOrderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tradePrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "makerFee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerFee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "makerNetQtyAfter", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerNetQtyAfter", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "makerEntryPriceAfter", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "takerEntryPriceAfter", + "type": "uint256" + } + ], + "name": "OrderMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "newQuantity", + "type": "int256" + } + ], + "name": "OrderUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newPortfolioMargin", + "type": "address" + } + ], + "name": "PortfolioMarginUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "closedQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidatorFee", + "type": "uint256" + } + ], + "name": "PositionLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "closedQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "settlementPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "settledBy", + "type": "address" + } + ], + "name": "PositionSettled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recordedBy", + "type": "address" + } + ], + "name": "SettlementPriceRecorded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int16", + "name": "newTakerFeeBps", + "type": "int16" + } + ], + "name": "TakerFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "CONTRACT_SIZE_HPS_DAY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EXPIRATION_INTERVAL_DAYS", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_ORACLE_STALENESS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_ORDERS_PER_PARTICIPANT_PER_EXPIRATION", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_PRICE_LEVELS_PER_SIDE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "QUANTITY_DECIMALS", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "cancelOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collectedFeesBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "_quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "_tif", + "type": "uint8" + } + ], + "name": "createOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "timeInForce", + "type": "uint8" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderIntent[]", + "name": "_intents", + "type": "tuple[]" + } + ], + "name": "createOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "dropActiveOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "expirationIntervalDays", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "firstFutureExpirationDate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "futureExpirationDatesCount", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getActiveExpirationDates", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getBestAskPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getBestBidPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getExpirationDates", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMarketPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getNetPositionDelta", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "getOrder", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.Order", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getOrderAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "buyQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellValue", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderAggregate", + "name": "aggregate_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getOrderAggregateAtExpiration", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "buyQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellValue", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderAggregate", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxLevels", + "type": "uint256" + } + ], + "name": "getOrderBookPrices", + "outputs": [ + { + "internalType": "uint256[]", + "name": "bids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "asks", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_isBid", + "type": "bool" + } + ], + "name": "getQuantityAtPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getRiskView", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "netPositionDelta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pendingFunding", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "buyOrderDelta", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellOrderDelta", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyOrderFillLoss", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellOrderFillLoss", + "type": "uint256" + } + ], + "internalType": "struct ILinearMarket.RiskView", + "name": "view_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getUnrealizedPnl", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserOrders", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "orderIds", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getUserOrdersAtExpiration", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "orderIds", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getUserPosition", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "netQuantity", + "type": "int256" + }, + { + "internalType": "int256", + "name": "netEntryValue", + "type": "int256" + } + ], + "internalType": "struct HashPowerFuturesBase.Position", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "hasRestingOrderDelta", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hook", + "outputs": [ + { + "internalType": "contract IPointsHook", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "_priceOracle", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_liquidationMarginPercent", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "_futureExpirationDatesCount", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_firstFutureExpirationDate", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "liquidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "bytes32[]", + "name": "_orderIds", + "type": "bytes32[]" + } + ], + "name": "liquidateOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_closeQty", + "type": "uint256" + } + ], + "name": "liquidatePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "_expirationAts", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_closeQtys", + "type": "uint256[]" + } + ], + "name": "liquidatePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationFeeBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationMarginPercent", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidatorShareBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "makerFeeBps", + "outputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minimumPriceIncrement", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "portfolioMargin", + "outputs": [ + { + "internalType": "contract IPortfolioMarginEngine", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "priceOracle", + "outputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "name": "recordSettlementPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + }, + { + "internalType": "int256", + "name": "_newQuantity", + "type": "int256" + } + ], + "name": "reduceOrderSize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "_orderIds", + "type": "bytes32[]" + } + ], + "name": "removeOutdatedOrders", + "outputs": [ + { + "internalType": "uint256", + "name": "removed", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "resetParticipantState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "resetState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "_futureExpirationDatesCount", + "type": "uint8" + } + ], + "name": "setFutureExpirationDatesCount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_hook", + "type": "address" + } + ], + "name": "setHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "_bps", + "type": "uint16" + } + ], + "name": "setLiquidationFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "_liquidationMarginPercent", + "type": "uint8" + } + ], + "name": "setLiquidationMarginPercent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "_bps", + "type": "uint16" + } + ], + "name": "setLiquidatorShareBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "_makerFeeBps", + "type": "int16" + } + ], + "name": "setMakerFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "_oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPortfolioMarginEngine", + "name": "_pm", + "type": "address" + } + ], + "name": "setPortfolioMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "_takerFeeBps", + "type": "int16" + } + ], + "name": "setTakerFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "settlePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_users", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_expirationAts", + "type": "uint256[]" + } + ], + "name": "settlePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "settlementPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "_quantity", + "type": "int256" + } + ], + "name": "simulateOrder", + "outputs": [ + { + "internalType": "int256", + "name": "filledQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "averageFillPrice", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "remainingQuantity", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "takerFeeBps", + "outputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "_cancelIds", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "internalType": "int256", + "name": "newQuantity", + "type": "int256" + } + ], + "internalType": "struct HashPowerFuturesBase.ReduceIntent[]", + "name": "_reduces", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "timeInForce", + "type": "uint8" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderIntent[]", + "name": "_intents", + "type": "tuple[]" + } + ], + "name": "updateOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract ICollateralVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawCollectedFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] as const; diff --git a/keeper/src/delivery/coordinator.ts b/keeper/src/delivery/coordinator.ts index bba247b..5082b63 100644 --- a/keeper/src/delivery/coordinator.ts +++ b/keeper/src/delivery/coordinator.ts @@ -8,7 +8,7 @@ import { } from "viem"; import { withUnstickRetry } from "../tx/unstick.ts"; import type pino from "pino"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi } from "../abi/HashPowerFutures.ts"; import type { Chain } from "../chain.ts"; import type { Config } from "../config.ts"; import type { EthUsdFeed } from "../oracle/ethUsdFeed.ts"; @@ -75,13 +75,13 @@ export class DeliveryCoordinator { this.unwatchers.push( this.chain.publicClient.watchContractEvent({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, eventName: "OrderMatched", onLogs: (logs) => this.onOrderMatched(logs), }), this.chain.publicClient.watchContractEvent({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, eventName: "PositionSettled", onLogs: (logs) => this.onPositionSettled(logs), }), @@ -151,14 +151,14 @@ export class DeliveryCoordinator { const [matched, settled] = await Promise.all([ this.chain.publicClient.getContractEvents({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, eventName: "OrderMatched", fromBlock: start, toBlock: end, }), this.chain.publicClient.getContractEvents({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, eventName: "PositionSettled", fromBlock: start, toBlock: end, @@ -258,7 +258,7 @@ export class DeliveryCoordinator { try { expirationAts = (await this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getActiveExpirationDates", args: [user], })) as readonly bigint[]; @@ -271,7 +271,7 @@ export class DeliveryCoordinator { const positions = (await this.chain.publicClient.multicall({ contracts: expirationAts.map((expirationAt) => ({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserPosition" as const, args: [user, expirationAt] as const, })), @@ -389,7 +389,7 @@ export class DeliveryCoordinator { positions.map((pos) => this.chain.publicClient.simulateContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "settlePosition", args: [pos.user, pos.expirationAt], account: this.chain.account, @@ -447,7 +447,7 @@ export class DeliveryCoordinator { hash = await withUnstickRetry(this.chain, this.logger, () => this.chain.walletClient.writeContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "settlePositions", args: [users, expirationAts], account: this.chain.account, @@ -511,7 +511,7 @@ export class DeliveryCoordinator { try { const sim = (await this.chain.publicClient.simulateContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "settlePosition", args, account: this.chain.account, diff --git a/keeper/src/discovery/tracker.ts b/keeper/src/discovery/tracker.ts index a186dc8..b5524b4 100644 --- a/keeper/src/discovery/tracker.ts +++ b/keeper/src/discovery/tracker.ts @@ -10,7 +10,7 @@ import type { Chain } from "../chain.ts"; import type { Config } from "../config.ts"; import { CollateralVaultAbi as collateralVaultAbi } from "collateral-margin-abi/CollateralVault.ts"; import { HashPowerPerpsDEXAbi as perpsAbi } from "derivatives-marketplace-abi/HashPowerPerpsDEX.ts"; -import { FuturesAbi as futuresAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi as futuresAbi } from "../abi/HashPowerFutures.ts"; /** * Set of user addresses with collateral or open positions/orders that the diff --git a/keeper/src/predict/snapshot.ts b/keeper/src/predict/snapshot.ts index 0b18096..0486d1b 100644 --- a/keeper/src/predict/snapshot.ts +++ b/keeper/src/predict/snapshot.ts @@ -4,7 +4,7 @@ import type { Config } from "../config.ts"; import { CollateralVaultAbi } from "collateral-margin-abi/CollateralVault.ts"; import { PortfolioMarginEngineAbi } from "collateral-margin-abi/PortfolioMarginEngine.ts"; import { HashPowerPerpsDEXAbi } from "derivatives-marketplace-abi/HashPowerPerpsDEX.ts"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi } from "../abi/HashPowerFutures.ts"; import type { AccountSnapshot, MMParams } from "@hashpower/portfolio-margin"; import { PerpsPositionAbi } from "../venues/perpsPositionAbi.ts"; @@ -121,19 +121,19 @@ export async function readAccountSnapshot( }, { address: config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getRiskView" as const, args: [user] as const, }, { address: config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getOrderAggregate" as const, args: [user] as const, }, { address: config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getActiveExpirationDates" as const, args: [user] as const, }, @@ -148,13 +148,13 @@ export async function readAccountSnapshot( contracts: [ ...expirationAts.map((expirationAt) => ({ address: config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserPosition" as const, args: [user, expirationAt] as const, })), ...expirationAts.map((expirationAt) => ({ address: config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "settlementPrice" as const, args: [expirationAt] as const, })), diff --git a/keeper/src/venues/futures.ts b/keeper/src/venues/futures.ts index 18b4f08..695aaff 100644 --- a/keeper/src/venues/futures.ts +++ b/keeper/src/venues/futures.ts @@ -2,7 +2,7 @@ import { pad, toHex, type Abi, type Address, type Hex } from "viem"; import type pino from "pino"; import type { Chain } from "../chain.ts"; import type { Config } from "../config.ts"; -import { FuturesAbi } from "futures-marketplace-abi/Futures.ts"; +import { HashPowerFuturesAbi } from "../abi/HashPowerFutures.ts"; import { sendLiquidate } from "../tx/liquidate.ts"; import { readAccountSnapshot, readMMParams } from "../predict/snapshot.ts"; import { type MMParams, solveFuturesClosesToTarget } from "@hashpower/portfolio-margin"; @@ -31,7 +31,7 @@ const LIQUIDATE_ORDERS_ABI = [ ] as const; const FUTURES_LIQUIDATE_ORDERS_ABI = [ - ...FuturesAbi.filter( + ...HashPowerFuturesAbi.filter( (item) => !( typeof item === "object" && @@ -82,7 +82,7 @@ export class FuturesVenue implements Venue { async readOpenOrders(user: Address): Promise { const orderIds = (await this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserOrders", args: [user], })) as readonly Hex[]; @@ -92,7 +92,7 @@ export class FuturesVenue implements Venue { const orders = await this.chain.publicClient.multicall({ contracts: orderIds.map((id) => ({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getOrder" as const, args: [id] as const, })), @@ -109,13 +109,13 @@ export class FuturesVenue implements Venue { const [expirationAts, marketPrice] = await Promise.all([ this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getActiveExpirationDates", args: [user], }) as Promise, this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getMarketPrice", }) as Promise, ]); @@ -125,7 +125,7 @@ export class FuturesVenue implements Venue { const positions = await this.chain.publicClient.multicall({ contracts: expirationAts.map((expirationAt) => ({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserPosition" as const, args: [user, expirationAt] as const, })), @@ -162,7 +162,7 @@ export class FuturesVenue implements Venue { if (targetIds === undefined) { targetIds = (await this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserOrders", args: [user], })) as readonly Hex[]; @@ -194,7 +194,7 @@ export class FuturesVenue implements Venue { this.getMMParams(), this.chain.publicClient.readContract({ address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getMarketPrice", }) as Promise, ]); @@ -229,7 +229,7 @@ export class FuturesVenue implements Venue { config: this.config, logger: this.logger, address: this.config.futures.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "liquidatePositions", args: [user, expirationAts, closeQtys], feeEventName: "PositionLiquidated", diff --git a/keeper/tests/integration/artifacts.ts b/keeper/tests/integration/artifacts.ts index 1266b19..97de775 100644 --- a/keeper/tests/integration/artifacts.ts +++ b/keeper/tests/integration/artifacts.ts @@ -126,7 +126,12 @@ export const artifacts = { multicall3: () => readArtifact("perps", "contracts/Multicall3", "Multicall3"), // ── futures (sibling repo) ──────────────────────────────────────────── - futures: () => readArtifact("futures", "contracts/Futures", "Futures"), + futures: () => + readArtifact( + "futures", + "contracts/HashPowerFutures", + "HashPowerFutures", + ), } as const; /** Resolved repo paths — exported for diagnostic logs. */ diff --git a/market-maker/package.json b/market-maker/package.json index 6963482..85c84f8 100644 --- a/market-maker/package.json +++ b/market-maker/package.json @@ -39,7 +39,6 @@ "amaro": "^1.1.9", "collateral-margin-contracts": "github:Lumerin-protocol/collateral-margin#dev&path:/contracts", "fraction.js": "^5.2.2", - "futures-contracts": "github:Lumerin-protocol/futures-marketplace#547ade13eb944b0e86bbef325749ed627cc886b4&path:/contracts", "js-yaml": "^4.1.0", "perps-contracts": "github:Lumerin-protocol/derivatives-marketplace#f7e219f704646ab654a2a8d0286c0f477148e299&path:/contracts", "pino": "^10.3.1", diff --git a/market-maker/pnpm-lock.yaml b/market-maker/pnpm-lock.yaml index 6457c1c..de471e3 100644 --- a/market-maker/pnpm-lock.yaml +++ b/market-maker/pnpm-lock.yaml @@ -26,9 +26,6 @@ importers: fraction.js: specifier: ^5.2.2 version: 5.3.4 - futures-contracts: - specifier: github:Lumerin-protocol/futures-marketplace#547ade13eb944b0e86bbef325749ed627cc886b4&path:/contracts - version: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts(@types/node@22.19.17)(ethers@5.8.0)(typescript@5.9.3) js-yaml: specifier: ^4.1.0 version: 4.1.1 @@ -459,10 +456,6 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@multicall/multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012': - resolution: {tarball: https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012} - version: 0.0.0 - '@noble/ciphers@1.3.0': resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} engines: {node: ^14.21.3 || >=16} @@ -638,31 +631,9 @@ packages: resolution: {integrity: sha512-m6iorjyhPK9ow5/trNs7qsBC/SOzJCO51pvvAF2W9nOiZ1t0RtCd+rlRmRmlWTv4M33V0wzIUeamJ2BPbzgUXA==} hasBin: true - '@peculiar/asn1-schema@2.7.0': - resolution: {integrity: sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==} - - '@peculiar/utils@2.0.3': - resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} - '@safe-global/api-kit@5.0.1': - resolution: {integrity: sha512-nbkSjrRTZh+LlD7e0A4fujKgLZnU5262e1iYTWsGx4ft6oo5kTAxc5pY0icls5PjmZI0UkMhYIv5izeBP7bcWg==} - - '@safe-global/protocol-kit@8.0.4': - resolution: {integrity: sha512-j6PXStIUYEGATdyE8CWMzE1uVAkDBta3o+3atwPTKVE5OIWuqk+QQ7s4kbm1x42yaMf9e5e7LxIJ8E8WmI3i0g==} - - '@safe-global/safe-deployments@1.37.60': - resolution: {integrity: sha512-PS+VLRm+FikrUNOutsIEE4Ytbtb+O57SHo8VSzYynR7lcSY1gfZ+7DUnKKLVDGCspoJIgh99jqJWGh9eqfn46Q==} - engines: {node: '>=22.0.0', pnpm: '>=10.16.0'} - - '@safe-global/safe-modules-deployments@3.0.8': - resolution: {integrity: sha512-RKct6dNFg4KbbpXchyosHmIygy58wLe+1WIATQ2VILGpcivBDcZdyvSl64BoocXCL2CroCBMJMzy0mW4iwZ0Zg==} - - '@safe-global/types-kit@4.0.1': - resolution: {integrity: sha512-zmIYyAH9mcBcqHszPgcfNjOJYuPSvWCcc/f8zeznh7N1HSA7jEoFErO06O4QDfwAAS4aEyoyjhPyfHbVYRheZg==} - '@scroll-tech/contracts@2.0.0': resolution: {integrity: sha512-O8sVaA/bVKH/mp+bBfUjZ/vYr5mdBExCpKRLre4r9TbXTtiaY9Uo5xU8dcG3weLxyK0BZqDTP2aCNp4Q0f7SeA==} @@ -1071,10 +1042,6 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - asn1js@3.0.10: - resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} - engines: {node: '>=12.0.0'} - assertion-error@1.1.0: resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} @@ -1550,11 +1517,6 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - futures-contracts@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts: - resolution: {path: /contracts, tarball: https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4} - version: 1.0.0 - engines: {node: 24.x} - get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1913,6 +1875,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012: + resolution: {tarball: https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012} + version: 0.0.0 + nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} @@ -2116,13 +2082,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - pvtsutils@1.3.6: - resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} - - pvutils@1.1.5: - resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} - engines: {node: '>=16.0.0'} - quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -3540,8 +3499,6 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@multicall/multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012': {} - '@noble/ciphers@1.3.0': {} '@noble/curves@1.4.2': @@ -3737,63 +3694,8 @@ snapshots: transitivePeerDependencies: - supports-color - '@peculiar/asn1-schema@2.7.0': - dependencies: - '@peculiar/utils': 2.0.3 - asn1js: 3.0.10 - tslib: 2.8.1 - optional: true - - '@peculiar/utils@2.0.3': - dependencies: - tslib: 2.8.1 - optional: true - '@pinojs/redact@0.4.0': {} - '@safe-global/api-kit@5.0.1(typescript@5.9.3)': - dependencies: - '@safe-global/protocol-kit': 8.0.4(typescript@5.9.3) - '@safe-global/types-kit': 4.0.1(typescript@5.9.3) - node-fetch: 2.7.0 - viem: 2.55.5(typescript@5.9.3) - transitivePeerDependencies: - - bufferutil - - encoding - - typescript - - utf-8-validate - - zod - - '@safe-global/protocol-kit@8.0.4(typescript@5.9.3)': - dependencies: - '@safe-global/safe-deployments': 1.37.60 - '@safe-global/safe-modules-deployments': 3.0.8 - '@safe-global/types-kit': 4.0.1(typescript@5.9.3) - abitype: 1.2.3(typescript@5.9.3) - semver: 7.8.5 - viem: 2.55.5(typescript@5.9.3) - optionalDependencies: - '@noble/curves': 1.9.1 - '@peculiar/asn1-schema': 2.7.0 - transitivePeerDependencies: - - bufferutil - - typescript - - utf-8-validate - - zod - - '@safe-global/safe-deployments@1.37.60': - dependencies: - semver: 7.8.5 - - '@safe-global/safe-modules-deployments@3.0.8': {} - - '@safe-global/types-kit@4.0.1(typescript@5.9.3)': - dependencies: - abitype: 1.2.3(typescript@5.9.3) - transitivePeerDependencies: - - typescript - - zod - '@scroll-tech/contracts@2.0.0': {} '@scure/base@1.1.9': {} @@ -4326,13 +4228,6 @@ snapshots: array-union@2.1.0: {} - asn1js@3.0.10: - dependencies: - pvtsutils: 1.3.6 - pvutils: 1.1.5 - tslib: 2.8.1 - optional: true - assertion-error@1.1.0: {} async-retry@1.3.3: @@ -4622,14 +4517,14 @@ snapshots: derivatives-contracts@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299#path:/contracts(@nomicfoundation/hardhat-ethers@3.1.3(ethers@5.8.0)(hardhat@2.28.6(typescript@5.9.3)))(@types/node@22.19.17)(ethers@5.8.0)(hardhat@2.28.6(typescript@5.9.3)): dependencies: '@chainlink/contracts': 1.5.0(@types/node@22.19.17)(ethers@5.8.0) - '@multicall/multicall3': https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012 + '@multicall/multicall3': multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012 '@noble/curves': 1.9.1 '@openzeppelin/contracts': 5.1.0 '@openzeppelin/contracts-upgradeable': 5.1.0(@openzeppelin/contracts@5.1.0) '@openzeppelin/hardhat-upgrades': 3.9.1(@nomicfoundation/hardhat-ethers@3.1.3(ethers@5.8.0)(hardhat@2.28.6(typescript@5.9.3)))(ethers@5.8.0)(hardhat@2.28.6(typescript@5.9.3)) collateral-margin: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/9372f537a57682bb126aede41aac51be004828a6 hashprice-oracle: https://codeload.github.com/Lumerin-protocol/hashprice-oracle/tar.gz/b65adbfeb7e6c4417747bfd3d94b6e89e162ed50 - multicall3: '@multicall/multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012' + multicall3: https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012 solidity-linked-list: 6.5.0 transitivePeerDependencies: - '@nomicfoundation/hardhat-ethers' @@ -4903,29 +4798,6 @@ snapshots: function-bind@1.1.2: {} - futures-contracts@https://codeload.github.com/Lumerin-protocol/futures-marketplace/tar.gz/547ade13eb944b0e86bbef325749ed627cc886b4#path:/contracts(@types/node@22.19.17)(ethers@5.8.0)(typescript@5.9.3): - dependencies: - '@chainlink/contracts': 1.5.0(@types/node@22.19.17)(ethers@5.8.0) - '@noble/curves': 1.9.1 - '@openzeppelin/contracts': 5.1.0 - '@openzeppelin/contracts-upgradeable': 5.1.0(@openzeppelin/contracts@5.1.0) - '@safe-global/api-kit': 5.0.1(typescript@5.9.3) - '@safe-global/protocol-kit': 8.0.4(typescript@5.9.3) - '@safe-global/types-kit': 4.0.1(typescript@5.9.3) - collateral-margin: link:.. - multicall3: '@multicall/multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012' - solidity-linked-list: 6.5.0 - viem: 2.55.5(typescript@5.9.3) - transitivePeerDependencies: - - '@types/node' - - bufferutil - - encoding - - ethers - - supports-color - - typescript - - utf-8-validate - - zod - get-caller-file@2.0.5: {} get-func-name@2.0.2: {} @@ -5334,6 +5206,8 @@ snapshots: ms@2.1.3: {} + multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012: {} + nice-try@1.0.5: {} node-addon-api@2.0.2: {} @@ -5542,14 +5416,6 @@ snapshots: punycode@2.3.1: {} - pvtsutils@1.3.6: - dependencies: - tslib: 2.8.1 - optional: true - - pvutils@1.1.5: - optional: true - quansync@0.2.11: {} queue-microtask@1.2.3: {} diff --git a/market-maker/src/abi/HashPowerFutures.ts b/market-maker/src/abi/HashPowerFutures.ts new file mode 100644 index 0000000..8e40a77 --- /dev/null +++ b/market-maker/src/abi/HashPowerFutures.ts @@ -0,0 +1,2128 @@ +export const HashPowerFuturesAbi = [ + { + "inputs": [ + { + "internalType": "contract ICollateralVault", + "name": "_vault", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + } + ], + "name": "AddressEmptyCode", + "type": "error" + }, + { + "inputs": [], + "name": "ArrayLengthMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "ERC1967InvalidImplementation", + "type": "error" + }, + { + "inputs": [], + "name": "ERC1967NonPayable", + "type": "error" + }, + { + "inputs": [], + "name": "EmptyBatch", + "type": "error" + }, + { + "inputs": [], + "name": "ExpirationDateNotAvailable", + "type": "error" + }, + { + "inputs": [], + "name": "ExpirationDateShouldBeInTheFuture", + "type": "error" + }, + { + "inputs": [], + "name": "FailedCall", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientMarginBalance", + "type": "error" + }, + { + "inputs": [], + "name": "InsuranceFundNotConfigured", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidDependency", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidFee", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInitialization", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidOracle", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPrice", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidQty", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidReduceQuantity", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidTimeInForce", + "type": "error" + }, + { + "inputs": [], + "name": "MaxOrdersPerParticipantPerExpirationReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPriceLevelsReached", + "type": "error" + }, + { + "inputs": [], + "name": "MaxPriceLevelsReached", + "type": "error" + }, + { + "inputs": [], + "name": "NotInitializing", + "type": "error" + }, + { + "inputs": [], + "name": "NotLiquidatable", + "type": "error" + }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotBelongToSender", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotBelongToUser", + "type": "error" + }, + { + "inputs": [], + "name": "OrderNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "OrdersStillOpen", + "type": "error" + }, + { + "inputs": [], + "name": "OverLiquidation", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "OwnableInvalidOwner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "OwnableUnauthorizedAccount", + "type": "error" + }, + { + "inputs": [], + "name": "PositionExpirationNotStartedYet", + "type": "error" + }, + { + "inputs": [], + "name": "PositionNotExists", + "type": "error" + }, + { + "inputs": [], + "name": "SettlementDateNotReached", + "type": "error" + }, + { + "inputs": [], + "name": "TimeInForceNotFilled", + "type": "error" + }, + { + "inputs": [], + "name": "UUPSUnauthorizedCallContext", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "slot", + "type": "bytes32" + } + ], + "name": "UUPSUnsupportedProxiableUUID", + "type": "error" + }, + { + "inputs": [], + "name": "UnsupportedTokenDecimals", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "min", + "type": "int256" + }, + { + "internalType": "int256", + "name": "max", + "type": "int256" + } + ], + "name": "ValueOutOfRange", + "type": "error" + }, + { + "inputs": [], + "name": "VaultMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "BadDebt", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "newFutureExpirationDatesCount", + "type": "uint8" + } + ], + "name": "FutureExpirationDatesCountUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "hook", + "type": "address" + } + ], + "name": "HookUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "version", + "type": "uint64" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newLiquidationFeeBps", + "type": "uint16" + } + ], + "name": "LiquidationFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "newLiquidationMarginPercent", + "type": "uint8" + } + ], + "name": "LiquidationMarginPercentUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint16", + "name": "newLiquidatorShareBps", + "type": "uint16" + } + ], + "name": "LiquidatorShareBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int16", + "name": "newMakerFeeBps", + "type": "int16" + } + ], + "name": "MakerFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newOracle", + "type": "address" + } + ], + "name": "OracleUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + } + ], + "name": "OrderCancelled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "name": "OrderCreated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "fee", + "type": "uint256" + } + ], + "name": "OrderLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "makerOrderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "taker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "tradePrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "makerFee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerFee", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "makerNetQtyAfter", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "takerNetQtyAfter", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "makerEntryPriceAfter", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "takerEntryPriceAfter", + "type": "uint256" + } + ], + "name": "OrderMatched", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "indexed": false, + "internalType": "int256", + "name": "newQuantity", + "type": "int256" + } + ], + "name": "OrderUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "address", + "name": "newPortfolioMargin", + "type": "address" + } + ], + "name": "PortfolioMarginUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "liquidator", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "closedQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "liquidatorFee", + "type": "uint256" + } + ], + "name": "PositionLiquidated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "user", + "type": "address" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "closedQuantity", + "type": "int256" + }, + { + "indexed": false, + "internalType": "int256", + "name": "pnl", + "type": "int256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "settlementPrice", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "settledBy", + "type": "address" + } + ], + "name": "PositionSettled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "recordedBy", + "type": "address" + } + ], + "name": "SettlementPriceRecorded", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "int16", + "name": "newTakerFeeBps", + "type": "int16" + } + ], + "name": "TakerFeeBpsUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "implementation", + "type": "address" + } + ], + "name": "Upgraded", + "type": "event" + }, + { + "inputs": [], + "name": "CONTRACT_SIZE_HPS_DAY", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "EXPIRATION_INTERVAL_DAYS", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_ORACLE_STALENESS", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_ORDERS_PER_PARTICIPANT_PER_EXPIRATION", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "MAX_PRICE_LEVELS_PER_SIDE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "QUANTITY_DECIMALS", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "UPGRADE_INTERFACE_VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "VERSION", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "cancelOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "collectedFeesBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "_quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "_tif", + "type": "uint8" + } + ], + "name": "createOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "timeInForce", + "type": "uint8" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderIntent[]", + "name": "_intents", + "type": "tuple[]" + } + ], + "name": "createOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "dropActiveOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "expirationIntervalDays", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "firstFutureExpirationDate", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "futureExpirationDatesCount", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getActiveExpirationDates", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getBestAskPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getBestBidPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getExpirationDates", + "outputs": [ + { + "internalType": "uint256[]", + "name": "", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "getMarketPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getNetPositionDelta", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "getOrder", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "participant", + "type": "address" + }, + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.Order", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getOrderAggregate", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "buyQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellValue", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderAggregate", + "name": "aggregate_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getOrderAggregateAtExpiration", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "buyQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellQty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyValue", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellValue", + "type": "uint256" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderAggregate", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_maxLevels", + "type": "uint256" + } + ], + "name": "getOrderBookPrices", + "outputs": [ + { + "internalType": "uint256[]", + "name": "bids", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "asks", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "_isBid", + "type": "bool" + } + ], + "name": "getQuantityAtPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getRiskView", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "netPositionDelta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "unrealizedPnl", + "type": "int256" + }, + { + "internalType": "int256", + "name": "pendingFunding", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "buyOrderDelta", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellOrderDelta", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "buyOrderFillLoss", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "sellOrderFillLoss", + "type": "uint256" + } + ], + "internalType": "struct ILinearMarket.RiskView", + "name": "view_", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "getUnrealizedPnl", + "outputs": [ + { + "internalType": "int256", + "name": "", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + } + ], + "name": "getUserOrders", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "orderIds", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getUserOrdersAtExpiration", + "outputs": [ + { + "internalType": "bytes32[]", + "name": "orderIds", + "type": "bytes32[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "getUserPosition", + "outputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "netQuantity", + "type": "int256" + }, + { + "internalType": "int256", + "name": "netEntryValue", + "type": "int256" + } + ], + "internalType": "struct HashPowerFuturesBase.Position", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "hasRestingOrderDelta", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "hook", + "outputs": [ + { + "internalType": "contract IPointsHook", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "_priceOracle", + "type": "address" + }, + { + "internalType": "uint8", + "name": "_liquidationMarginPercent", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "_futureExpirationDatesCount", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "_firstFutureExpirationDate", + "type": "uint256" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_participant", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + } + ], + "name": "liquidateOrder", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "bytes32[]", + "name": "_orderIds", + "type": "bytes32[]" + } + ], + "name": "liquidateOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_closeQty", + "type": "uint256" + } + ], + "name": "liquidatePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256[]", + "name": "_expirationAts", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "_closeQtys", + "type": "uint256[]" + } + ], + "name": "liquidatePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationFeeBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidationMarginPercent", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "liquidatorShareBps", + "outputs": [ + { + "internalType": "uint16", + "name": "", + "type": "uint16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "makerFeeBps", + "outputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "minimumPriceIncrement", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "portfolioMargin", + "outputs": [ + { + "internalType": "contract IPortfolioMarginEngine", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "priceOracle", + "outputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proxiableUUID", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + } + ], + "name": "recordSettlementPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_orderId", + "type": "bytes32" + }, + { + "internalType": "int256", + "name": "_newQuantity", + "type": "int256" + } + ], + "name": "reduceOrderSize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "_orderIds", + "type": "bytes32[]" + } + ], + "name": "removeOutdatedOrders", + "outputs": [ + { + "internalType": "uint256", + "name": "removed", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "resetParticipantState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_participants", + "type": "address[]" + } + ], + "name": "resetState", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "_futureExpirationDatesCount", + "type": "uint8" + } + ], + "name": "setFutureExpirationDatesCount", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_hook", + "type": "address" + } + ], + "name": "setHook", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "_bps", + "type": "uint16" + } + ], + "name": "setLiquidationFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "_liquidationMarginPercent", + "type": "uint8" + } + ], + "name": "setLiquidationMarginPercent", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint16", + "name": "_bps", + "type": "uint16" + } + ], + "name": "setLiquidatorShareBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "_makerFeeBps", + "type": "int16" + } + ], + "name": "setMakerFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract AggregatorV3Interface", + "name": "_oracle", + "type": "address" + } + ], + "name": "setOracle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "contract IPortfolioMarginEngine", + "name": "_pm", + "type": "address" + } + ], + "name": "setPortfolioMargin", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int16", + "name": "_takerFeeBps", + "type": "int16" + } + ], + "name": "setTakerFeeBps", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_user", + "type": "address" + }, + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + } + ], + "name": "settlePosition", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "_users", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "_expirationAts", + "type": "uint256[]" + } + ], + "name": "settlePositions", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "settlementPrice", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_expirationAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_price", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "_quantity", + "type": "int256" + } + ], + "name": "simulateOrder", + "outputs": [ + { + "internalType": "int256", + "name": "filledQuantity", + "type": "int256" + }, + { + "internalType": "uint256", + "name": "averageFillPrice", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "remainingQuantity", + "type": "int256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "takerFeeBps", + "outputs": [ + { + "internalType": "int16", + "name": "", + "type": "int16" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "_cancelIds", + "type": "bytes32[]" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "orderId", + "type": "bytes32" + }, + { + "internalType": "int256", + "name": "newQuantity", + "type": "int256" + } + ], + "internalType": "struct HashPowerFuturesBase.ReduceIntent[]", + "name": "_reduces", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "price", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expirationAt", + "type": "uint256" + }, + { + "internalType": "int256", + "name": "quantity", + "type": "int256" + }, + { + "internalType": "enum HashPowerFuturesBase.TimeInForce", + "name": "timeInForce", + "type": "uint8" + } + ], + "internalType": "struct HashPowerFuturesBase.OrderIntent[]", + "name": "_intents", + "type": "tuple[]" + } + ], + "name": "updateOrders", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newImplementation", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeToAndCall", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "vault", + "outputs": [ + { + "internalType": "contract ICollateralVault", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "withdrawCollectedFees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } +] as const; diff --git a/market-maker/src/adapters/futures/events.ts b/market-maker/src/adapters/futures/events.ts index c8074df..408d997 100644 --- a/market-maker/src/adapters/futures/events.ts +++ b/market-maker/src/adapters/futures/events.ts @@ -4,7 +4,7 @@ import type { VenueEvent, VenueEvents, } from "../../core/adapter.ts"; -import { FuturesAbi } from "futures-contracts/abi/Futures"; +import { HashPowerFuturesAbi } from "../../abi/HashPowerFutures.ts"; /** Instrument id for a futures expiry, e.g. `futures:1893456000`. */ export function futuresInstrumentId(expirationAt: bigint): string { @@ -17,7 +17,7 @@ type FuturesLog = Log< false, undefined, false, - typeof FuturesAbi + typeof HashPowerFuturesAbi >; /** Multiplexes one viem watcher across many subscribers. Decode-only. */ @@ -45,7 +45,7 @@ export class FuturesVenueEvents implements VenueEvents { private attachWatcher(): void { this.unwatch = this.publicClient.watchContractEvent({ address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, onLogs: (logs) => { for (const log of logs) { const evt = decodeEvent(log as FuturesLog); diff --git a/market-maker/src/adapters/futures/instrument.ts b/market-maker/src/adapters/futures/instrument.ts index c43151d..7ee8821 100644 --- a/market-maker/src/adapters/futures/instrument.ts +++ b/market-maker/src/adapters/futures/instrument.ts @@ -14,7 +14,7 @@ import type { ReduceIntent, } from "../../core/adapter.ts"; import { TimeInForce } from "../../core/adapter.ts"; -import { FuturesAbi } from "futures-contracts/abi/Futures"; +import { HashPowerFuturesAbi } from "../../abi/HashPowerFutures.ts"; import { fillLossFromNotionals } from "../../core/math.ts"; import type { FuturesVenueAdapter } from "./venue.ts"; import { FuturesOwnOrders } from "./ownOrders.ts"; @@ -63,7 +63,7 @@ export class FuturesInstrumentAdapter implements InstrumentAdapter { async getPosition(): Promise { const pos = await this.venue.publicClient.readContract({ address: this.venue.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getUserPosition", args: [this.venue.wallet.account.address, this.expirationAt], }); @@ -181,7 +181,7 @@ export class FuturesInstrumentAdapter implements InstrumentAdapter { encodeCancel(intent: CancelIntent): `0x${string}` { return encodeFunctionData({ - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "cancelOrder", args: [intent.orderId], }); @@ -297,7 +297,7 @@ export class FuturesInstrumentAdapter implements InstrumentAdapter { try { return await this.venue.publicClient.estimateContractGas({ address: this.venue.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "createOrder", // Futures 3.0: createOrder(price, expirationAt, signedQuantity, timeInForce) args: [1_000_000n, this.expirationAt, 1n, 0], @@ -312,7 +312,7 @@ export class FuturesInstrumentAdapter implements InstrumentAdapter { if (this.tickCache !== null) return this.tickCache; const tick = await this.venue.publicClient.readContract({ address: this.venue.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "minimumPriceIncrement", }); this.tickCache = tick; @@ -341,7 +341,7 @@ class FuturesBook implements BookSource { // Same shape as perps `getOrderBookPrices(depth)`, with expirationAt first. const [bidPrices, askPrices] = await v.publicClient.readContract({ address: v.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getOrderBookPrices", args: [expirationAt, depth], }); @@ -351,13 +351,13 @@ class FuturesBook implements BookSource { const allCalls = [ ...bidPrices.map((p) => ({ address: v.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getQuantityAtPrice" as const, args: [expirationAt, p, true] as const, })), ...askPrices.map((p) => ({ address: v.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getQuantityAtPrice" as const, args: [expirationAt, p, false] as const, })), diff --git a/market-maker/src/adapters/futures/ownOrders.ts b/market-maker/src/adapters/futures/ownOrders.ts index 1ae7421..20ddcfe 100644 --- a/market-maker/src/adapters/futures/ownOrders.ts +++ b/market-maker/src/adapters/futures/ownOrders.ts @@ -5,7 +5,7 @@ import type { OwnOrderSource, Unsubscribe, } from "../../core/adapter.ts"; -import { FuturesAbi } from "futures-contracts/abi/Futures"; +import { HashPowerFuturesAbi } from "../../abi/HashPowerFutures.ts"; import type { FuturesVenueAdapter } from "./venue.ts"; import { futuresInstrumentId } from "./events.ts"; @@ -96,7 +96,7 @@ export class FuturesOwnOrders implements OwnOrderSource { const allCalls = orderIds.map((id) => ({ address: this.venue.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getOrder" as const, args: [id] as const, })); diff --git a/market-maker/src/adapters/futures/venue.ts b/market-maker/src/adapters/futures/venue.ts index ec4c86a..19bb9d5 100644 --- a/market-maker/src/adapters/futures/venue.ts +++ b/market-maker/src/adapters/futures/venue.ts @@ -12,7 +12,7 @@ import type { WalletContext, } from "../../core/adapter.ts"; import type { NetworkClients } from "../../core/client.ts"; -import { FuturesAbi } from "futures-contracts/abi/Futures"; +import { HashPowerFuturesAbi } from "../../abi/HashPowerFutures.ts"; import { CollateralVaultAbi } from "collateral-margin-contracts/abi/CollateralVault.ts"; import { PortfolioMarginEngineAbi } from "collateral-margin-contracts/abi/PortfolioMarginEngine.ts"; import { Multicall3Abi } from "perps-contracts/abi/Multicall3.ts"; @@ -127,7 +127,7 @@ export class FuturesVenueAdapter implements VenueAdapter { const { token } = await this.resolveAddresses(); const oracle = await this.publicClient.readContract({ address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "priceOracle", }); const [oracleDecimals, tokenDecimals] = await this.publicClient.multicall({ @@ -219,7 +219,7 @@ export class FuturesVenueAdapter implements VenueAdapter { private async readExpirationAts(): Promise { const dates = await this.publicClient.readContract({ address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "getExpirationDates", }); return [...dates]; @@ -283,12 +283,12 @@ export class FuturesVenueAdapter implements VenueAdapter { contracts: [ { address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "vault", }, { address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "portfolioMargin", }, ], @@ -344,7 +344,7 @@ export class FuturesVenueAdapter implements VenueAdapter { } const liqMarginPct = await this.publicClient.readContract({ address: this.address, - abi: FuturesAbi, + abi: HashPowerFuturesAbi, functionName: "liquidationMarginPercent", }); this.marginPercentCache = BigInt(liqMarginPct); @@ -404,7 +404,12 @@ class FuturesCollateralAccount implements BatchableCollateralAccount { ] as MarginReadPlan["shared"]; const venue = [ - { address: this.venue.address, abi: FuturesAbi, functionName: "getUnrealizedPnl", args: [owner] }, + { + address: this.venue.address, + abi: HashPowerFuturesAbi, + functionName: "getUnrealizedPnl", + args: [owner], + }, ] as MarginReadPlan["venue"]; const decode = (results: readonly unknown[]): CollateralSnapshot => { From fbc4515fbbe571f1de70cc26628d65213178cc48 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 13:46:29 +0200 Subject: [PATCH 15/26] fix(futures): sync consumer ABI Co-authored-by: Cursor --- keeper/src/abi/HashPowerFutures.ts | 13 ------------- market-maker/src/abi/HashPowerFutures.ts | 13 ------------- 2 files changed, 26 deletions(-) diff --git a/keeper/src/abi/HashPowerFutures.ts b/keeper/src/abi/HashPowerFutures.ts index 8e40a77..88f9399 100644 --- a/keeper/src/abi/HashPowerFutures.ts +++ b/keeper/src/abi/HashPowerFutures.ts @@ -1767,19 +1767,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_participants", - "type": "address[]" - } - ], - "name": "resetParticipantState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { diff --git a/market-maker/src/abi/HashPowerFutures.ts b/market-maker/src/abi/HashPowerFutures.ts index 8e40a77..88f9399 100644 --- a/market-maker/src/abi/HashPowerFutures.ts +++ b/market-maker/src/abi/HashPowerFutures.ts @@ -1767,19 +1767,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address[]", - "name": "_participants", - "type": "address[]" - } - ], - "name": "resetParticipantState", - "outputs": [], - "stateMutability": "nonpayable", - "type": "function" - }, { "inputs": [ { From ecb877be5e41ca5815f98f629dd6524e93fd4658 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 14:05:00 +0200 Subject: [PATCH 16/26] fix(keeper): read futures orders via AtExpiration multicalls Follow the thinner futures ABI: discover ids and aggregates over getExpirationDates instead of removed cross-expiry getters. Co-authored-by: Cursor --- keeper/src/abi/HashPowerFutures.ts | 117 ------------------ keeper/src/predict/snapshot.ts | 62 +++++++--- keeper/src/venues/futures.ts | 45 +++++-- keeper/src/venues/types.ts | 3 +- keeper/tests/integration/helpers.ts | 24 +++- keeper/tests/predict/coordinator.test.ts | 4 + .../tests/predict/coordinatorAlerts.test.ts | 4 + keeper/tests/predict/snapshot.test.ts | 32 ++++- keeper/tests/venues/futures.test.ts | 36 ++++-- keeper/tests/venues/reduceToTarget.test.ts | 26 ++-- market-maker/src/abi/HashPowerFutures.ts | 117 ------------------ 11 files changed, 184 insertions(+), 286 deletions(-) diff --git a/keeper/src/abi/HashPowerFutures.ts b/keeper/src/abi/HashPowerFutures.ts index 88f9399..d2b2255 100644 --- a/keeper/src/abi/HashPowerFutures.ts +++ b/keeper/src/abi/HashPowerFutures.ts @@ -1003,44 +1003,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_expirationAt", - "type": "uint256" - } - ], - "name": "getBestAskPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_expirationAt", - "type": "uint256" - } - ], - "name": "getBestBidPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getExpirationDates", @@ -1067,25 +1029,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_participant", - "type": "address" - } - ], - "name": "getNetPositionDelta", - "outputs": [ - { - "internalType": "int256", - "name": "", - "type": "int256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -1127,47 +1070,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_user", - "type": "address" - } - ], - "name": "getOrderAggregate", - "outputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "buyQty", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "sellQty", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "buyValue", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "sellValue", - "type": "uint256" - } - ], - "internalType": "struct HashPowerFuturesBase.OrderAggregate", - "name": "aggregate_", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -1347,25 +1249,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_user", - "type": "address" - } - ], - "name": "getUserOrders", - "outputs": [ - { - "internalType": "bytes32[]", - "name": "orderIds", - "type": "bytes32[]" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/keeper/src/predict/snapshot.ts b/keeper/src/predict/snapshot.ts index 0486d1b..d1137f2 100644 --- a/keeper/src/predict/snapshot.ts +++ b/keeper/src/predict/snapshot.ts @@ -64,19 +64,20 @@ export async function readMMParams( * Read everything needed to evaluate `mmSurplus(P)` for a single user as a * function of price. Two RPC round-trips: * - * 1. Bulk multicall: balance, both venues' `getRiskView` / `getOrderAggregate`, - * the perp position, futures activeExpirationAts. - * 2. Per-expiry multicall: hydrate each aggregate via `getUserPosition`, plus - * its `settlementPrice` — an expiry that has settled but not yet been swept - * out of the active set is marked at that pinned price and carries no delta, - * so the predictor cannot treat it like a live leg. + * 1. Bulk multicall: balance, perps risk/aggregate/position, futures risk, + * futures active position expiries, and the tradable delivery window. + * 2. Per-expiry multicall: hydrate each futures position via `getUserPosition` + * + `settlementPrice`, and sum `getOrderAggregateAtExpiration` over the + * tradable window for unclamped limit-price totals. * - * Round-trip 2 collapses to zero calls when the user has no futures - * positions (the common case for perps-only users). + * Round-trip 2 collapses to zero position/settlement calls when the user has + * no futures positions (the common case for perps-only users). Order-aggregate + * calls still run when the tradable window is non-empty. * * `getRiskView` carries the per-side order delta but reports fill loss only at the * current mark, and the clamp makes that non-invertible once it reads zero — so the - * per-side limit-price totals come from `getOrderAggregate` and the predictor derives + * per-side limit-price totals come from the order-aggregate cache (perps: + * `getOrderAggregate`; futures: summed AtExpiration) and the predictor derives * fill loss at whatever price it is evaluating. Pending funding also rides in * `getRiskView`, replacing the separate `getPendingFunding` read. */ @@ -91,8 +92,8 @@ export async function readAccountSnapshot( perpRisk, perpOrderAggregate, futuresRisk, - futuresOrderAggregate, activeExpirationAts, + tradableExpirationAts, ] = await chain.publicClient.multicall({ contracts: [ { @@ -128,22 +129,36 @@ export async function readAccountSnapshot( { address: config.futures.address, abi: HashPowerFuturesAbi, - functionName: "getOrderAggregate" as const, + functionName: "getActiveExpirationDates" as const, args: [user] as const, }, { address: config.futures.address, abi: HashPowerFuturesAbi, - functionName: "getActiveExpirationDates" as const, - args: [user] as const, + functionName: "getExpirationDates" as const, }, ] as const, allowFailure: false, }); const expirationAts = activeExpirationAts as readonly bigint[]; + const orderExpirationAts = tradableExpirationAts as readonly bigint[]; const futuresPositions: AccountSnapshot["futures"]["positions"] = []; - if (expirationAts.length > 0) { + + type OrderAggregate = { + buyQty: bigint; + sellQty: bigint; + buyValue: bigint; + sellValue: bigint; + }; + let futuresOrderAggregate: OrderAggregate = { + buyQty: 0n, + sellQty: 0n, + buyValue: 0n, + sellValue: 0n, + }; + + if (expirationAts.length > 0 || orderExpirationAts.length > 0) { const perExpiry = await chain.publicClient.multicall({ contracts: [ ...expirationAts.map((expirationAt) => ({ @@ -158,9 +173,16 @@ export async function readAccountSnapshot( functionName: "settlementPrice" as const, args: [expirationAt] as const, })), + ...orderExpirationAts.map((expirationAt) => ({ + address: config.futures.address, + abi: HashPowerFuturesAbi, + functionName: "getOrderAggregateAtExpiration" as const, + args: [user, expirationAt] as const, + })), ], allowFailure: false, }); + for (let i = 0; i < expirationAts.length; i++) { const pos = perExpiry[i] as { netQuantity: bigint; netEntryValue: bigint } | undefined; const settlementPrice = perExpiry[expirationAts.length + i] as bigint | undefined; @@ -174,6 +196,18 @@ export async function readAccountSnapshot( settlementPrice: settlementPrice ?? 0n, }); } + + const orderOffset = expirationAts.length * 2; + for (let i = 0; i < orderExpirationAts.length; i++) { + const aggregate = perExpiry[orderOffset + i] as OrderAggregate | undefined; + if (aggregate === undefined) continue; + futuresOrderAggregate = { + buyQty: futuresOrderAggregate.buyQty + aggregate.buyQty, + sellQty: futuresOrderAggregate.sellQty + aggregate.sellQty, + buyValue: futuresOrderAggregate.buyValue + aggregate.buyValue, + sellValue: futuresOrderAggregate.sellValue + aggregate.sellValue, + }; + } } const funding = perpRisk.pendingFunding; diff --git a/keeper/src/venues/futures.ts b/keeper/src/venues/futures.ts index 695aaff..b5b7aba 100644 --- a/keeper/src/venues/futures.ts +++ b/keeper/src/venues/futures.ts @@ -80,13 +80,7 @@ export class FuturesVenue implements Venue { } async readOpenOrders(user: Address): Promise { - const orderIds = (await this.chain.publicClient.readContract({ - address: this.config.futures.address, - abi: HashPowerFuturesAbi, - functionName: "getUserOrders", - args: [user], - })) as readonly Hex[]; - + const orderIds = await this.readActiveOrderIds(user); if (orderIds.length === 0) return []; const orders = await this.chain.publicClient.multicall({ @@ -160,12 +154,7 @@ export class FuturesVenue implements Venue { ): Promise { let targetIds = ids; if (targetIds === undefined) { - targetIds = (await this.chain.publicClient.readContract({ - address: this.config.futures.address, - abi: HashPowerFuturesAbi, - functionName: "getUserOrders", - args: [user], - })) as readonly Hex[]; + targetIds = await this.readActiveOrderIds(user); } if (targetIds.length === 0) { return { skipped: "notLiquidatable" }; @@ -245,6 +234,36 @@ export class FuturesVenue implements Venue { : { feeEarned: result.feeEarned, positionsClosed: Number(contractsClosed) }; } + /** + * Active-window resting order ids: `getExpirationDates()` then + * `getUserOrdersAtExpiration` per delivery (no cross-expiry on-chain getter). + */ + private async readActiveOrderIds(user: Address): Promise { + const expirationAts = (await this.chain.publicClient.readContract({ + address: this.config.futures.address, + abi: HashPowerFuturesAbi, + functionName: "getExpirationDates", + })) as readonly bigint[]; + + if (expirationAts.length === 0) return []; + + const perExpiry = await this.chain.publicClient.multicall({ + contracts: expirationAts.map((expirationAt) => ({ + address: this.config.futures.address, + abi: HashPowerFuturesAbi, + functionName: "getUserOrdersAtExpiration" as const, + args: [user, expirationAt] as const, + })), + allowFailure: false, + }); + + const orderIds: Hex[] = []; + for (const ids of perExpiry as readonly (readonly Hex[])[]) { + for (const id of ids) orderIds.push(id); + } + return orderIds; + } + private async getMMParams(): Promise { if (this.mmParams !== undefined) return this.mmParams; this.mmParams = await readMMParams(this.chain, this.config); diff --git a/keeper/src/venues/types.ts b/keeper/src/venues/types.ts index 03c3b19..2bd1d8a 100644 --- a/keeper/src/venues/types.ts +++ b/keeper/src/venues/types.ts @@ -79,7 +79,8 @@ export interface Venue { /** * Calls `liquidateOrders(user, ids[])` on the venue. Keeper-chosen ids; * on-chain stop-on-failure keeps prior cancels and stops when healthy. - * When `ids` is omitted the venue reads `getUserOrders` first. + * When `ids` is omitted the venue discovers resting ids first (perps: + * `getUserOrders`; futures: `getExpirationDates` + per-expiry order ids). */ liquidateOrders(user: Address, ids?: readonly Hex[]): Promise; diff --git a/keeper/tests/integration/helpers.ts b/keeper/tests/integration/helpers.ts index 37be777..859ed91 100644 --- a/keeper/tests/integration/helpers.ts +++ b/keeper/tests/integration/helpers.ts @@ -139,12 +139,28 @@ export async function readFuturesOrderIds( stack: DeployedStack, user: Address, ): Promise { - return (await stack.publicClient.readContract({ + const expirationAts = (await stack.publicClient.readContract({ address: stack.addresses.futures, abi: stack.abis.futures, - functionName: "getUserOrders", - args: [user], - })) as readonly Hex[]; + functionName: "getExpirationDates", + })) as readonly bigint[]; + if (expirationAts.length === 0) return []; + + const perExpiry = await stack.publicClient.multicall({ + contracts: expirationAts.map((expirationAt) => ({ + address: stack.addresses.futures, + abi: stack.abis.futures, + functionName: "getUserOrdersAtExpiration" as const, + args: [user, expirationAt] as const, + })), + allowFailure: false, + }); + + const orderIds: Hex[] = []; + for (const ids of perExpiry as readonly (readonly Hex[])[]) { + for (const id of ids) orderIds.push(id); + } + return orderIds; } /** Resolves to true once `user` is flat on perps. */ diff --git a/keeper/tests/predict/coordinator.test.ts b/keeper/tests/predict/coordinator.test.ts index 45211b6..d0c29cb 100644 --- a/keeper/tests/predict/coordinator.test.ts +++ b/keeper/tests/predict/coordinator.test.ts @@ -134,8 +134,12 @@ function buildHarness({ }; case "getOrderAggregate": return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; + case "getOrderAggregateAtExpiration": + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; case "getActiveExpirationDates": return []; + case "getExpirationDates": + return []; case "computePortfolioIM": return balance / 2n; case "computePortfolioMM": { diff --git a/keeper/tests/predict/coordinatorAlerts.test.ts b/keeper/tests/predict/coordinatorAlerts.test.ts index 91ec8cc..7dd7bd2 100644 --- a/keeper/tests/predict/coordinatorAlerts.test.ts +++ b/keeper/tests/predict/coordinatorAlerts.test.ts @@ -106,8 +106,12 @@ function buildHarness({ balance, perpEntry }: { balance: bigint; perpEntry: bigi }; case "getOrderAggregate": return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; + case "getOrderAggregateAtExpiration": + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; case "getActiveExpirationDates": return []; + case "getExpirationDates": + return []; case "computePortfolioIM": return imAtPriceTokens(currentPrice); case "computePortfolioMM": diff --git a/keeper/tests/predict/snapshot.test.ts b/keeper/tests/predict/snapshot.test.ts index 0d892c2..4aa760d 100644 --- a/keeper/tests/predict/snapshot.test.ts +++ b/keeper/tests/predict/snapshot.test.ts @@ -30,6 +30,8 @@ function makeConfig(): Config { function makeChain(scripted: { activeExpirationAts?: readonly bigint[]; + /** Tradable window for futures order aggregates; defaults to activeExpirationAts. */ + tradableExpirationAts?: readonly bigint[]; futuresPositions?: Record; /** Keyed by expiry; absent means the expiry has not settled. */ settlementPrices?: Record; @@ -91,8 +93,8 @@ function makeChain(scripted: { }; } case "getOrderAggregate": { - const orders = - (c.address === PERPS ? scripted.perpOrders : scripted.futuresOrders) ?? NO_ORDERS; + // Perps-only cross-user aggregate. + const orders = scripted.perpOrders ?? NO_ORDERS; return { buyQty: 0n, sellQty: 0n, @@ -100,8 +102,34 @@ function makeChain(scripted: { sellValue: orders.sellValue, }; } + case "getOrderAggregateAtExpiration": { + const orders = scripted.futuresOrders ?? NO_ORDERS; + const dates = + scripted.tradableExpirationAts ?? scripted.activeExpirationAts ?? []; + // Put the full venue totals on the first expiry so a single-window + // sum matches the scripted RestingOrders values. + const expirationAt = c.args?.[1] as bigint; + const isFirst = dates.length === 0 || expirationAt === dates[0]; + return { + buyQty: 0n, + sellQty: 0n, + buyValue: isFirst ? orders.buyValue : 0n, + sellValue: isFirst ? orders.sellValue : 0n, + }; + } case "getActiveExpirationDates": return scripted.activeExpirationAts ?? []; + case "getExpirationDates": { + if (scripted.tradableExpirationAts !== undefined) { + return scripted.tradableExpirationAts; + } + if (scripted.activeExpirationAts !== undefined) { + return scripted.activeExpirationAts; + } + // Flat accounts still need one window slot when futures order + // totals are scripted without explicit expiries. + return scripted.futuresOrders === undefined ? [] : [0n]; + } case "imSpotShock": return scripted.imShock ?? 10n ** 17n; case "mmSpotShock": diff --git a/keeper/tests/venues/futures.test.ts b/keeper/tests/venues/futures.test.ts index f2e86c7..749ad8d 100644 --- a/keeper/tests/venues/futures.test.ts +++ b/keeper/tests/venues/futures.test.ts @@ -47,12 +47,21 @@ const silentLogger = { const DELIVERY_AT = 1_756_416_000n; -function makeReadHandler(marketPrice: bigint, listResult: readonly unknown[]) { +function makeReadHandler( + marketPrice: bigint, + listResult: readonly unknown[], + opts: { orderIdsByExpiry?: Record } = {}, +) { return (call: ReadCall): unknown => { if (call.functionName === "getMarketPrice") return marketPrice; - if (call.functionName === "getUserOrders" || call.functionName === "getActiveExpirationDates") { + if (call.functionName === "getActiveExpirationDates") { return listResult; } + if (call.functionName === "getExpirationDates") { + return opts.orderIdsByExpiry === undefined + ? [] + : Object.keys(opts.orderIdsByExpiry).map((k) => BigInt(k)); + } throw new Error(`unexpected readContract call: ${call.functionName}`); }; } @@ -66,7 +75,7 @@ describe("futures venue: marketLabel", () => { }); describe("futures venue: readOpenOrders", () => { - it("returns empty when getUserOrders is empty (no extra multicall)", async () => { + it("returns empty when the tradable window has no dates (no multicall)", async () => { let multicallCount = 0; const chain = makeChainStub({ readContract: makeReadHandler(100n, []), @@ -78,7 +87,7 @@ describe("futures venue: readOpenOrders", () => { const venue = new FuturesVenue(chain, makeConfigStub(), silentLogger); const orders = await venue.readOpenOrders(BUYER); assert.equal(orders.length, 0); - assert.equal(multicallCount, 0, "no multicall when no orders"); + assert.equal(multicallCount, 0, "no multicall when no tradable dates"); }); it("hydrates each order's expirationAt as its marketId", async () => { @@ -86,14 +95,27 @@ describe("futures venue: readOpenOrders", () => { "0x000000000000000000000000000000000000000000000000000000000000000a", "0x000000000000000000000000000000000000000000000000000000000000000b", ]; + const expiryB = DELIVERY_AT + 86_400n; + let multicallStep = 0; const chain = makeChainStub({ - readContract: makeReadHandler(100n, orderIds), + readContract: makeReadHandler(100n, [], { + orderIdsByExpiry: { + [DELIVERY_AT.toString()]: [orderIds[0]!], + [expiryB.toString()]: [orderIds[1]!], + }, + }), multicall: (calls) => { + multicallStep++; + if (multicallStep === 1) { + assert.equal(calls.length, 2); + for (const c of calls) assert.equal(c.functionName, "getUserOrdersAtExpiration"); + return [[orderIds[0]!], [orderIds[1]!]]; + } assert.equal(calls.length, 2); for (const c of calls) assert.equal(c.functionName, "getOrder"); return [ { participant: BUYER, expirationAt: DELIVERY_AT, price: 50n, quantity: 1n }, - { participant: BUYER, expirationAt: DELIVERY_AT + 86_400n, price: 60n, quantity: -1n }, + { participant: BUYER, expirationAt: expiryB, price: 60n, quantity: -1n }, ]; }, }); @@ -102,7 +124,7 @@ describe("futures venue: readOpenOrders", () => { assert.equal(orders.length, 2); assert.equal(orders[0]?.id, orderIds[0]); assert.equal(orders[0]?.marketId, expirationAtMarketId(DELIVERY_AT)); - assert.equal(orders[1]?.marketId, expirationAtMarketId(DELIVERY_AT + 86_400n)); + assert.equal(orders[1]?.marketId, expirationAtMarketId(expiryB)); }); }); diff --git a/keeper/tests/venues/reduceToTarget.test.ts b/keeper/tests/venues/reduceToTarget.test.ts index e88e930..ba8d14e 100644 --- a/keeper/tests/venues/reduceToTarget.test.ts +++ b/keeper/tests/venues/reduceToTarget.test.ts @@ -30,9 +30,9 @@ const EMPTY_RISK_VIEW = { } as const; /** - * The bulk read `readAccountSnapshot` issues, in order: balance, the perp position, - * then each venue's `getRiskView` / `getOrderAggregate` pair, then the active futures - * expiries. Only the expiry list varies between these cases. + * The bulk read `readAccountSnapshot` issues, in order: balance, perp position, + * perps risk/aggregate, futures risk, active position expiries, tradable window. + * Only the expiry lists vary between these cases. */ function snapshotMulticall(balance: bigint, expiries: readonly bigint[]) { return [ @@ -41,7 +41,7 @@ function snapshotMulticall(balance: bigint, expiries: readonly bigint[]) { EMPTY_RISK_VIEW, [0n, 0n], EMPTY_RISK_VIEW, - [0n, 0n], + expiries, expiries, ]; } @@ -87,13 +87,14 @@ function makeChainStub(opts: { return snapshotMulticall(opts.balance, [EXPIRY]); } if (fns[0] === "getUserPosition") { - // The per-expiry read batches `getUserPosition` and `settlementPrice`; - // an unsettled expiry prices at 0. - return contracts.map((c) => - c.functionName === "settlementPrice" - ? 0n - : { netQuantity: opts.netQuantity, netEntryValue: opts.netEntryValue }, - ); + // Per-expiry batch: positions, settlement prices, order aggregates. + return contracts.map((c) => { + if (c.functionName === "settlementPrice") return 0n; + if (c.functionName === "getOrderAggregateAtExpiration") { + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; + } + return { netQuantity: opts.netQuantity, netEntryValue: opts.netEntryValue }; + }); } throw new Error(`unexpected multicall head: ${fns[0]}`); }, @@ -155,6 +156,9 @@ describe("futures venue: reduceToTarget", () => { if (fns[0] === "getUserPosition") { return contracts.map((c) => { if (c.functionName === "settlementPrice") return 0n; + if (c.functionName === "getOrderAggregateAtExpiration") { + return { buyQty: 0n, sellQty: 0n, buyValue: 0n, sellValue: 0n }; + } const expirationAt = c.args?.[1] as bigint; return { netQuantity: 4n, diff --git a/market-maker/src/abi/HashPowerFutures.ts b/market-maker/src/abi/HashPowerFutures.ts index 88f9399..d2b2255 100644 --- a/market-maker/src/abi/HashPowerFutures.ts +++ b/market-maker/src/abi/HashPowerFutures.ts @@ -1003,44 +1003,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_expirationAt", - "type": "uint256" - } - ], - "name": "getBestAskPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, - { - "inputs": [ - { - "internalType": "uint256", - "name": "_expirationAt", - "type": "uint256" - } - ], - "name": "getBestBidPrice", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "getExpirationDates", @@ -1067,25 +1029,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_participant", - "type": "address" - } - ], - "name": "getNetPositionDelta", - "outputs": [ - { - "internalType": "int256", - "name": "", - "type": "int256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -1127,47 +1070,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_user", - "type": "address" - } - ], - "name": "getOrderAggregate", - "outputs": [ - { - "components": [ - { - "internalType": "uint256", - "name": "buyQty", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "sellQty", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "buyValue", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "sellValue", - "type": "uint256" - } - ], - "internalType": "struct HashPowerFuturesBase.OrderAggregate", - "name": "aggregate_", - "type": "tuple" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { @@ -1347,25 +1249,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "view", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_user", - "type": "address" - } - ], - "name": "getUserOrders", - "outputs": [ - { - "internalType": "bytes32[]", - "name": "orderIds", - "type": "bytes32[]" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { From b4e8d2973d607da7d0a26309c3e79a91984f602a Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 14:23:56 +0200 Subject: [PATCH 17/26] =?UTF-8?q?feat(pme):=20add=20isLiquidatable=20?= =?UTF-8?q?=E2=80=94=20the=20canonical=20cross-venue=20health=20predicate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Liquidatability is a property of the portfolio (balance < portfolio MM), not of any single venue, so the predicate moves here. The venues' own isLiquidatable views answered it inconsistently (futures gated on venue-local state, perps on portfolio-wide order delta) and are being removed; whether a venue holds anything actionable stays answerable via its hasRestingOrderDelta and position views. Co-authored-by: Cursor --- contracts/contracts/PortfolioMarginEngine.sol | 11 ++++++++++- .../contracts/interfaces/IPortfolioMarginEngine.sol | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/contracts/contracts/PortfolioMarginEngine.sol b/contracts/contracts/PortfolioMarginEngine.sol index 8776533..3bfec98 100644 --- a/contracts/contracts/PortfolioMarginEngine.sol +++ b/contracts/contracts/PortfolioMarginEngine.sol @@ -68,7 +68,7 @@ contract PortfolioMarginEngine is using EnumerableSet for EnumerableSet.AddressSet; uint256 private constant MAX_ORACLE_STALENESS = 1 hours; - string public constant VERSION = "2.0.0"; + string public constant VERSION = "2.1.0"; // ── Storage ───────────────────────────────────────────────────────────── @@ -281,6 +281,15 @@ contract PortfolioMarginEngine is return vault.balanceOf(user) >= _computeMargin(user, false); } + /// @notice Whether the account is liquidatable: vault balance below portfolio MM. + /// The exact predicate the venues' liquidation entry points enforce. + /// @dev See {IPortfolioMarginEngine-isLiquidatable}. Deliberately the strict inverse + /// of {isHealthy}; kept as its own entry point because it is the question keepers + /// and venue UIs ask, and `isHealthy` is not part of the venue-facing interface. + function isLiquidatable(address user) external view returns (bool) { + return vault.balanceOf(user) < _computeMargin(user, false); + } + /// @notice Check if user can place an order requiring additionalIM (in token decimals). function canPlaceOrder(address user, uint256 additionalIM) external view returns (bool) { return vault.balanceOf(user) >= _computeMargin(user, true) + additionalIM; diff --git a/contracts/contracts/interfaces/IPortfolioMarginEngine.sol b/contracts/contracts/interfaces/IPortfolioMarginEngine.sol index 55a822f..f59b6d5 100644 --- a/contracts/contracts/interfaces/IPortfolioMarginEngine.sol +++ b/contracts/contracts/interfaces/IPortfolioMarginEngine.sol @@ -22,6 +22,15 @@ interface IPortfolioMarginEngine { /// @notice Portfolio Initial and Maintenance Margin from one shared market snapshot. function computePortfolioMargins(address user) external view returns (uint256 im, uint256 mm); + /// @notice Whether the account is liquidatable: vault balance below portfolio MM. + /// @dev The canonical cross-venue health predicate — liquidatability is a property of + /// the portfolio, not of any single venue, so it lives here. No venue-local state + /// check is needed: an account with no state anywhere has MM = 0, and a balance + /// below zero is impossible. Whether a specific venue holds anything actionable + /// is a separate question, answered by that venue's `hasRestingOrderDelta` and + /// position views. + function isLiquidatable(address user) external view returns (bool); + /// @notice Margin charged against a delta-one resting order's notional (both token /// decimals). /// @dev Lets a market size order margin from the engine's risk knob without importing From d6eea43b8d477d7ebf4e7bde6fba1c0f4047ca9a Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 10 Aug 2026 14:41:23 +0200 Subject: [PATCH 18/26] refactor: drop venue isLiquidatable from mocks, ABIs and consumers The venues removed their isLiquidatable views; the PME predicate added in the previous commit is the single source. PerpsDEXMock loses the dead balance/maintenance-margin model, MarginEngineMock answers the new interface member, the keeper/market-maker HashPowerFutures ABI copies are synced from the regenerated futures ABI, and the pending MM process test reads the predicate from the PME. Co-authored-by: Cursor --- contracts/abi/ContractErrors.json | 5 ++++ contracts/abi/ContractErrors.ts | 5 ++++ contracts/abi/IPortfolioMarginEngine.json | 19 +++++++++++++++ contracts/abi/IPortfolioMarginEngine.ts | 19 +++++++++++++++ contracts/abi/PortfolioMarginEngine.json | 24 +++++++++++++++++++ contracts/abi/PortfolioMarginEngine.ts | 24 +++++++++++++++++++ .../contracts/mocks/MarginEngineMock.sol | 5 ++++ contracts/contracts/mocks/PerpsDEXMock.sol | 16 ------------- keeper/src/abi/HashPowerFutures.ts | 19 --------------- market-maker/src/abi/HashPowerFutures.ts | 19 --------------- .../market-maker.process.test.ts | 4 ++-- 11 files changed, 103 insertions(+), 56 deletions(-) diff --git a/contracts/abi/ContractErrors.json b/contracts/abi/ContractErrors.json index 325dbdd..d140f22 100644 --- a/contracts/abi/ContractErrors.json +++ b/contracts/abi/ContractErrors.json @@ -4,6 +4,11 @@ "name": "NoPoints", "type": "error" }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, { "inputs": [ { diff --git a/contracts/abi/ContractErrors.ts b/contracts/abi/ContractErrors.ts index b0b170f..91a5946 100644 --- a/contracts/abi/ContractErrors.ts +++ b/contracts/abi/ContractErrors.ts @@ -4,6 +4,11 @@ export const contractErrors = [ "name": "NoPoints", "type": "error" }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, { "inputs": [ { diff --git a/contracts/abi/IPortfolioMarginEngine.json b/contracts/abi/IPortfolioMarginEngine.json index 4be2dfc..ecb685b 100644 --- a/contracts/abi/IPortfolioMarginEngine.json +++ b/contracts/abi/IPortfolioMarginEngine.json @@ -93,6 +93,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/IPortfolioMarginEngine.ts b/contracts/abi/IPortfolioMarginEngine.ts index a9a1c57..138323c 100644 --- a/contracts/abi/IPortfolioMarginEngine.ts +++ b/contracts/abi/IPortfolioMarginEngine.ts @@ -93,6 +93,25 @@ export const IPortfolioMarginEngineAbi = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/PortfolioMarginEngine.json b/contracts/abi/PortfolioMarginEngine.json index a5dc6b5..fc0df67 100644 --- a/contracts/abi/PortfolioMarginEngine.json +++ b/contracts/abi/PortfolioMarginEngine.json @@ -71,6 +71,11 @@ "name": "OracleNotSet", "type": "error" }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, { "inputs": [ { @@ -476,6 +481,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/abi/PortfolioMarginEngine.ts b/contracts/abi/PortfolioMarginEngine.ts index 1461c08..baaa180 100644 --- a/contracts/abi/PortfolioMarginEngine.ts +++ b/contracts/abi/PortfolioMarginEngine.ts @@ -71,6 +71,11 @@ export const PortfolioMarginEngineAbi = [ "name": "OracleNotSet", "type": "error" }, + { + "inputs": [], + "name": "OracleStale", + "type": "error" + }, { "inputs": [ { @@ -476,6 +481,25 @@ export const PortfolioMarginEngineAbi = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "user", + "type": "address" + } + ], + "name": "isLiquidatable", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { diff --git a/contracts/contracts/mocks/MarginEngineMock.sol b/contracts/contracts/mocks/MarginEngineMock.sol index ef888d2..57d03ea 100644 --- a/contracts/contracts/mocks/MarginEngineMock.sol +++ b/contracts/contracts/mocks/MarginEngineMock.sol @@ -49,6 +49,11 @@ contract MarginEngineMock is IPortfolioMarginEngine { return false; } + /// @dev MM is always zero here, and a balance below zero is impossible. + function isLiquidatable(address) external pure returns (bool) { + return false; + } + function imSpotShock() external pure returns (uint256) { return 0; } diff --git a/contracts/contracts/mocks/PerpsDEXMock.sol b/contracts/contracts/mocks/PerpsDEXMock.sol index f8fdb0b..c446c32 100644 --- a/contracts/contracts/mocks/PerpsDEXMock.sol +++ b/contracts/contracts/mocks/PerpsDEXMock.sol @@ -23,9 +23,7 @@ contract PerpsDEXMock is ILinearMarket { } mapping(address => Position) private _positions; - mapping(address => uint256) private _balances; mapping(address => int256) private _unrealizedPnl; - mapping(address => uint256) private _maintenanceMargin; mapping(address => int256) private _pendingFunding; mapping(address => uint256) private _buyOrderDelta; mapping(address => uint256) private _sellOrderDelta; @@ -38,19 +36,10 @@ contract PerpsDEXMock is ILinearMarket { _positions[user] = Position(qty, netEntryValue); } - function setBalance(address user, uint256 bal) external { - _balances[user] = bal; - } - function setUnrealizedPnl(address user, int256 pnl) external { _unrealizedPnl[user] = pnl; } - /// @dev Only MM is modelled: it is the threshold `isLiquidatable` compares balance against. - function setMaintenanceMargin(address user, uint256 mm) external { - _maintenanceMargin[user] = mm; - } - function getUserPosition(address user) external view returns (Position memory) { return _positions[user]; } @@ -105,9 +94,4 @@ contract PerpsDEXMock is ILinearMarket { function hasRestingOrderDelta(address user) external view returns (bool) { return _buyOrderDelta[user] != 0 || _sellOrderDelta[user] != 0; } - - function isLiquidatable(address user) external view returns (bool) { - if (_positions[user].netQuantity == 0) return false; - return _balances[user] < _maintenanceMargin[user]; - } } diff --git a/keeper/src/abi/HashPowerFutures.ts b/keeper/src/abi/HashPowerFutures.ts index d2b2255..f4d4ae3 100644 --- a/keeper/src/abi/HashPowerFutures.ts +++ b/keeper/src/abi/HashPowerFutures.ts @@ -1369,25 +1369,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_participant", - "type": "address" - } - ], - "name": "isLiquidatable", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/market-maker/src/abi/HashPowerFutures.ts b/market-maker/src/abi/HashPowerFutures.ts index d2b2255..f4d4ae3 100644 --- a/market-maker/src/abi/HashPowerFutures.ts +++ b/market-maker/src/abi/HashPowerFutures.ts @@ -1369,25 +1369,6 @@ export const HashPowerFuturesAbi = [ "stateMutability": "nonpayable", "type": "function" }, - { - "inputs": [ - { - "internalType": "address", - "name": "_participant", - "type": "address" - } - ], - "name": "isLiquidatable", - "outputs": [ - { - "internalType": "bool", - "name": "", - "type": "bool" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [ { diff --git a/market-maker/tests-pending/market-maker.process.test.ts b/market-maker/tests-pending/market-maker.process.test.ts index 7e3528a..0976b77 100644 --- a/market-maker/tests-pending/market-maker.process.test.ts +++ b/market-maker/tests-pending/market-maker.process.test.ts @@ -615,8 +615,8 @@ describe("MM process — post-fill on-chain state", () => { it("should not be liquidatable with sufficient collateral", async () => { const isLiquidatable = (await publicClient.readContract({ - address: deployment.contracts.perpsAddress, - abi: hashPowerPerpsDexAbi, + address: deployment.contracts.pmeAddress, + abi: PortfolioMarginEngineAbi, functionName: "isLiquidatable", args: [MM_ACCOUNT.address], })) as boolean; From b8a7647ed8f8061aa9fcd6305d26512935cad88e Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 12:22:17 +0200 Subject: [PATCH 19/26] fix(keeper): pin pnpm 10 to keep git subpath deps pnpm 11.1.1 records git subpath dependencies as integrity-only, dropping the `path:` field. A cold store then unpacks the whole repository into node_modules, so collateral-margin-abi/CollateralVault.ts and @hashpower/portfolio-margin do not resolve and TypeCheck fails with TS2307 on every CI run. --- keeper/package.json | 2 +- keeper/pnpm-lock.yaml | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/keeper/package.json b/keeper/package.json index bb7d632..4e8286f 100644 --- a/keeper/package.json +++ b/keeper/package.json @@ -34,5 +34,5 @@ "@typescript/native-preview": "7.0.0-dev.20260511.1", "pino-pretty": "^13.1.3" }, - "packageManager": "pnpm@11.1.1" + "packageManager": "pnpm@10.28.1" } diff --git a/keeper/pnpm-lock.yaml b/keeper/pnpm-lock.yaml index fc7b5d8..3aaef0c 100644 --- a/keeper/pnpm-lock.yaml +++ b/keeper/pnpm-lock.yaml @@ -67,28 +67,24 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [musl] '@biomejs/cli-linux-arm64@2.4.13': resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.13': resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [musl] '@biomejs/cli-linux-x64@2.4.13': resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - libc: [glibc] '@biomejs/cli-win32-arm64@2.4.13': resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} @@ -103,7 +99,7 @@ packages: os: [win32] '@hashpower/portfolio-margin@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/portfolio-margin': - resolution: {gitHosted: true, integrity: sha512-mxgdMq81THnD1ytwHTwSxHhAVsYdGw6YjcwHAxzUQG0mMrfjdeSx/Yipcg117u3QUSSnU9L8YJ4J09xmujZOOQ==, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} + resolution: {path: /portfolio-margin, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} version: 0.1.0 engines: {node: '>=22'} @@ -201,7 +197,7 @@ packages: engines: {node: '>=8.0.0'} collateral-margin-abi@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/contracts/abi: - resolution: {gitHosted: true, integrity: sha512-mxgdMq81THnD1ytwHTwSxHhAVsYdGw6YjcwHAxzUQG0mMrfjdeSx/Yipcg117u3QUSSnU9L8YJ4J09xmujZOOQ==, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} + resolution: {path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} version: 0.0.0 colorette@2.0.20: @@ -211,7 +207,7 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi: - resolution: {gitHosted: true, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535} + resolution: {path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535} version: 0.0.0 end-of-stream@1.4.5: From 5f74df86318bd67682b7e79a5142103a517f9023 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 12:22:26 +0200 Subject: [PATCH 20/26] fix(ci): run market-maker tests on node 24 Node 22 refuses to strip types from .ts files under node_modules, so any test reaching perps-contracts/abi/HashPowerPerpsDEX.ts crashes on import. Match the keeper workflow, which already runs node 24. --- .github/workflows/market-maker-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/market-maker-tests.yml b/.github/workflows/market-maker-tests.yml index 0ac2149..5e99f00 100644 --- a/.github/workflows/market-maker-tests.yml +++ b/.github/workflows/market-maker-tests.yml @@ -50,7 +50,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "22" + node-version: "24" cache: "pnpm" cache-dependency-path: market-maker/pnpm-lock.yaml From d4e7659c9ae4cac04e703ca7b786c1837ec87d50 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 12:41:49 +0200 Subject: [PATCH 21/26] fix(ci): pin siblings to the converged contract branches The keeper integration harness deploys HashPowerFutures and reads a signed netEntryValue, neither of which exists at the previously pinned commits, so every fixture failed on a missing Hardhat artifact. Track futures #258 and perps #97 until both land on dev. --- .github/workflows/keeper-test.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/keeper-test.yml b/.github/workflows/keeper-test.yml index 54cc92e..5697804 100644 --- a/.github/workflows/keeper-test.yml +++ b/.github/workflows/keeper-test.yml @@ -88,18 +88,18 @@ jobs: uses: actions/checkout@v5 with: repository: Lumerin-protocol/derivatives-marketplace - # createOrder with explicit TIF + getRiskView; implements this branch's - # engine interface (PR #90). - ref: 8df78601bbbad049cee962509f9c9e8b9d9b2503 + # Signed netEntryValue on the position tuple; implements this branch's + # engine interface (PR #97). + ref: 316be14c529071df4d2ff71321ab4a6fbedcfaaf path: perps - name: Checkout futures-marketplace uses: actions/checkout@v5 with: repository: Lumerin-protocol/futures-marketplace - # createOrder with explicit TIF + getRiskView; implements this branch's - # engine interface (PR #247). - ref: 16d026960fa15d0fb73822e6750ae8e3f14a4c6d + # HashPowerFutures rename and per-delivery order reads; implements this + # branch's engine interface (PR #258). + ref: 5a543ae0c59c651790bc99a8550da9a2ffd30c2b path: futures-marketplace - name: Install perps contracts dependencies From a8ac309ea8f7114844d2175f970dcbbd00d11368 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 12:41:49 +0200 Subject: [PATCH 22/26] fix(keeper): read futures order ids without multicall3 The test public client has no multicall3 configured, matching every other reader in this file, so readFuturesOrderIds must fan out readContract calls instead of batching them. --- keeper/tests/integration/helpers.ts | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/keeper/tests/integration/helpers.ts b/keeper/tests/integration/helpers.ts index 859ed91..20c1563 100644 --- a/keeper/tests/integration/helpers.ts +++ b/keeper/tests/integration/helpers.ts @@ -146,18 +146,20 @@ export async function readFuturesOrderIds( })) as readonly bigint[]; if (expirationAts.length === 0) return []; - const perExpiry = await stack.publicClient.multicall({ - contracts: expirationAts.map((expirationAt) => ({ - address: stack.addresses.futures, - abi: stack.abis.futures, - functionName: "getUserOrdersAtExpiration" as const, - args: [user, expirationAt] as const, - })), - allowFailure: false, - }); + const perExpiry = await Promise.all( + expirationAts.map( + (expirationAt) => + stack.publicClient.readContract({ + address: stack.addresses.futures, + abi: stack.abis.futures, + functionName: "getUserOrdersAtExpiration", + args: [user, expirationAt], + }) as Promise, + ), + ); const orderIds: Hex[] = []; - for (const ids of perExpiry as readonly (readonly Hex[])[]) { + for (const ids of perExpiry) { for (const id of ids) orderIds.push(id); } return orderIds; From a1bf400e3dfeac83f793072d06a4bfd072d354a7 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 13:36:57 +0200 Subject: [PATCH 23/26] fix(ci): run market-maker tests through the package script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Invoking node directly skipped `--import=amaro/strip`, so the first test to load a contract ABI shipped as .ts inside node_modules crashed with ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING. Match the keeper workflow's action versions too, and drop the pnpm cache from both so CI always installs against a cold store — a warm store is what hid the git subpath breakage. --- .github/workflows/keeper-test.yml | 2 -- .github/workflows/market-maker-tests.yml | 16 +++++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/keeper-test.yml b/.github/workflows/keeper-test.yml index 5697804..f4fb2e5 100644 --- a/.github/workflows/keeper-test.yml +++ b/.github/workflows/keeper-test.yml @@ -44,8 +44,6 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" - cache: "pnpm" - cache-dependency-path: keeper/pnpm-lock.yaml - name: Install keeper dependencies working-directory: ./keeper diff --git a/.github/workflows/market-maker-tests.yml b/.github/workflows/market-maker-tests.yml index 5e99f00..6d8e3c6 100644 --- a/.github/workflows/market-maker-tests.yml +++ b/.github/workflows/market-maker-tests.yml @@ -40,19 +40,17 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 with: package_json_file: market-maker/package.json - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: "24" - cache: "pnpm" - cache-dependency-path: market-maker/pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile @@ -63,8 +61,8 @@ jobs: - name: Typecheck run: pnpm typecheck - # Skip the pretest (which calls sync-abi.sh, requires the perps and - # futures-marketplace contract repos to be checked out side-by-side). - # Tests cover core/ and apps/ logic; ABI shape is validated at runtime. + # Must go through the package script: it supplies `--import=amaro/strip`, + # without which node refuses to strip types from the contract ABIs that + # ship as .ts inside node_modules. - name: Run tests - run: node --test --test-force-exit --test-concurrency=1 'tests/**/*.test.ts' + run: pnpm test From a06131f4b7a00d90c0cfc728e4b9ee4629e895d8 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 14:13:42 +0200 Subject: [PATCH 24/26] chore: adopt pnpm 11.22 across every package pnpm 11.1.1 dropped the `path:` field from git subpath resolutions, which made installs depend on store state (pnpm/pnpm#12304, fixed in 11.7.0). Move every package to a release carrying that fix instead of holding keeper back on pnpm 10, and restore the CI store cache now that a warm store can no longer mask a lossy lockfile. pnpm 11 migrates the contracts build allowance out of package.json into pnpm-workspace.yaml. --- .github/workflows/keeper-test.yml | 2 ++ .github/workflows/market-maker-tests.yml | 2 ++ contracts/package.json | 7 +------ contracts/pnpm-lock.yaml | 2 +- contracts/pnpm-workspace.yaml | 3 +++ indexer/package.json | 2 +- keeper/package.json | 2 +- keeper/pnpm-lock.yaml | 10 +++++++--- market-maker/package.json | 2 +- market-maker/pnpm-lock.yaml | 12 ++++++------ points-indexer/package.json | 2 +- portfolio-margin/package.json | 2 +- 12 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 contracts/pnpm-workspace.yaml diff --git a/.github/workflows/keeper-test.yml b/.github/workflows/keeper-test.yml index f4fb2e5..5697804 100644 --- a/.github/workflows/keeper-test.yml +++ b/.github/workflows/keeper-test.yml @@ -44,6 +44,8 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" + cache: "pnpm" + cache-dependency-path: keeper/pnpm-lock.yaml - name: Install keeper dependencies working-directory: ./keeper diff --git a/.github/workflows/market-maker-tests.yml b/.github/workflows/market-maker-tests.yml index 6d8e3c6..d9d1611 100644 --- a/.github/workflows/market-maker-tests.yml +++ b/.github/workflows/market-maker-tests.yml @@ -51,6 +51,8 @@ jobs: uses: actions/setup-node@v6 with: node-version: "24" + cache: "pnpm" + cache-dependency-path: market-maker/pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/contracts/package.json b/contracts/package.json index e084762..4c1b8f2 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -36,10 +36,5 @@ "dotenv": "^16.4.1", "viem": "^2.52.2" }, - "packageManager": "pnpm@10.28.1", - "pnpm": { - "onlyBuiltDependencies": [ - "hardhat-viem-abi" - ] - } + "packageManager": "pnpm@11.22.0" } diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index b3a8f2a..2b9c202 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -755,7 +755,7 @@ packages: resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} hardhat-viem-abi@https://codeload.github.com/lsheva/hardhat-viem-abi/tar.gz/54198ea8c9ad9b05c0c23a057ae3b1c7ab82a93a#path:packages/hardhat-viem-abi: - resolution: {path: packages/hardhat-viem-abi, tarball: https://codeload.github.com/lsheva/hardhat-viem-abi/tar.gz/54198ea8c9ad9b05c0c23a057ae3b1c7ab82a93a} + resolution: {gitHosted: true, path: packages/hardhat-viem-abi, tarball: https://codeload.github.com/lsheva/hardhat-viem-abi/tar.gz/54198ea8c9ad9b05c0c23a057ae3b1c7ab82a93a} version: 1.0.0-alpha.2 engines: {node: '>=22'} peerDependencies: diff --git a/contracts/pnpm-workspace.yaml b/contracts/pnpm-workspace.yaml new file mode 100644 index 0000000..17496f7 --- /dev/null +++ b/contracts/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + esbuild: false + hardhat-viem-abi@https://codeload.github.com/lsheva/hardhat-viem-abi/tar.gz/54198ea8c9ad9b05c0c23a057ae3b1c7ab82a93a#path:packages/hardhat-viem-abi: true diff --git a/indexer/package.json b/indexer/package.json index 3d9f116..0477030 100644 --- a/indexer/package.json +++ b/indexer/package.json @@ -34,5 +34,5 @@ "matchstick-as": "0.6.0", "typescript": "^5.9.3" }, - "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316" + "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621" } diff --git a/keeper/package.json b/keeper/package.json index 4e8286f..16d8813 100644 --- a/keeper/package.json +++ b/keeper/package.json @@ -34,5 +34,5 @@ "@typescript/native-preview": "7.0.0-dev.20260511.1", "pino-pretty": "^13.1.3" }, - "packageManager": "pnpm@10.28.1" + "packageManager": "pnpm@11.22.0" } diff --git a/keeper/pnpm-lock.yaml b/keeper/pnpm-lock.yaml index 3aaef0c..7a38658 100644 --- a/keeper/pnpm-lock.yaml +++ b/keeper/pnpm-lock.yaml @@ -67,24 +67,28 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@2.4.13': resolution: {integrity: sha512-NzkUDSqfvMBrPplKgVr3aXLHZ2NEELvvF4vZxXulEylKWIGqlvNEcwUcj9OLrn75TD3lJ/GIqCVlBwd1MZCuYQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@2.4.13': resolution: {integrity: sha512-Z601MienRgTBDza/+u2CH3RSrWoXo9rtr8NK6A4KJzqGgfxx+H3VlyLgTJ4sRo40T3pIsqpTmiOQEvYzQvBRvQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@2.4.13': resolution: {integrity: sha512-Az3ZZedYRBo9EQzNnD9SxFcR1G5QsGo6VEc2hIyVPZ1rdKwee/7E9oeBBZFpE8Z44ekxsDQBqbiWGW5ShOhUSQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@2.4.13': resolution: {integrity: sha512-Px9PS2B5/Q183bUwy/5VHqp3J2lzdOCeVGzMpphYfl8oSa7VDCqenBdqWpy6DCy/en4Rbf/Y1RieZF6dJPcc9A==} @@ -99,7 +103,7 @@ packages: os: [win32] '@hashpower/portfolio-margin@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/portfolio-margin': - resolution: {path: /portfolio-margin, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} + resolution: {gitHosted: true, integrity: sha512-mxgdMq81THnD1ytwHTwSxHhAVsYdGw6YjcwHAxzUQG0mMrfjdeSx/Yipcg117u3QUSSnU9L8YJ4J09xmujZOOQ==, path: /portfolio-margin, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} version: 0.1.0 engines: {node: '>=22'} @@ -197,7 +201,7 @@ packages: engines: {node: '>=8.0.0'} collateral-margin-abi@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/contracts/abi: - resolution: {path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} + resolution: {gitHosted: true, integrity: sha512-mxgdMq81THnD1ytwHTwSxHhAVsYdGw6YjcwHAxzUQG0mMrfjdeSx/Yipcg117u3QUSSnU9L8YJ4J09xmujZOOQ==, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} version: 0.0.0 colorette@2.0.20: @@ -207,7 +211,7 @@ packages: resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} derivatives-marketplace-abi@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535#path:/contracts/abi: - resolution: {path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535} + resolution: {gitHosted: true, integrity: sha512-JkJW4F+2DgaUOFLgP0OO9QuvJl4bh4m5gle+CVlKH8o+WBRqY2+c3OoFl7ml9lzdwGGwWjOFdchqY4KP/a6OnQ==, path: /contracts/abi, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/8b7ed0f3572d0ea8039a11757b7c1b963be75535} version: 0.0.0 end-of-stream@1.4.5: diff --git a/market-maker/package.json b/market-maker/package.json index 85c84f8..429e312 100644 --- a/market-maker/package.json +++ b/market-maker/package.json @@ -52,7 +52,7 @@ "pino-pretty": "^13.1.3", "typescript": "^5.8.0" }, - "packageManager": "pnpm@10.28.1", + "packageManager": "pnpm@11.22.0", "pnpm": { "onlyBuiltDependencies": [ "keccak@3.0.4", diff --git a/market-maker/pnpm-lock.yaml b/market-maker/pnpm-lock.yaml index de471e3..127eddb 100644 --- a/market-maker/pnpm-lock.yaml +++ b/market-maker/pnpm-lock.yaml @@ -1212,12 +1212,12 @@ packages: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} collateral-margin-contracts@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c#path:/contracts: - resolution: {path: /contracts, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} + resolution: {gitHosted: true, path: /contracts, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/c34b4a360d6616d017b157a4a9e27e1a8e60079c} version: 1.0.0 engines: {node: 24.x} collateral-margin@https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/9372f537a57682bb126aede41aac51be004828a6: - resolution: {tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/9372f537a57682bb126aede41aac51be004828a6} + resolution: {gitHosted: true, tarball: https://codeload.github.com/Lumerin-protocol/collateral-margin/tar.gz/9372f537a57682bb126aede41aac51be004828a6} version: 1.0.0 color-convert@2.0.1: @@ -1304,7 +1304,7 @@ packages: engines: {node: '>= 0.8'} derivatives-contracts@https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299#path:/contracts: - resolution: {path: /contracts, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299} + resolution: {gitHosted: true, path: /contracts, tarball: https://codeload.github.com/Lumerin-protocol/derivatives-marketplace/tar.gz/f7e219f704646ab654a2a8d0286c0f477148e299} version: 1.0.0 engines: {node: 24.x} @@ -1346,7 +1346,7 @@ packages: engines: {node: '>=6'} era-contracts@https://codeload.github.com/matter-labs/era-contracts/tar.gz/446d391d34bdb48255d5f8fef8a8248925fc98b9: - resolution: {tarball: https://codeload.github.com/matter-labs/era-contracts/tar.gz/446d391d34bdb48255d5f8fef8a8248925fc98b9} + resolution: {gitHosted: true, tarball: https://codeload.github.com/matter-labs/era-contracts/tar.gz/446d391d34bdb48255d5f8fef8a8248925fc98b9} version: 0.1.0 es-define-property@1.0.1: @@ -1595,7 +1595,7 @@ packages: resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} hashprice-oracle@https://codeload.github.com/Lumerin-protocol/hashprice-oracle/tar.gz/b65adbfeb7e6c4417747bfd3d94b6e89e162ed50: - resolution: {tarball: https://codeload.github.com/Lumerin-protocol/hashprice-oracle/tar.gz/b65adbfeb7e6c4417747bfd3d94b6e89e162ed50} + resolution: {gitHosted: true, tarball: https://codeload.github.com/Lumerin-protocol/hashprice-oracle/tar.gz/b65adbfeb7e6c4417747bfd3d94b6e89e162ed50} version: 1.0.0 hasown@2.0.3: @@ -1876,7 +1876,7 @@ packages: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} multicall3@https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012: - resolution: {tarball: https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012} + resolution: {gitHosted: true, tarball: https://codeload.github.com/mds1/multicall3/tar.gz/b667d67ecfa5361a81e8f110234ce242613b0012} version: 0.0.0 nice-try@1.0.5: diff --git a/points-indexer/package.json b/points-indexer/package.json index 34e8f44..c13b147 100644 --- a/points-indexer/package.json +++ b/points-indexer/package.json @@ -51,5 +51,5 @@ "hardhat-matchstick-ts" ] }, - "packageManager": "pnpm@10.28.1+sha512.7d7dbbca9e99447b7c3bf7a73286afaaf6be99251eb9498baefa7d406892f67b879adb3a1d7e687fc4ccc1a388c7175fbaae567a26ab44d1067b54fcb0d6a316" + "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621" } diff --git a/portfolio-margin/package.json b/portfolio-margin/package.json index 1023d45..ceeb2c1 100644 --- a/portfolio-margin/package.json +++ b/portfolio-margin/package.json @@ -31,5 +31,5 @@ "engines": { "node": ">=22" }, - "packageManager": "pnpm@11.1.1" + "packageManager": "pnpm@11.22.0" } From c4c615393fb57e03a1340b4bd74d58e09e781154 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 14:36:58 +0200 Subject: [PATCH 25/26] fix(ci): declare build allowances for pnpm 11 pnpm 11 stopped reading `pnpm.onlyBuiltDependencies` from package.json and turned ignored build scripts into a hard install error, so market-maker and points-indexer both failed to install. Move the allowances to allowBuilds and name the packages that must stay unbuilt. Git dependencies are keyed by exact resolution, so matchstick-ts is listed at both pinned commits. --- market-maker/package.json | 8 +------- market-maker/pnpm-workspace.yaml | 4 ++++ points-indexer/package.json | 6 ------ points-indexer/pnpm-workspace.yaml | 9 +++++++++ 4 files changed, 14 insertions(+), 13 deletions(-) create mode 100644 market-maker/pnpm-workspace.yaml create mode 100644 points-indexer/pnpm-workspace.yaml diff --git a/market-maker/package.json b/market-maker/package.json index 429e312..efd1030 100644 --- a/market-maker/package.json +++ b/market-maker/package.json @@ -52,11 +52,5 @@ "pino-pretty": "^13.1.3", "typescript": "^5.8.0" }, - "packageManager": "pnpm@11.22.0", - "pnpm": { - "onlyBuiltDependencies": [ - "keccak@3.0.4", - "secp256k1@4.0.4" - ] - } + "packageManager": "pnpm@11.22.0" } diff --git a/market-maker/pnpm-workspace.yaml b/market-maker/pnpm-workspace.yaml new file mode 100644 index 0000000..65c6398 --- /dev/null +++ b/market-maker/pnpm-workspace.yaml @@ -0,0 +1,4 @@ +allowBuilds: + "@arbitrum/nitro-contracts": false + keccak: true + secp256k1: true diff --git a/points-indexer/package.json b/points-indexer/package.json index c13b147..b7bb961 100644 --- a/points-indexer/package.json +++ b/points-indexer/package.json @@ -45,11 +45,5 @@ "typescript": "^5.9.3", "viem": "2.52.2" }, - "pnpm": { - "onlyBuiltDependencies": [ - "matchstick-ts", - "hardhat-matchstick-ts" - ] - }, "packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621" } diff --git a/points-indexer/pnpm-workspace.yaml b/points-indexer/pnpm-workspace.yaml new file mode 100644 index 0000000..e39e68a --- /dev/null +++ b/points-indexer/pnpm-workspace.yaml @@ -0,0 +1,9 @@ +# pnpm 11 keys allowBuilds by exact resolution, so a git dependency needs one +# entry per pinned commit. Both the current pin and the v0.4.2 tag are listed so +# bumping the spec does not break installs mid-flight. +allowBuilds: + esbuild: false + hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/hardhat-matchstick-ts: true + matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/matchstick-ts: true + hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/hardhat-matchstick-ts: true + matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/matchstick-ts: true From 8741e7009c3285918de1b47aea4604280c026f10 Mon Sep 17 00:00:00 2001 From: "Oleksandr (Shev) Shevchuk" Date: Mon, 17 Aug 2026 14:50:54 +0200 Subject: [PATCH 26/26] chore(points-indexer): track matchstick-ts by tag Follow the v0.4.2 tag instead of a raw commit and repoint the allowBuilds entries at the resolution it pins. --- points-indexer/package.json | 4 ++-- points-indexer/pnpm-lock.yaml | 24 ++++++++++++------------ points-indexer/pnpm-workspace.yaml | 7 ++----- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/points-indexer/package.json b/points-indexer/package.json index b7bb961..8badcc6 100644 --- a/points-indexer/package.json +++ b/points-indexer/package.json @@ -39,9 +39,9 @@ "assemblyscript": "^0.19.23", "collateral-margin-contracts": "link:../contracts", "hardhat": "^3.9.1", - "hardhat-matchstick-ts": "github:lsheva/matchstick-ts#6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad&path:packages/hardhat-matchstick-ts", + "hardhat-matchstick-ts": "github:lsheva/matchstick-ts#v0.4.2&path:packages/hardhat-matchstick-ts", "matchstick-as": "0.6.0", - "matchstick-ts": "github:lsheva/matchstick-ts#6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad&path:packages/matchstick-ts", + "matchstick-ts": "github:lsheva/matchstick-ts#v0.4.2&path:packages/matchstick-ts", "typescript": "^5.9.3", "viem": "2.52.2" }, diff --git a/points-indexer/pnpm-lock.yaml b/points-indexer/pnpm-lock.yaml index 2029541..9d0dcaa 100644 --- a/points-indexer/pnpm-lock.yaml +++ b/points-indexer/pnpm-lock.yaml @@ -43,14 +43,14 @@ importers: specifier: ^3.9.1 version: 3.9.1 hardhat-matchstick-ts: - specifier: github:lsheva/matchstick-ts#6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad&path:packages/hardhat-matchstick-ts - version: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/hardhat-matchstick-ts(@nomicfoundation/hardhat-network-helpers@3.0.11(hardhat@3.9.1))(@nomicfoundation/hardhat-viem@3.0.9(hardhat@3.9.1)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)))(hardhat@3.9.1) + specifier: github:lsheva/matchstick-ts#v0.4.2&path:packages/hardhat-matchstick-ts + version: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/hardhat-matchstick-ts(@nomicfoundation/hardhat-network-helpers@3.0.11(hardhat@3.9.1))(@nomicfoundation/hardhat-viem@3.0.9(hardhat@3.9.1)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)))(hardhat@3.9.1) matchstick-as: specifier: 0.6.0 version: 0.6.0 matchstick-ts: - specifier: github:lsheva/matchstick-ts#6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad&path:packages/matchstick-ts - version: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/matchstick-ts(@graphprotocol/graph-cli@0.98.1(@types/node@25.9.2)(typescript@5.9.3)(zod@3.25.76))(@graphprotocol/graph-ts@0.38.2)(matchstick-as@0.6.0)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)) + specifier: github:lsheva/matchstick-ts#v0.4.2&path:packages/matchstick-ts + version: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/matchstick-ts(@graphprotocol/graph-cli@0.98.1(@types/node@25.9.2)(typescript@5.9.3)(zod@3.25.76))(@graphprotocol/graph-ts@0.38.2)(matchstick-as@0.6.0)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1367,9 +1367,9 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/hardhat-matchstick-ts: - resolution: {path: packages/hardhat-matchstick-ts, tarball: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad} - version: 0.4.1 + hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/hardhat-matchstick-ts: + resolution: {gitHosted: true, path: packages/hardhat-matchstick-ts, tarball: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9} + version: 0.4.2 engines: {node: '>=22.6'} peerDependencies: '@nomicfoundation/hardhat-network-helpers': ^3 @@ -1721,9 +1721,9 @@ packages: matchstick-as@0.6.0: resolution: {integrity: sha512-E36fWsC1AbCkBFt05VsDDRoFvGSdcZg6oZJrtIe/YDBbuFh8SKbR5FcoqDhNWqSN+F7bN/iS2u8Md0SM+4pUpw==} - matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/matchstick-ts: - resolution: {path: packages/matchstick-ts, tarball: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad} - version: 0.4.1 + matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/matchstick-ts: + resolution: {gitHosted: true, path: packages/matchstick-ts, tarball: https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9} + version: 0.4.2 engines: {node: '>=22.6'} hasBin: true peerDependencies: @@ -3763,7 +3763,7 @@ snapshots: graphql@16.11.0: {} - hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/hardhat-matchstick-ts(@nomicfoundation/hardhat-network-helpers@3.0.11(hardhat@3.9.1))(@nomicfoundation/hardhat-viem@3.0.9(hardhat@3.9.1)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)))(hardhat@3.9.1): + hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/hardhat-matchstick-ts(@nomicfoundation/hardhat-network-helpers@3.0.11(hardhat@3.9.1))(@nomicfoundation/hardhat-viem@3.0.9(hardhat@3.9.1)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)))(hardhat@3.9.1): dependencies: '@nomicfoundation/hardhat-network-helpers': 3.0.11(hardhat@3.9.1) '@nomicfoundation/hardhat-viem': 3.0.9(hardhat@3.9.1)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)) @@ -4135,7 +4135,7 @@ snapshots: dependencies: wabt: 1.0.24 - matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/matchstick-ts(@graphprotocol/graph-cli@0.98.1(@types/node@25.9.2)(typescript@5.9.3)(zod@3.25.76))(@graphprotocol/graph-ts@0.38.2)(matchstick-as@0.6.0)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)): + matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/matchstick-ts(@graphprotocol/graph-cli@0.98.1(@types/node@25.9.2)(typescript@5.9.3)(zod@3.25.76))(@graphprotocol/graph-ts@0.38.2)(matchstick-as@0.6.0)(viem@2.52.2(typescript@5.9.3)(zod@3.25.76)): dependencies: '@graphprotocol/graph-cli': 0.98.1(@types/node@25.9.2)(typescript@5.9.3)(zod@3.25.76) '@graphprotocol/graph-ts': 0.38.2 diff --git a/points-indexer/pnpm-workspace.yaml b/points-indexer/pnpm-workspace.yaml index e39e68a..9c2036b 100644 --- a/points-indexer/pnpm-workspace.yaml +++ b/points-indexer/pnpm-workspace.yaml @@ -1,9 +1,6 @@ -# pnpm 11 keys allowBuilds by exact resolution, so a git dependency needs one -# entry per pinned commit. Both the current pin and the v0.4.2 tag are listed so -# bumping the spec does not break installs mid-flight. +# pnpm 11 keys allowBuilds by exact resolution, so the matchstick entries must be +# updated whenever the git specs move to a different commit. allowBuilds: esbuild: false - hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/hardhat-matchstick-ts: true - matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/6c7e4b5d584a9fc75b66d1f6c03820e20313c9ad#path:packages/matchstick-ts: true hardhat-matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/hardhat-matchstick-ts: true matchstick-ts@https://codeload.github.com/lsheva/matchstick-ts/tar.gz/d613d674898f6d0d01a880dd4e1b514f707b93b9#path:packages/matchstick-ts: true