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
- 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.
- SnapshotReads the whole account and all 21 pools at one block, then rechecks the block hash.
snapshot.ts - ReconcileRebuilds health equity and margin requirements from raw state and compares them with the bank's own getter.
reconcile.ts - ScenarioFixes absolute BTC and ETH prices; every other held market stays at its reference mark. The object is hashed.
scenario.ts - CandidatesPrices 25, 50, 75 and 100% reductions against the live book, then values what remains at the scenario prices.
candidates.ts - Confirm and refreshJournals the chosen action before anything is sent and re-reads the account right before sending.
action.ts · journal.ts · refresh.ts - GuardExecutes the reduction only if the live position equals the confirmed one, and reverts if the position flips or grows.
ReduceOnlyGuard.sol - 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 - 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.
| Component | Used for | Reference |
|---|---|---|
| Perps testnet on Somnia Hideki | The venue every read and every order goes to. Any other chain is refused. | chain 50383 · docs |
| MarginBank | Cross-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 pools | All 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 book | The one call the guard makes: an IOC order for the owner, cancelling the taker on a self-match. | selector 0x80054449 |
| OperatorPermissionsRegistry | The 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 indexer | TP/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-sdk | The 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 |
| USDso | The 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.
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.
| Check | Block | Result |
|---|---|---|
| Every holder of every pool rebuilt with floored PnL | 33,516,436 | 7,700 / 7,700 exact |
| The same holders with the SDK's truncation | 33,516,436 | 2,271 / 7,700 exact |
| Accounts with 3, 6, 9 and 21 markets (the last with 181 resting orders) | 33,646,741 – 33,647,348 | all 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(selector0x80054449) 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
| Code | Meaning |
|---|---|
| 0 | The guarded reduction was bound, recomputed GOAL_MET, and cleanup was verified. |
| 1 | Refused before signing anything. Nothing was broadcast and no cleanup ran. |
| 2 | A transaction was signed but none reached the node. The account was not touched and no cleanup ran. |
| 10 | Cleanup verified, but no reduction was demonstrated: nothing needed, nothing feasible, the confirmation was invalidated, or the goal was not met. |
| 20 | A started run aborted (soft loss stop, a foreign transaction, an interruption) and cleanup was verified. |
| 30 | Cleanup was not verified. |
| 40 | A transaction is unresolved. Nothing else was sent; resume with the same plan and directory. |
| 50 | An unexpected error after transactions reached the network, none of them unresolved. Inspect the run, then resume it. |
| 60 | The 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
| Network | Somnia Hideki testnet, chain 50383. Any other chain is refused. |
|---|---|
| MarginBank | 0x478769E1177C0773c43A593DCCc4Ad15F15F1394 |
| BTC pool | 0x4181dd08E73BbCdd1Ba3E615A18f6abD941b94C0 |
| ETH pool | 0xD6Cb1DD58580B84D76068aC21594219ce03aeddf |
| Operator registry | 0x6B2FbfeD328FF5F20C0BE7D3C3437613aFFE8D3a |
| USDso | 0xed7993A74dAe1e6656B704A424DcD8711aAf4F72 |
| Client | @somnia-chain/markets-sdk 0.32.1, viem 2.57.7, TypeScript 5.9.3, Node 22 |
| Contracts | Solidity 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.
| Claim | Where | Result |
|---|---|---|
| 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 model | Fixture | 260 / 260 unit tests |
| The independent read-back of a run, on a real-venue fork run replayed from its recorded chain answers | Fork, block 34,671,145, replayed | 93 / 93 checks hold; every tampered copy in the tests fails |
| Guard behaviour against mocks | Fixture | 9 / 9 Solidity tests |
| IOC limits, guard invariants, rollback after venue fills, per-fill ledger | Fork | 38 / 38 Solidity fork tests |
| The full loop on a real 6-market account: analyse, guard, refresh, send, bind, recompute | Fork, block 33,472,615 | ETH −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 drift | Fork, blocks 33,472,615 and 33,609,475 | 12 / 12 scenarios as expected |
| Independent reconciliation and the bank's rounding | Live, read-only | EXACT for every account checked |
| One guarded reduction on the test account, with cleanup | Live write | Run 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.