Gravity Bridge Denom-Mapping Registry Poisoning Drain (2026-05-29)

  • Name: Gravity Bridge denom-mapping registry poisoning drain
  • Date: 2026-05-29
  • Protocol: Gravity Bridge
  • Chain: Gravity Bridge + Osmosis + Ethereum
  • Category: bridge verification failure / registry poisoning / token-identity aliasing / metadata-trust failure
  • Loss: about $5.4M across USDC, USDT, WETH, and PAXG
  • Summary: Gravity Bridge did not lose funds because its Ethereum custody contract signatures were forged. It lost funds because the bridge’s token-identity registry was poisoned before those signatures were ever requested. The attacker minted worthless Osmosis tokenfactory assets, IBC-transferred them onto Gravity chain, then called Ethereum-side deployERC20() with fabricated cosmosDenom strings that embedded the real Ethereum custody-token addresses as path segments. Public analysis indicates the resulting MsgERC20DeployedClaim flow registered those real custody-token addresses as the canonical Ethereum representation of the fake Cosmos-side denoms. Once DenomToERC20Lookup was poisoned, legitimate validator-signed outgoing batches paid real USDC, USDT, WETH, and PAXG to the attacker against worthless source-side balances.
  • Trigger / entry point:
    • Gravity’s Ethereum contract 0xa4108aa1ec4967f8b52220a4f7e94a8201f2d906 exposed a permissionless deployERC20() path for Cosmos-originated assets.
    • The function accepted _cosmosDenom as an arbitrary string and emitted it verbatim in ERC20DeployedEvent.
    • Gravity chain’s handleErc20Deployed path checked that the claim carried a structurally valid Ethereum address and metadata matching the denom, but did not cross-check the claimed token contract against the existing ETH-originated custody-token registry before writing canonical mappings.
    • The attacker first registered a minimal Gravity validator presence (gravityvaloper1rdwckpx2p3mwu4k8xdz8vmd4x3mqx4v9ars8k) with 80 GRAV so its orchestrator could submit claims alongside legitimate validators.
  • Root cause:
    • Gravity treated denomination identity as trustworthy even though the attacker could shape the string that bridged identity between Cosmos-side denom state and Ethereum-side token registration.
    • The bridge’s permissionless token-registration path accepted attacker-controlled denom strings without restricting them to the actual ibc/HASH identity the destination chain had assigned.
    • The Gravity-chain attestation handler then accepted a claim path that public analysis says ended up registering real Ethereum custody-token addresses for those fake denoms, while metadata checks were satisfied because the attacker had created predictable IBC metadata on purpose.
    • The most conservative preserved lesson is therefore not validators signed malicious withdrawals; it is validators signed withdrawals against a registry the attacker had already taught to lie about asset identity.
    • One technical detail remains publicly incomplete: open-source analysis shows the permissive input and the missing registry cross-check clearly, but the exact mechanism by which the fabricated denom path caused token_contract in accepted claims to resolve to the real custody-token addresses instead of the freshly deployed wrapper contracts still awaits a fuller official postmortem.
  • Exploit path:
    1. The attacker registered Gravity validator julia666, self-delegating 80 GRAV so its orchestrator could participate in claim submission.
    2. On Osmosis, attacker address osmo1m9athjzah02f2mnrgtcke7e5ya3zpvw8lccuss minted four worthless tokenfactory assets designed to mirror USDC, USDT, WETH, and PAXG.
    3. Those fake tokens were IBC-transferred to Gravity chain over Osmosis channel-144 / Gravity channel-10, where each received a normal ibc/HASH denom plus predictable bank metadata.
    4. On Ethereum, attacker address 0x73e95ae5f3b87e02d4547afe86d0d466e9450d6b called deployERC20() four times, passing fabricated _cosmosDenom strings that embedded the real custody-token contract addresses as path components.
    5. Example: the fake USDC path embedded real USDC contract 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 inside the denom string instead of using only the plain ibc/HASH identifier.
    6. Validators observed those ERC20DeployedEvent emissions and submitted MsgERC20DeployedClaim attestations on Gravity chain.
    7. Public analysis indicates handleErc20Deployed then poisoned the denom-to-ERC20 registry so the fake denoms mapped to the real custody-token addresses rather than to the fresh wrapper contracts.
    8. The attacker next submitted MsgBatchSendToEthClaim withdrawals for the fake Cosmos-side assets.
    9. DenomToERC20Lookup returned the poisoned real-token addresses, validators signed outgoing batches 41572 through 41575, and Ethereum relayers executed legitimate batch withdrawals into attacker wallet 0x7b582033061b96cc3f9421e73a749ed7c62da1f9.
    10. Real custody was released: 4,349,701 USDC, 434,072 USDT, 274.34 WETH, and 14.16 PAXG, after which part of the haul moved toward laundering address 0xc8c71ae4261e55a66d9967f2ac252be4e669f562 and secondary EOA 0x4d3ca32e687e871a58b78acac73be59ac37c7a47.
  • What the attacker needed to be true:
    • A minimal validator registration had to be enough to satisfy the claim-submission gate even without meaningful voting weight in Ethereum valset signatures.
    • The bridge had to accept permissionless token registration on Ethereum for Cosmos-originated assets.
    • _cosmosDenom had to remain an attacker-controlled free-form string rather than a validated canonical denom identity.
    • Gravity-chain attestation processing had to trust metadata compatibility and address format more than canonical asset-origin cross-checks.
    • Once the registry was poisoned, validator-signed outgoing batches had to treat the false mapping as normal truth and release real custody without a second balance-sheet sanity check.
  • Technical anatomy worth remembering:
    • This is a strong bridge-verification-failure case where the attacker did not forge signatures, bypass the batch verifier, or steal validator keys. The attacker changed what the bridge believed those honest signatures referred to.
    • The exploit path is a reminder that asset registries are part of bridge authenticity. If token identity can be aliased, honest validator signatures can still authorize fake withdrawals.
    • The metadata checks were not a defense because the attacker controlled the source-side token creation and therefore the predictable IBC metadata that the bridge later treated as confirmation.
    • Gravity had the shape of a cleaner trust model than many multisig bridges, but this incident shows that bridge integrity can still fail in the translation layer between source-chain denoms, token metadata, and destination-side custody asset identity.
    • The missing official postmortem matters. Public evidence is already enough to preserve the registry-poisoning lesson, but the remaining uncertainty about the exact claim-construction path is part of the historical record rather than something to smooth away.
  • Representative transactions, claims, and contracts:
    • Gravity Ethereum contract: 0xa4108aa1ec4967f8b52220a4f7e94a8201f2d906
    • Attacker Ethereum deployer address: 0x73e95ae5f3b87e02d4547afe86d0d466e9450d6b
    • Attacker validator: gravityvaloper1rdwckpx2p3mwu4k8xdz8vmd4x3mqx4v9ars8k
    • Attacker orchestrator Ethereum key: 0x91b52e07132a49dad1b7a3939a51547f79468ec7
    • Validator-creation transaction: F6AA34E8D01C55A5F1E38310E816DBB4BEE25B2FDB29ACD7ED1E25352BA62009
    • Fake USDC deploy transaction: 0x92b2a61d88b3f68d7d3b5cba4a12f53e0c226287702d72de80a60515987e3532
    • Fake USDT deploy transaction: 0x364ebefab8e7c8ddd425447349cc99dcd75ab1751612f7ef9c60c20fdb9a2bb6
    • Fake WETH deploy transaction: 0x871433af303743d61b3f2a38174e9e6655fdc8937c0689122685509dcabbf066
    • Fake PAXG deploy transaction: 0x67a181c333cb75231613c125215168a1eacbb4021d07cf0d91ca8bbaa65f8245
    • Accepted Gravity-chain MsgERC20DeployedClaim transactions:
      • USDC: 786C41F0B9DA6EB8F59A1BC3299504AE47EFC4BBAB13673E391E307DB1BCEE49
      • USDT: 9DF358F96E2BDE91FA96D1CC1CBEBE42E5CA2DD2F5745CD2ADA2591751101A9D
      • WETH: 388112D6C4BEDD74D34157AEC689195B942A8EDC4F476C4230C638CDF399E499
      • PAXG: 14BC7A377F959973613473A5EFB623D4011AED86BE00D68B782DB6D6720F9A37
    • Primary theft wallet: 0x7b582033061b96cc3f9421e73a749ed7c62da1f9
    • Secondary holding wallet: 0x4d3ca32e687e871a58b78acac73be59ac37c7a47
    • Laundering / Tornado-funded address highlighted in public tracing: 0xc8c71ae4261e55a66d9967f2ac252be4e669f562
  • Response / mitigation:
    • Gravity Bridge told validators to halt validators and orchestrators while the incident was investigated.
    • The bridge was paused, stopping further Ethereum↔Cosmos transfers.
    • Public discussion after the halt remained thin; outside researchers reconstructed the registry-poisoning path faster than the team published a formal root-cause writeup.
    • At review time, the open record still lacked a full official postmortem explaining the claim-construction step that turned the attacker-controlled denom path into accepted real-token contract addresses.
  • Current upstream Gravity source does, however, make the missing boundary much more explicit than the incident-era public explanations did: handleErc20Deployed() now calls validateCosmosOriginatedMapping() before metadata checks and then rejects pre-existing mappings in both directions through getCosmosOriginatedERC20ForDenom() and getCosmosOriginatedDenomForERC20(). Even without a polished postmortem, that later hardening preserves the core lesson: origin admission and destination-reserve reservation must fail closed before metadata compatibility or governance-whitelisting logic gets a vote.

Three asset-identity promotions this incident collapsed

  1. A Cosmos-side asset-looking object existed at all

    • The attacker minted worthless Osmosis tokenfactory assets, pushed them over IBC, and gave Gravity a superficially normal ibc/HASH denom plus metadata trail to look at.
    • That should only have meant a candidate foreign asset has appeared, not this thing is already meaningful bridge identity.
  2. That candidate was promoted into canonical Cosmos-originated identity for this exact bridge mapping

    • Gravity’s Ethereum-side deployERC20() accepted attacker-authored _cosmosDenom strings, and handleErc20Deployed accepted a claim flow whose shallow checks focused on address shape and metadata compatibility.
    • In practice, the bridge let a user-shaped denomination path answer the much stronger question what exact source-side asset identity is this?.
  3. That admitted identity inherited live redeemability over an already-funded Ethereum reserve path

    • Once DenomToERC20Lookup was poisoned, later validator-signed MsgBatchSendToEthClaim withdrawals no longer needed to do anything exotic; they just spent against the bridge’s new canonical answer.
    • That is the step that turned a bad registration event into real USDC, USDT, WETH, and PAXG release. Gravity collapsed candidate asset discovered, canonical identity admitted, and existing reserve now redeemable by that identity into one operational path.

Pattern tags

  • asset-identity-registry-poisoning
  • bridge-verification-failure
  • The decisive break was not forged validator signatures or a fake withdrawal proof; it was the earlier moment when deployERC20() plus handleErc20Deployed let attacker-authored denom identity become canonical bridge asset identity.
  • Attacker-controlled IBC metadata satisfied the bridge’s compatibility checks, so looks like the right token metadata was allowed to stand in for the much stronger claim this denom is actually entitled to this destination-side reserve path.
  • Once DenomToERC20Lookup was poisoned, every later batch withdrawal became structurally ordinary. Honest validators and relayers kept authorizing the bridge’s own corrupted answer.
  • The preserved invariant is therefore new source-side asset discovered must stay separate from existing destination reserve may now be redeemed by this asset; Gravity collapsed those two states into one registry write.

Why the exploit worked

  • The attacker never had to defeat Gravity’s Ethereum batch-signature scheme. They only had to corrupt the lookup table that told those honest signatures which Ethereum asset a Cosmos-side denom redeemed.
  • Gravity’s permissionless deployERC20() path accepted _cosmosDenom as an unconstrained string and emitted it verbatim into the event stream. That gave the attacker a place to smuggle real custody-token addresses into what should have been only a canonical denom identifier.
  • On the Gravity-chain side, the acceptance logic prioritized structurally valid Ethereum address plus metadata compatibility over the stronger question is this denom actually allowed to point at an already-live ETH-originated reserve? Because the attacker had created the Osmosis tokenfactory assets and their IBC transfer path, the downstream bank metadata was predictable enough to satisfy those checks by construction.
  • The exploit therefore lived in the bridge’s long-lived asset-identity state, not in one later withdrawal packet. After the registry write, the bridge had already learned the wrong economic meaning: fake Cosmos-side balances now appeared to redeem real USDC, USDT, WETH, and PAXG.
  • That is why the downstream batches looked so normal. MsgBatchSendToEthClaim and Ethereum-side execution were not individually malformed; they consumed a registry the attacker had already taught to lie.
  • The still-missing official postmortem matters because one claim-construction detail remains open, but it does not change the core preserved lesson: registry truth and reserve entitlement were insufficiently fused, so semantic corruption upstream became ordinary custody release downstream.

Detection and audit pivots

  • Review every asset registration or token deployment path as if it were already a custody path. If a registry write can later decide which live reserve a withdrawal touches, then that write deserves the same adversarial review as a direct unlock function.

  • Check guard ordering, not just guard existence. Gravity is a good reminder that the strongest questions should run first: does this candidate denom even qualify as a cosmos-originated identity, and is the destination token already reserved in either direction, before any metadata or governance-acceptance path can make the asset look legitimate.

  • Treat metadata matching as weak evidence only. Name/symbol/decimals compatibility, especially when source-side metadata is attacker-created or IBC-derived, should never by itself authorize a mapping into an already-live custody token.

  • Diff both directions of asset identity before writing canonical mappings: denom -> token, token -> denom, and is this token already an ETH-originated reserve object? Gravity’s public reconstructions are useful precisely because the missing cross-check appears to have lived right next to the vulnerable path.

  • Ask whether downstream withdrawals consume a canonicalized asset-identity object or just the current registry answer. If later batch logic trusts DenomToERC20Lookup without re-proving reserve entitlement, registry poisoning already has direct line-of-sight to custody.

  • Keep semantic-state failures separate from signer-compromise analysis. A bridge can have honest validators, valid signatures, and correct relayer execution while still being broken if the shared asset registry no longer preserves one truthful meaning for reserve redemption.

  • Comparable exploit pattern:

Why this incident matters

  • It broadens the bridge corpus beyond signer compromise, proof-parser bugs, and event-spoofed deposits into the nastier registry tells honest components to lie branch.
  • It shows that bridge asset identity is itself a security boundary. If a fake source-side denomination can become synonymous with a real custody token, downstream validator signatures and batch execution will look perfectly normal while the bridge is already broken.
  • It is also a good example of why permissionless asset registration is not a harmless UX feature when registry entries later decide what custody asset outgoing batches release.
  • The preserved healthy-side boundary is sharper than validate metadata better: discovery of a new Cosmos-side asset should have stayed distinct from the much stronger act of granting that asset rights over Gravity’s already-live USDC, USDT, WETH, and PAXG reserve paths. Bridge asset-identity admission and destination reservation discipline is the positive-design companion for that missing separation.
  • It also sharpens the separate blast-radius question preserved in bridge bounded release design: once the registry was poisoned, one false identity mapping still had direct line-of-sight to large live USDC, USDT, WETH, and PAXG custody instead of first hitting tighter per-route release limits or slower high-value controls.

Failure-prevention checklist

  • Treat asset-registration handlers as custody-critical code. A permissionless deployERC20() or denom-registration path should only be able to create an isolated wrapper candidate, not silently grant rights over an already-live Ethereum custody token.
  • Derive canonical asset identity from provenance-first facts — source chain, canonical denom or source contract, route, custody mode, and explicit destination reservation — rather than from free-form strings plus metadata compatibility.
  • Reserve destination-native and already-canonical ERC-20 custody contracts up front. New Cosmos-originated asset admissions should fail closed or mint fresh wrappers whenever the destination token address is already tied to an ETH-originated reserve object.
  • Split asset discovered, metadata accepted, wrapper deployed, and asset may redeem existing reserve into separate lifecycle states with stronger evidence and tighter authority at each step.
  • Make identity-table writes monotonic and collision-resistant: no aliasing through path formatting, embedded addresses, wrapper/native ambiguity, or later governance/tooling rewrites that can shadow an existing reserve mapping.
  • Bind outgoing batch construction and Ethereum release logic to the same canonical identity object used at admission time, and add route-local reconciliation so a fake Cosmos-side denom cannot quietly drain the reserve bucket of a real custody asset.
  • Put low initial caps, rate limits, and easy pause authority under newly admitted or newly remapped asset identities so one poisoned registration cannot immediately command full USDC/USDT/WETH/PAXG release authority.
  • Log and alert on every denom-to-token mapping mutation, especially any event where a previously unseen Cosmos-side asset would newly touch an established Ethereum custody contract.

Audit prompts

  • What exact tuple defines canonical asset identity on this bridge route, and which parts of that tuple can a user influence through denom strings, IBC paths, metadata, or token-deployment helpers?
  • Can any permissionless registration path bind a previously unseen Cosmos-side asset to an already-existing Ethereum custody token instead of forcing creation of a fresh isolated wrapper?
  • Are name, symbol, decimals, or IBC metadata ever allowed to do security work that should have required stronger origin-authenticity proof?
  • Which code path proves that a proposed denom -> ERC20 mapping is not colliding with an ETH-originated reserve object already backing live withdrawals?
  • If an attacker creates a fake source-side asset that looks operationally identical to USDC or WETH, where is the transition from looks similar to may redeem this reserve explicitly denied?
  • Do later MsgBatchSendToEthClaim withdrawals consume one immutable canonical identity object, or do they trust whatever answer the mutable registry currently returns?
  • What governance, upgrade, or operator action can rewrite asset mappings today, and how quickly could that widen rights over an existing reserve before operators notice?
  • If one bad registry entry lands, what per-route cap, pause, withdrawal delay, or downstream reconciliation job stops the next thousand withdrawals from looking ordinary?

Lessons that should already be standard

  • Asset registries are bridge verifiers with friendlier names. If they answer the wrong what does this asset redeem? question, honest validator signatures only automate the theft.
  • Permissionless listing stops being a product feature the moment it can bind new source-side assets to established destination reserves.
  • Metadata similarity is presentation, not provenance. A bridge that lets name/symbol/decimals look right stand in for canonical origin identity is asking users’ UX fields to carry custody authority.
  • The dangerous state transition is new asset observed to existing premium reserve now redeemable. Safe bridges keep those two states far apart and make the upgrade path explicit.
  • Monitoring by symbol or aggregate token bucket is too coarse. Operators need per-origin, per-route backing views or poisoned mappings will hide inside apparently normal USDC, USDT, WETH, or PAXG totals.
  • Bounded release matters for identity failures too. Even after a registry goes bad, the bridge should not let one false mapping immediately spend deep live custody as if nothing unusual happened.

Internal linkages

Control surface