Blockchain Security
Zodiac Treated Four Bytes of Revert Data as Authorization in the Gnosis Pay ERC-1271 Exploit
Legacy @gnosis.pm/zodiac 3.4.0 introduced the ERC-1271 call-status regression; the representative Delay pinned still-affected 3.4.1; v3.0.2 was the first fixed tag after the package rename; and current 4.3.0 is SOSEC's default evaluation target for new deployments and migrations, with old signing paths paused, old queues closed, and normal-return and revert-magic signers tested as a pair.

In this article
1 The operational decision comes first: stop the old path, inspect old queues, and deploy verifiable runtime
Decision: an enabled Roles or Delay path enters remediation scope when it can reach the affected contract-signature checker or when its runtime provenance remains unknown. Immediately pause managed APIs, relayers, and background jobs that produce the old appended contract-signature form, then read every module implementation and unresolved queue item. Every candidate still needs local compatibility, reproducible-build, on-chain runtime, and positive/negative acceptance evidence.
Four literal objects operate in sequence: the signer/module Safe supplies a contract-signer identity → the Zodiac checker interprets the call result as authorization or rejection → the Delay queue persists that decision and starts a clock → the asset/card Safe holds assets and executes the matured operation. The signer Safe and asset Safe in the representative path are different accounts: one participates in authentication; the other performs custody and final execution.
A Solidity low-level call returns two values at once. status reports whether the child call completed normally, while data contains normal return data or revert data. The incident validator ended with REVERT(0,4) while making its error data equal the ERC-1271 success value 0x1626ba7e. A safe predicate requires both status=true and matching magic. The failed control-flow result remains authoritative alongside those bytes.
Gnosis reported that the June 1, 2026 attack removed about $1.496 million and left another roughly $300,000 inaccessible to the attacker; 5,281 wallets with at least one dollar held about $1.8 million in total exposure. Monitoring observed the first large unauthorized transfer at 06:17 UTC and the team confirmed root cause at 08:06 UTC. Gnosis then paused relevant services, repaired modules, restored accounts in stages, absorbed the loss, and restored user balances. The public outcome establishes incident scale and the user result. Operators still need chain evidence for module replacement and queue disposition on every instance.
The lineage contains two numbering generations, so comparing “3.4.1” and “3.0.2” as bare numbers reverses chronology and the security conclusion. The v4 line contained an equivalent status check from its early public tags, and current 4.3.0 retains it. An inventory treats package namespace, tag, commit, module implementation, and runtime hash as one version identity. Operations read deployed code first and use the label to explain provenance.
The decisive state change occurred before assets moved. Zodiac interpreted a failed frame as an enabled module signer, and Delay persisted the operation. After cooldown, the execution function checked queue head, operation hash, and time without calling the original signer again. A source patch prevents new misclassification while authority already stored under the old runtime may remain executable. Patch deployment, old queues, account authority, and financial restoration are therefore four concurrent closure objects.
2.1 Three digests bind different state; a deployed patch applies to new signer decisions
moduleTxHash is an EIP-712 digest under the Delay domain over the selector-bearing canonical call and timestamp-derived salt; it keys consumed[signer][moduleTxHash]. The fallback handler wraps that digest in a SafeMessage under the signer Safe's domain. Delay's queue hash covers only to/value/data/operation, with the numeric nonce locating its mapping entry. Investigation tooling retains all three identities and keeps replay, signature, and queue questions separate.
This state split determines repair order. A new checker rejects a new failed-magic request. For an existing item, executeNextTx() uses the stored operation hash and clock. Operations records therefore need two closure blocks: the block where new vulnerable authorization stopped and the block where the final authorization created by the old predicate became non-actionable. The account closes this technical path only after the second point.
3 Runtime, signer reachability, and queue state together define exposure
Potential scope begins with actual on-chain instances. On every supported chain, list each Safe, singleton, owners, threshold, guard, fallback handler, enabled modules, module proxy, implementation, runtime hash, cooldown, expiration, and queue head and tail. Follow minimal proxies, implementation slots, factory parameters, or custom master-copy relationships to the executing code. Explorer names, ABI, package lockfiles, and bytecode length are candidate-discovery aids; final classification is bound to the incident-block execution path.
Runtime classification uses vulnerable, fixed, and unknown states. Fixed source or a reproducible build can expose _isValidContractSignature() directly. For an unknown implementation, a fixed-block fork presents two signers with identical four bytes delivered through RETURN and REVERT. Authorization of both identifies this regression; authorization of the normal return alone closes this defect. A real production module call may write a queue item, so dynamic classification runs on a fork or dedicated low-value account.
Reachability then narrows candidates through public moduleOnly entries, appended signature tails, contract signer types, module enablement, and caller capability. Queue state raises urgency. Every account records separate evidence for potentially exposed, attempted, queued, executed, contained, and restored states. Contained means new vulnerable authorization has stopped and every old item has a terminal state. Restored additionally requires trusted authority and reconciled user value.
3.1 Exact asset details belong in one ledger; exposure, transfer, control, and restoration stay separate
Gnosis's asset table reports a total of approximately $1,496,151: $641,159 GNO, $453,175 EURe, $399,121 USDC.e, $2,202 SAFE, $323 WETH, $135 xDAI, $28 USDC, and $7 USDT. The components are displayed as whole dollars, so their visible subtotal differs from the approximate total by one dollar through rounding. The case keeps both the official total and every line item, with raw token quantity and log index as the time-stable ledger.
Roughly $1.8 million of total exposure across 5,281 wallets with at least one dollar exceeded the transferred-asset table, and the project described about $300,000 as inaccessible to the attacker. The public post-mortem assigns no single chain reason to every dollar. Account ledgers use available evidence to record freezing, failed execution, cancellation, unrealized value, or unknown disposition. Gnosis restored user balances from its own resources, so financial restoration may also exceed asset recovery.
EURe migration, wrapping, and intermediate contracts can emit several Transfer events for one economic asset. Reconciliation builds a flow graph from token contract, uint256 amount, decimals, log index, and pre/post balance, then calculates net inflow to attacker-controlled addresses. Dollar valuation has a separate price source and timestamp, stored beside the incident-day project account.
The representative 17.855442968223287575 GNO and 364.071530831603544366 EURe amounts come from the second execution receipt and balance check and establish mechanism only. The official table supplies aggregate incident scope; the two evidence sets answer different questions. Scope covers the Gnosis Pay card-Safe compositions using the relevant Delay/Roles modules and signature path. The claim does not extend to Gnosis Chain consensus or ordinary Safe core.
4 Detection and containment must beat the queue clock
A Delay queue event is a security state change. The live pipeline joins it to complete input, historical implementation, and trace, then emits caller, Delay, signer Safe, fallback handler, first-slot candidate, membership-reach state, asset Safe, queue index, executable and expiry time, operation, MultiSend children, token, recipient, and registered service intent. High-value delegatecall, authority changes, first-seen signer paths, and items without registered intent enter human review at the creation block.
Failed magic is a high-specificity signal. Archive traces or fixed-block replay find an ERC-1271 call with failed status and a magic prefix, then confirm that an ancestor reached the Zodiac checker. A top-level receipt lacks the internal status/data pair, so the evidence pipeline retains traces or reproducible historical replay material.
trace condition:
call.type = STATICCALL
call.selector ∈ {0x1626ba7e, 0x20c13b0b}
call.success = false
call.output[0:4] = 0x1626ba7e
enrichment:
signer Safe / handler / Zodiac ancestor
membership reached or not reached
queue mutation and registered intent
Execution clustering groups by caller, beneficiary, inner target, runtime hash, and normalized batch hash, then counts Safes and asset types within ten minutes. Managed services retain request ID, session, device, signer method, relayer, Safe, salt, expected digest, and transaction hash. Behavioral trace detects changed addresses and recompiled validators; intent records distinguish legitimate direct calls from unauthorized queueing.
4.1 Queue flooding requires role separation, rate limits, and auditable batch cancellation
An actor can exhaust human review with many low-value entries and hide a high-impact action among them. Production design separates queues by role or risk and applies verifiable queue-rate limits by caller, account, and window. Owner, module, guard, upgrade, unlimited allowance, arbitrary delegatecall, and high-value transfer operations receive longer cooldown. Observable rejection records let flood control preserve the evidence stream.
Batch cancellation follows the deployed version. Sample v1.1.0 can only use setTxNonce() to skip a contiguous index range. Before that action, the operator lists every hash, user intent, maximum consequence, and resulting head; an independent authority approves the operation and state is reread after finality. A future per-hash or per-role batch facility should constrain guardians to revoke, pause, extend cooldown, or disable modules, with no authority to transfer assets, add owners, or lower threshold.
Response order follows dependency:
- Freeze new authorization. Stop every old signing API, relayer, and job; preserve request, configuration, trace, and queue snapshots.
- Sort by time remaining. Decode all unresolved items and prioritize authority changes, delegatecall, approvals, and high-value asset actions.
- Neutralize through independent authority. Cancel, batch-skip, disable the module, or protect assets, recording a terminal state for each item.
- Replace vulnerable runtime. Bind source, build, and on-chain code and close every alternate route that delegates into the old checker.
- Restore account and value. Reconcile authority and balances from a trusted block, then complete financial accounting and user notice.
- Admit cohorts after paired tests. The normal signer passes; the revert-magic signer fails before any queue side effect.
SOSEC starting recommendation: high-value cooldown covers the local ninety-day P99 for detection, intent confirmation, emergency signing, and chain finality, plus a congestion buffer established by stress testing. Wallet operations and contract engineering jointly own it; measurement window, threshold, and rollback trigger are calibrated per chain. The sample's historical 180 seconds describes the incident condition; every production baseline needs local calibration.
5 Repair ends when runtime, old queues, account authority, and user assets are all trusted
The upgrade manifest uses on-chain relationships as keys: chain ID, Safe, module proxy, implementation, eth_getCode, runtime hash, source commit, compiler, optimizer, libraries, immutables, initialization data, replacement transaction, and enable/disable blocks. Fixed source describes expected behavior for a build. On-chain hash and upgrade transaction establish when a user account obtained it.
A new or migratable system evaluates current 4.3.0 first; v3.0.2 marks only the first-fixed boundary in the v3 lineage. Compatibility tests cover EOA, approved-hash, standard ERC-1271, nested Safe, Roles, Delay, and existing business encoding. When a module is immutable, deploy a reviewed replacement and use an atomic or explicitly ordered Safe transaction to disable the old module and enable the new one. Old address, initialization values, and replacement transaction enter inventory.
Queue disposition surrounds code replacement. First enumerate items authenticated by the old runtime and pin creation block, executable time, expiry, inner action, and intent. Read them again after upgrade to confirm cancellation, skip, expiry, or re-authorization. An isolated checker upgrade leaves stored execution rights; isolated queue clearing lets the vulnerable entry generate new ones. The same change ticket closes both.
Account restoration starts at a trusted block before malicious queueing and compares owners, threshold, guard, fallback handler, modules, allowances, delegated authority, sessions, and balances. Current state may already contain authority changes caused by queued actions, so copying it into a replacement can preserve risk. Technical restoration and financial restoration have separate timestamps and evidence and meet at the user closure point.
5.1 Paired canaries, rollback conditions, and cohort reopening form production admission
The positive canary uses a standards-compliant contract signer that normally RETURNs magic, creates one harmless queue record through the production-shaped path, and is cancelled or executed under supervision. The negative canary uses REVERT(0,4) with the same magic and must fail at signer recovery, leaving consumed, queue nonce, hash, timestamp, and events unchanged. Both carry registered intent and remain visible to the behavioral alert.
Rollback triggers include runtime-hash mismatch, wrong implementation resolution, legitimate-signer incompatibility, any side effect from revert magic, an omitted old item, unreconciled account authority, or missing monitoring visibility. Rollback keeps managed entry paused, restores an accepted implementation and module relationship, and preserves completed safety cancellations. The selected artifact must retain the correction before restoration.
Reopening proceeds by cohort. Each batch records Safe count, asset ceiling, entry point, implementation hash, paired acceptance, queue terminal states, and observation window. An anomaly stops the next cohort and rolls back the current one. After full reopening, historical hunting continues from the first vulnerable deployment block through the block where the last old authorization became non-actionable; every failed-magic, suspicious-queue, or clustered-execution hit receives account review.
The interface labels a Delay action as queued and shows executable time, expiry, action summary, and available cancellation route. Final status changes after execution. Emergency notice uses at least one channel independent of the primary session. User restoration evidence links to the specific transaction, raw token amount, and account state; the global dollar total remains summary context.
6 Technical appendix keeps the minimum receipt for each question: source for decisions, bytes for input, trace for path
This appendix carries reproduction details that the main narrative no longer needs to derive repeatedly. Each evidence class answers one question. Source describes how a pinned version handles call results. ABI and Account Kit explain transaction construction. Incident-block runtime explains the machine behavior of the unverified validator. Trace shows the order and status in which the objects met. Storage and logs show how authorization became state and produced asset effects.
6.1 Source receipts pin the regression, representative deployment, fix, and current stable line
The parent of 9a9e380 retained success && magic; that commit replaced the first tuple element with an empty position and shipped in legacy 3.4.0. The 3.4.1 snapshot still uses a data-only predicate in SignatureChecker.sol, lines 127–135. Fix 4cf1530 restored status, v3.0.2 resolves to commit 4a24501572db953f355634e20701dbf50732f625, and current v4.3.0 resolves to b5fad243… with the invariant still present.
Representative Delay proxy 0x7ECf…78f3 resolves to implementation 0x4A97E65188A950Dd4b0f21F9b5434dAeE0BBF9f5, whose verified source matches Delay v1.1.0 commit 515737f67bb704d0ce14d45cd6f7cc8e6b4655d1; package.json, lines 1–32, locks legacy 3.4.1. execTransactionFromModule(), lines 128–140, writes the queue; executeNextTx(), lines 176–200, consumes the head; and lines 214–220 define the four-field hash.
Safe and its handler form the sample propagation path. The pinned CompatibilityFallbackHandler.sol, lines 28–80, translates the selector and constructs SafeMessage; Safe.sol, lines 274–331, establishes that the candidate call precedes membership. Later Safe commit 77901a5a1ad835b74ad3b72f73a8412cfe491c57 added successful-status and exact-length checks as independent defense in depth. Zodiac's success && magic remains the root-cause repair for this incident.
6.2 Byte receipts pin two offset origins, the four-byte revert block, and the chain reproduction entry
The representative input is 871 bytes long. Zodiac reads outer r=0xce7d…5561, absolute s=0x244, and v=0 from complete msg.data, then hands the 0xc2-byte region between 0x244 and salt to the signer Safe. Safe starts a new origin inside that slice: slot one identifies candidate 0x5a779…97a35 with relative s=0x82 and v=0. Slot two contains the attacker EOA, but the threshold-one process ended when slot one reverted.
complete calldata origin:
outer r = signer Safe 0xce7d…5561
outer s = 0x244 → 0xc2-byte Safe signature slice
salt = ABI-encoded millisecond timestamp
Safe-slice origin:
slot 0 r = candidate 0x5a779…97a35
slot 0 s = 0x82 → length 0x20 || 32 zero bytes
slot 1 = present, unprocessed at threshold 1
Fixed-block reads show threshold one and owners 0x25d21d6a390b41ae032121761f3f62b8f675713e, 0xe92ed76e5b853f246fd6dc8bef58339ed1fa05bd, 0xc75e0137aeb875b76f326481bdbf19877ce01c4d, and 0x1f778edc79aec4e17ea4daf8e2f438cec26a7d13. The candidate is absent from that set and the trace ends before GS026. The mechanism section uses that fact to correct the role; the final case boundary records the candidate role and the absence of acquired owner authority.
The malicious contract has no verified source. Its incident-block runtime recognizes ERC-1271 selectors 0x1626ba7e and legacy 0x20c13b0b. The terminal block forms 0x1626ba7e through PUSH4 0x0b135d3f → SHL 0xe1 → MSTORE → REVERT(0,4). Fixed-block eth_call and the attack trace both show failed status with the same error data. These machine facts classify the validator. Public evidence leaves authorship, source language, and reconnaissance method unknown.
Independent reproduction preserves raw input, block hash, implementation storage, and runtime; establishes the four-argument Delay ABI boundary; parses the Zodiac tail and Safe-relative offsets; recomputes ModuleTx and queue hashes under their actual formulas; joins item 11 creation to execution input; checks token logs and balances in the second receipt; and finally compares old checker, fixed checker, and legitimate signer on a fixed-block fork. Full raw input, runtime, and RPC JSON remain controlled case material. The public report supplies selectors, offsets, addresses, source lines, and transaction hashes for review.
Public evidence establishes the predicate, representative deployment, nested signature, queue/execute pairing, asset effect, and project restoration. The post-mortem does not enumerate every module replacement, cancellation, and authority-restoration transaction for every account, and the actor's account-selection method remains undisclosed. Operators complete those objects with their historical account graph, request logs, and chain receipts. A missing object remains unknown or pending instead of being inferred from exploit success.
7 Closing assessment: four bytes describe data; authorization must retain call status
The root cause is one verifiable Boolean expression. The affected Zodiac checker accepted magic in returnData while discarding success=false from the same call. The representative path persisted that result as Delay item 11, and a separate asset Safe completed MultiSend inside the executable window. Repair has equally explicit scope: deploy verifiable success && magic runtime, disable old modules, disposition old queues, and restore account state and user value.
The reusable engineering rule is compact. Whenever a low-level call supplies evidence for authorization, price, ownership, or execution, status and data form one indivisible invariant. Whenever a time system persists authorization, monitoring, independent cancellation authority, and enough response time jointly create its security value. Paired canaries, runtime inventory, and queue terminal states turn that rule from code-review advice into an accepted production control.
Gnosis restored user balances, supplying the financial endpoint for this event. Every other deployment remains governed by its own chain runtime, old queues, and account authority. A reader should close this report able to state four things precisely: which four objects connect, which predicate failed, why stored execution rights require separate disposition after a patch, and which evidence admits an account back to service.
Research record
8Evidence, objects, and sources
The material below preserves the identifiers and references used in this report.
8.1Research objects
Products, actors, techniques, affected objects, and control points discussed in the report.
The representative Delay inherited an ERC-1271 checker that discarded low-level call status
Current stable npm version on July 27, 2026; production still requires local compatibility and runtime acceptance
Gnosis Chain contract that reverts with the ERC-1271 magic value
Deployed the malicious contract and called multiple Delays
ERC-1271 isValidSignature(bytes32,bytes) success return value
Restores the staticcall success condition
Legacy 3.4.0 change that ignored call status
Created Delay queue item 11 at 08:29:05 UTC
Executed the same MultiSend batch at 08:53:45 UTC
8.2Event chronology
- Legacy Zodiac 3.4.0 released
The signature refactor entered a release with low-level call status ignored.
- Malicious ERC-1271 contract deployed
The attacker EOA created the revert-magic contract on Gnosis Chain.
- First large transfer
Gnosis monitoring observed the first large unauthorized transfer.
- Root cause confirmed
The team located Zodiac ERC-1271 result handling and paused relevant services.
- Fix and regression committed
The status check returned and a revert-magic signer test entered the repository.
- Public post-mortem current revision
Gnosis recorded its current impact, restoration, and root-cause account.
- Current stable line reviewed
The current stable npm version is @gnosis-guild/zodiac-core 4.3.0.
8.3Sources and material
- Gnosis incident post-mortemhttps://www.gnosis.io/blog/post-mortem-gnosis-pay-vulnerability-exploit
- EIP-1271 contract-signature standardhttps://eips.ethereum.org/EIPS/eip-1271
- Zodiac Core regression commit 9a9e380https://github.com/gnosisguild/zodiac-core/commit/9a9e38025ad7518f6e5350aeac673435a7db70f1
- Zodiac Core fix commit 4cf1530https://github.com/gnosisguild/zodiac-core/commit/4cf15302228a578985b0af2a3e7000dcded2da6b
- Revert-magic regression test b87fe79https://github.com/gnosisguild/zodiac-core/commit/b87fe79e66c9c5bc3c46f3018ea53ebddbed516b
- Representative Zodiac Delay v1.1.0 pinned sourcehttps://github.com/gnosisguild/zodiac-modifier-delay/blob/515737f67bb704d0ce14d45cd6f7cc8e6b4655d1/contracts/Delay.sol#L108-L220
- @gnosis-guild/zodiac-core 4.3.0 current stable packagehttps://www.npmjs.com/package/%40gnosis-guild%2Fzodiac-core/v/4.3.0
- Safe 1.4.1 core sourcehttps://github.com/safe-fndn/safe-smart-account/blob/bf943f80fec5ac647159d26161446ac5d716a294/contracts/Safe.sol
- Safe 1.4.1 signature decoder sourcehttps://github.com/safe-fndn/safe-smart-account/blob/bf943f80fec5ac647159d26161446ac5d716a294/contracts/common/SignatureDecoder.sol
- Historical Gnosis Pay Account Kit transaction and ERC-1271 encodinghttps://github.com/gnosispay/account-kit/blob/51f60789df3252c06aab963654a183f25abc6afd/src/entrypoints/accounts-actions/execute.ts#L43-L318
- Safe 1.4.1 compatibility fallback-handler pathhttps://github.com/safe-fndn/safe-smart-account/blob/bf943f80fec5ac647159d26161446ac5d716a294/contracts/handler/CompatibilityFallbackHandler.sol#L28-L80
- Safe4337Module v0.2.0 handler inheritancehttps://github.com/safe-global/safe-modules/blob/d5de32548c968533ace53f514fd8ce2d276c1a27/4337/contracts/Safe4337Module.sol#L1-L22
- Safe4337Module v0.2.0 deployment manifesthttps://github.com/safe-global/safe-modules-deployments/blob/e6b1e025b281d295224408ea8be54a89ea3dd060/src/assets/safe-4337-module/v0.2.0/safe-4337-module.json
- Safe post-incident defense-in-depth commit 77901a5https://github.com/safe-global/safe-smart-account/commit/77901a5a1ad835b74ad3b72f73a8412cfe491c57
- Safe non-owner reverting-contract regression testhttps://github.com/safe-global/safe-smart-account/blob/77901a5a1ad835b74ad3b72f73a8412cfe491c57/test/core/Safe.Signatures.spec.ts#L1123-L1145
- Malicious ERC-1271 contract and bytecodehttps://gnosis.blockscout.com/address/0x5A77953CAa27eD4638F4DfdC665b8064D0e97A35
- Malicious contract creation transactionhttps://gnosis.blockscout.com/tx/0x771a8040f3ae89fd28eecb96bbe1bfe452a868a09febe0368eb00a1105603ea2
- Representative queue transactionhttps://gnosis.blockscout.com/tx/0x5f562057e1963964fc3a6d8b105057f3ab9f9dd7091ff5bdd8d51453f51bd456
- Representative execution transactionhttps://gnosis.blockscout.com/tx/0x4d3888cf3c68bffb2013001aabaf7b3596fc5d3f5d4cc5f139487d8a4140ae14
- Representative signer/module Safehttps://gnosis.blockscout.com/address/0xce7d2f8e54e1104ed7a4c1f88dac63aa4a825561
- Representative asset/card Safehttps://gnosis.blockscout.com/address/0x0cae88383f2d1f308292e58cd71984bb6120e03d
- Safe smart-account signature documentationhttps://docs.safe.global/advanced/smart-account-signatures
- Solidity revert and error-data semanticshttps://docs.soliditylang.org/en/latest/control-structures.html#revert-statement