Margin Rescue built on dreamDEX

How it works

Margin Rescue is a TypeScript client, a Solidity guard and a run ledger built around one question about a dreamDEX cross-margin account: if BTC and ETH move together, what is left of the maintenance buffer, and which concrete reduction brings it back to a target? This page describes each part and the evidence behind it. Everything runs against dreamDEX perps on the Somnia Hideki testnet, chain 50383.

  • built on dreamDEX
  • Somnia Hideki · chain 50383
  • markets SDK 0.32.1 · viem 2.57.7
  • Solidity 0.8.28 · Foundry 1.8.4

The pipeline

Every step hands a pinned, hashed artifact to the next, so a result can always be traced back to the block it was computed from.

Each stage lights in turn as a packet travels the line.
  1. SnapshotReads the whole account and all 21 pools at one block, then rechecks the block hash.snapshot.ts
  2. ReconcileRebuilds health equity and margin requirements from raw state and compares them with the bank's own getter.reconcile.ts
  3. ScenarioFixes absolute BTC and ETH prices; every other held market stays at its reference mark. The object is hashed.scenario.ts
  4. CandidatesPrices 25, 50, 75 and 100% reductions against the live book, then values what remains at the scenario prices.candidates.ts
  5. Confirm and refreshJournals the chosen action before anything is sent and re-reads the account right before sending.action.ts · journal.ts · refresh.ts
  6. GuardExecutes the reduction only if the live position equals the confirmed one, and reverts if the position flips or grows.ReduceOnlyGuard.sol
  7. Bind and recomputeCredits a receipt only if it is the confirmed action, then values the real post-fill account at the original scenario prices.receipt.ts · recompute.ts
  8. Clean upCloses what the run opened through the guard, revokes grants, withdraws and verifies the end state at one block.cleanup.ts · txlog.ts

What comes from dreamDEX

Margin Rescue is built on dreamDEX's perps testnet and uses the venue's own contracts and tools as they are. Nothing of the venue is forked, wrapped or re-hosted; the only new contract is the owner's own guard.

dreamDEX components, and what Margin Rescue does with each
ComponentUsed forReference
Perps testnet on Somnia HidekiThe venue every read and every order goes to. Any other chain is refused.chain 50383 · docs
MarginBankCross-margin USDso collateral and the account's health. Its own getters (getAccountHealth and the requirements) are what the independent rebuild has to match, to the raw unit.0x4787…1394
PerpPoolFactory and the perps poolsAll 21 pools, read at the pinned block: positions, resting orders, books, marks and funding. Every candidate is priced on its pool's book.getPerpPools
placeOrderFor on a pool's order bookThe one call the guard makes: an IOC order for the owner, cancelling the taker on a self-match.selector 0x80054449
OperatorPermissionsRegistryThe owner's per-pool grant that admits the guard, the same grant dreamDEX's “Link a bot” writes; revoked again at cleanup.0x6B2F…8D3a · Link a bot
Perp stop registry and the markets indexerTP/SL stops, listed from the indexer's GraphQL endpoint and then checked on chain at the pinned block.refused if the indexer is behind the block
@somnia-chain/markets-sdkThe venue's TypeScript SDK: the chain definition, the deployment's addresses, and the contract errors and events every receipt is decoded with.0.32.1
USDsoThe collateral token. Balance and allowance are read; only the owner's live run deposits and withdraws.0xed79…4F72

What is Margin Rescue's own: the TypeScript client (snapshot, reconciliation, scenario, candidates, executor, run ledger, this site) and ReduceOnlyGuard.sol, deployed by each owner for their own account.

One snapshot, one block

An analysis starts by sampling one block number and hash. Every read that follows is pinned to that block: the bank's collateral and health getters, every position in every one of the factory's 21 pools (the bank's active list is cross-checked against the full factory list), resting orders, the account mode, wallet balance and allowance, and the BTC and ETH order books. TP/SL stops are listed from the indexer, paged to the end, refused if the indexer has not yet processed the pinned block, and then checked on chain at that block. After the reads, the block hash is checked again to confirm the block is still canonical.

Anything missing or inconsistent becomes a named problem on the snapshot: a stale mark, a pool outside the bank's list, a failed read, an unlisted stop set. Any problem blocks a recommendation.

The accounting

Health is rebuilt from raw integers with the deployment's own rounding. No floating point is used anywhere a decision is made.

Health, per held market, in raw units
E_risk  = unlocked + Σ floor((mark − entry) × size / oneBase)
                   − Σ ceil(size × (cumFunding − entryFundingIndex) / (1e18 × oneBase))
E_total = E_risk + locked
MM      = Σ ceil(floor(|size| × mark / oneBase) × mmBps / 10000)
buffer  = E_risk − MM

E_risk is what the bank compares with its requirements. Collateral locked behind resting orders is not part of it, and cancelling an order only moves collateral from locked to unlocked. TP/SL stops prepay STT, which is not USDso collateral and is never counted.

How the rounding was established (live, read-only)
CheckBlockResult
Every holder of every pool rebuilt with floored PnL33,516,4367,700 / 7,700 exact
The same holders with the SDK's truncation33,516,4362,271 / 7,700 exact
Accounts with 3, 6, 9 and 21 markets (the last with 181 resting orders)33,646,741 – 33,647,348all EXACT

Because every reconciled account matched the bank to the raw unit, the model is treated as deployment-exact and the numerical error bound is zero. Truncation fails for every account with a negative, inexact PnL leg, by at most one raw unit per such leg.

The fixed scenario

A scenario is a canonical JSON object whose id is its SHA-256. It fixes the chain, MarginBank, account, reference block number, hash and time, and, for every held market, the pool, units, reference mark, absolute scenario price, configured risk rates and fees. It also records the funding assumption, the model version and the target buffer.

Only BTC and ETH can be shocked. Every other held market stays at its reference mark and stays in the calculation. A shock of −8% on a reference of 79,976.86 becomes the absolute price 73,578.71, and that price is kept: after an execution the account is valued at 73,578.71 again, not shocked another 8% from wherever the market went.

The shock is instantaneous and static: no price path, no stop or liquidation on the way, no funding accrual during the shock. A later snapshot with changed risk rates or a newly held market cannot be valued under the old scenario.

Candidates

The candidate set is small and explicit: no action; reductions of 25, 50, 75 and 100% of each BTC or ETH position, aligned down to the lot and deduplicated; and, on request, cancelling all ordinary resting orders.

Each reduction is previewed now, against the order book read at the reference block, with a protective IOC limit 50 bps beyond the touch. Realized PnL is floored per fill, fees are charged per fill, and funding owed on the whole position is settled once. The remaining portfolio is then valued at the scenario prices.

A candidate is unavailable if it rounds to zero, falls below the minimum order, would leave a remainder below the minimum, would self-match a resting order of the same account, or finds nothing inside its limit. If doing nothing already meets the target, nothing is recommended. Among candidates that meet it, the smallest current notional reduction is preferred, then the lowest cost. That is a choice among the evaluated candidates, not a global optimum.

The reduce-only guard

The venue has no immediate reduce-only order: placeOrder and placeOrderFor take no such flag, the order type is a closed enum of four, and the official close helper sends a plain opposite-side IOC. So ReduceOnlyGuard acts as the account's own operator, with these properties:

  • Deployed per owner, with the chain (50383), MarginBank and trusted pools fixed at construction.
  • Admitted by the owner's per-pool grant of placeOrderFor (selector 0x80054449) in the OperatorPermissionsRegistry, the same grant dreamDEX's “Link a bot” writes.
  • Callable only by the owner. It places only IOC orders for the owner, cancelling the taker on a self-match.
  • Each action has an id that commits to the chain, guard, bank, owner, pool, the exact signed position the owner confirmed, side, quantity, limit, deadline, order type and a nonce.
  • Before the venue call: the live position must equal the confirmed one exactly, the side must reduce it, and 0 < quantity ≤ |position|, on the lot and above the minimum. The id is reserved and a reentrancy lock held.
  • After the venue call: the position must be on the same side or flat, no larger, and reduced by at most the quantity. Otherwise the whole transaction reverts, venue fills included.
  • It holds no tokens and has no deposit, withdraw, approve, cancel or arbitrary-call function.

On a fork of the real venue, a test-only harness that skips the guard's preconditions makes the venue flip a position. The production postcondition then reverts everything, and the trace is kept as evidence.

Execution and the run ledger

An action record is written before anything is broadcast. It tracks three states separately: the transaction (NOT_SENT, PENDING, UNKNOWN, REVERTED, CONFIRMED, NOT_INCLUDED, INVALIDATED), the fill (NO_FILL, PARTIAL_FILL, REQUESTED_FILL) and the goal (GOAL_MET, GOAL_NOT_MET, BASELINE_CHANGED, UNAVAILABLE, EVIDENCE_MISMATCH).

Refresh. Right before sending, the account is read again at one pinned block. The confirmation is invalidated, and never sent, if any position, collateral, order, stop, account mode, allowance or risk setting changed, if the fresh read does not reconcile exactly, if the candidates re-evaluated under the original scenario now select a different one, if the confirmed order would fill a different amount against the fresh book, or if less than 30 seconds remain to its deadline.

Unknown transactions. Every transaction a run sends is persisted with its hash and nonce before the signed bytes leave the process; the bytes themselves are never written. A step without a receipt is resolved from chain data before anything else is sent. Only a canonical receipt settles it. A failed lookup is not “not found”, and a used nonce proves nothing until the transaction that consumed it is located with a different hash. Until then the step stays UNKNOWN, its worst-case fee stays reserved, and the run stops with exit 40 so it can be resumed.

Cleanup. Cleanup is driven by chain state. Each remaining position is closed by a guarded full close, at most three attempts per leg; a leg the guard cannot close is reported and never closed with an unguarded order. Grants are revoked, collateral withdrawn and the allowance cleared, and the end state is verified at one block across all 21 pools. Every main-phase send must leave enough gas under the plan's cap for the cleanup the run could need right after it: at most 12 transactions at 13M gas each for the live plan.

Binding and recomputation

Before a receipt is credited, it is bound to the saved confirmation: sender, destination, exact calldata, hash, nonce, value, chain, the guard's single ReduceExecuted event and the taker order's owner. A mismatch is EVIDENCE_MISMATCH. In tests, 19 synthetic tamperings and six alterations of a real fork receipt were all refused.

Then the ledger has to be explained by the transaction's own rows: the size moved by exactly the fills, realized PnL floored per fill, funding settled once on the whole position, fees on each fill's notional, unlocked collateral moved by realized − funding − fees, and nothing else in the account touched. Anything unexplained gives BASELINE_CHANGED rather than credit. Finally the real remaining portfolio is valued at the scenario's original absolute prices and shown beside the baseline, the forecast and the account's actual health.

Exit codes

npm run mr:live
CodeMeaning
0The guarded reduction was bound, recomputed GOAL_MET, and cleanup was verified.
1Refused before signing anything. Nothing was broadcast and no cleanup ran.
2A transaction was signed but none reached the node. The account was not touched and no cleanup ran.
10Cleanup verified, but no reduction was demonstrated: nothing needed, nothing feasible, the confirmation was invalidated, or the goal was not met.
20A started run aborted (soft loss stop, a foreign transaction, an interruption) and cleanup was verified.
30Cleanup was not verified.
40A transaction is unresolved. Nothing else was sent; resume with the same plan and directory.
50An unexpected error after transactions reached the network, none of them unresolved. Inspect the run, then resume it.
60The run's own ledger cannot be read back. Inspect the account on chain before anything else.

An unexpected error is classified from the run ledger persisted before each broadcast and the process's own count of signatures and broadcasts, never assumed to be a refusal. After 40, 50 or 60 no fresh run of the account starts until that run is finished. A resume that stops before signing anything writes nothing and exits with the run's own state; one that reaches a new result it cannot write exits with that result, never 0.

The read-only commands exit non-zero too: reconcile 3 unless EXACT, analyze 3 when blocked, plan and refresh 4 when the confirmation is refused or invalidated, and recompute 0 only for GOAL_MET.

Network, contracts, versions

NetworkSomnia Hideki testnet, chain 50383. Any other chain is refused.
MarginBank0x478769E1177C0773c43A593DCCc4Ad15F15F1394
BTC pool0x4181dd08E73BbCdd1Ba3E615A18f6abD941b94C0
ETH pool0xD6Cb1DD58580B84D76068aC21594219ce03aeddf
Operator registry0x6B2FbfeD328FF5F20C0BE7D3C3437613aFFE8D3a
USDso0xed7993A74dAe1e6656B704A424DcD8711aAf4F72
Client@somnia-chain/markets-sdk 0.32.1, viem 2.57.7, TypeScript 5.9.3, Node 22
ContractsSolidity 0.8.28 (optimizer 200 runs, cancun), Foundry 1.8.4, forge-std v1.17.0

Evidence

Each claim on this site is labelled by where it was checked. Fixture means synthetic data in unit tests. Fork means a local anvil fork of the real venue at a recorded block. Live means the Somnia Hideki network itself. The live write, one guarded reduction on the project's own test account, has run: goal met, cleanup verified. The app shows its transactions, recomputation and cleanup.

ClaimWhereResult
Scenario arithmetic, locks, funding and rounding (the plan's two worked fixtures included), the transaction ledger, exit codes after injected faults, the publication boundary and the run-state modelFixture260 / 260 unit tests
The independent read-back of a run, on a real-venue fork run replayed from its recorded chain answersFork, block 34,671,145, replayed93 / 93 checks hold; every tampered copy in the tests fails
Guard behaviour against mocksFixture9 / 9 Solidity tests
IOC limits, guard invariants, rollback after venue fills, per-fill ledgerFork38 / 38 Solidity fork tests
The full loop on a real 6-market account: analyse, guard, refresh, send, bind, recomputeFork, block 33,472,615ETH −50% filled 0.8165 ETH inside the limit; recomputed buffer equals the forecast; GOAL_MET
The live executor: happy path, tampering, book change, partial fill, owner change, timeout, RPC outage, refused preflight, cleanup retries and failure, a result that cannot be written, funding driftFork, blocks 33,472,615 and 33,609,47512 / 12 scenarios as expected
Independent reconciliation and the bank's roundingLive, read-onlyEXACT for every account checked
One guarded reduction on the test account, with cleanupLive writeRun 1: GOAL_MET, cleanup verified

Limitations

  • The scenario is static. It says nothing about the path to the shocked prices, about fills of resting orders or stops on the way, or about liquidation. An expected fill is not a guaranteed fill.
  • Candidates are finite. The preferred candidate is the best among those evaluated, not a global optimum.
  • Faults on the fork were injected by the rehearsal. They show how the executor responds, not how often such events happen live.
  • Stops are listed from the indexer and checked on chain; the registry has no on-chain enumeration, so their completeness still rests on the indexer.
  • Fork gas figures follow Ethereum's schedule, not Somnia's. The live plan asks the live RPC for estimates.
  • Testnet only. Nothing here touches mainnet or real funds.