Reproduced Exploit
SYMMIO `liquidatePartyA` accepts a replayable, nonce-free Muon liquidation signature
Chain
Other
Category
signature
Date
Aug 2023
Source
AuditVault
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Balances (native / ERC-20 / NFT), Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Reproduction of a public audit finding curated by AuditVault — the original finding: 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non. Standalone Foundry PoC and full write-up: 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non_exp in the
evm-hack-registrymirror.
Vulnerability classes: vuln/signature/replay · vuln/defi/liquidation · impact/loss-of-funds
Reproduction: the test deploys the REAL audited SYMMIO liquidation facet +
LibMuonsignature verification, produces a genuinely-valid Muon gateway signature (the test holds the gateway key), and replays it after the liquidated party has become solvent — draining the party's entire allocated balance through the real liquidation path.
Root cause#
LibMuon.verifyLiquidationSig builds the hash the Muon oracle signs from
reqId, liquidationId, upnl, totalUnrealizedLoss, symbolIds, prices, timestamp
and the chain id — but no partyANonces[partyA] / partyBNonces term
(src/symm/contracts/libraries/LibMuon.sol, the
verifyLiquidationSig payload). Every other Muon schema in the same file
(verifyPartyAUpnl, verifyPairUpnl, …) DOES include the party nonce, so their signatures
are invalidated the moment a party acts. The liquidation schema is the exception.
Because the signed hash is invariant under the nonce, a single valid liquidation signature
stays valid across a party's legitimate state changes. A liquidator (or the protocol, or a
malicious party racing a close) can therefore submit a stale liquidation authorization —
carrying an out-of-date, more-negative upnl — against a party that is no longer
liquidatable. liquidatePartyA trusts the signature's stale upnl for the
availableBalance < 0 solvency check, so the solvent party is liquidated anyway and loses
its funds. The recommended fix (SYMMIO PR #34) is to include the partyA/partyB nonces in the
liquidation signature.
Note: the vendored LibMuon.verifyTSSAndGateway body is commented out by the SYMMIO devs
"for testing" — their own comment states it must be enabled in the production-deployed
version. The PoC restores the production gateway-ECDSA check so the signature is genuinely
verified; the vulnerable verifyLiquidationSig schema is left byte-for-byte as audited.
Exploit walkthrough (numbers from the test)#
PartyA has allocated = 109, an open LONG position with cva = 6, mm = 10, lf = 4
(so cva + lf = 10). partyAAvailableBalanceForLiquidation(upnl) = allocated - (cva + lf) + upnl.
- Sign while liquidatable (nonce 0). At
upnl = -100, available= 109 - 10 - 100 = -1 < 0, so the gateway signs a liquidation payload for partyA. No nonce is bound. - PartyA legitimately acts.
partyANonces[partyA]advances to 1 and partyA's true position is now solvent (realupnl ≈ -5→ available= 109 - 10 - 5 = 94 > 0). A correctly-signed fresh signature carryingupnl = -5is rejected by the real guard:"LiquidationFacet: PartyA is solvent". - Replay the stale signature. The old signature still passes the real
verifyLiquidationSigat nonce 1 (the hash omits the nonce) and its staleupnl = -100makesavailable = -1 < 0, soliquidatePartyAmarks the solvent partyA for liquidation. - Finish the liquidation with the same signature (
setSymbolsPrice) andliquidatePositionsPartyA. PartyA's entireallocatedbalance is zeroed:109 → 0.
testSchemaOmitsNonce additionally proves the root cause at the hash level: the liquidation
hash is byte-identical at nonce 0 and nonce 1 (replayable), whereas the nonce-bearing
verifyPartyAUpnl schema's hash changes (not replayable).
Reproduction#
cd 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non_exp
../_shared/run-poc/run_poc.sh 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non_exp -vvvvv
Expected result: 2 passed. testExploit asserts partyA's allocated balance goes from
109e18 to 0 via the replayed stale signature; testSchemaOmitsNonce asserts the
liquidation hash ignores the nonce while the party-upnl hash binds it. See
test/26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non_exp.sol.
Sources#
- AuditVault finding #26346
- Sherlock 2023-08 SYMMIO contest — vulnerable code under
symmio-core/ LibMuon.verifyLiquidationSig- SYMMIO fix PR #34 — add partyA/partyB nonces to the liquidation signature
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non_exp (evm-hack-registry mirror).
- AuditVault finding: 26346-h-1-liquidatepartya-requires-signature-which-doesnt-have-non.
- Upstream DeFiHackLabs PoC directory: src/test.
Alerts & third-party analyses
- DeFiHackLabs incident explorer: search "SYMMIO
liquidatePartyAaccepts a replayable, nonce-free Muon liquidation signature". - Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.