Share on XShare on LinkedInShare on Telegram
Hack Analysis

Gravity Bridge $5.4M Denom Mapping Exploit (Explained)

Gravity Bridge lost $5.4M after an attacker poisoned the denom-to-ERC20 registry using a fabricated cosmosDenom string, draining real USDC, USDT, WETH, and PAXG from custody.

Author
QuillAudits Team
June 3, 2026
Gravity Bridge $5.4M Denom Mapping Exploit (Explained)
Share on XShare on LinkedInShare on Telegram

On May 30, 2026, Gravity Bridge lost $5.4M across four assets USDC, USDT, WETH, and PAXG. The attacker minted worthless tokens on Osmosis, embedded real Ethereum custody token addresses inside fabricated denom strings, and used Gravity's permissionless deployERC20() function to corrupt the bridge's token registry. Once the registry mapped fake Cosmos-side balances to real Ethereum custody assets, withdrawing the real tokens was straightforward. No flash loan, no key compromise, no reentrancy. Just a crafted string that the bridge accepted without question, and a registry that trusted whatever it was told.

Hack Analysis

Prior to the attack, the attacker registered a validator on Gravity chain under julia666 (gravityvaloper1rdwckpx2p3mwu4k8xdz8vmd4x3mqx4v9ars8k) on May 29th, self-delegating 80 GRAV. The following transaction set up orchestrator address gravity1wy3cfsv4k5g0u8kxyfuyd9xm2r46l2djyu4nel linked to Ethereum key 0x91B52e07132a49DAD1B7a3939a51547f79468Ec7. This gave the attacker a registered presence in the Gravity validator set, satisfying the checkOrchestratorValidatorInSet requirement that gates claim submission, allowing their orchestrator to submit MsgERC20DeployedClaim messages alongside legitimate validators. On-chain evidence confirms this Ethereum key was not part of valset 2912 and carried no meaningful weight in either Cosmos attestation or Ethereum signature verification.

Screenshot 2026-06-03 at 4.49.01 PM.png

Screenshot 2026-06-03 at 4.48.23 PM.png

Screenshot 2026-06-03 at 4.49.27 PM.png

The attacker began on Osmosis chain, where the tokenfactory module allows any address to create and mint arbitrary tokens at zero cost. Using the address osmo1m9athjzah02f2mnrgtcke7e5ya3zpvw8lccuss, four fake tokens were created, one mirroring each real asset sitting in Gravity Bridge's Ethereum custody contract: USDC, USDT, WETH, and PAXG. These tokens held no value and represented nothing. Once minted, the attacker IBC-transferred all four to Gravity chain over channel-144 on the Osmosis side, received on Gravity over channel-10, where each was assigned a plain ibc/HASH denom identifier by the bridge.

Screenshot 2026-06-03 at 1.34.09 PM.png

Screenshot 2026-06-03 at 1.35.02 PM.png

Screenshot 2026-06-03 at 1.35.20 PM.png

Screenshot 2026-06-03 at 1.35.48 PM.png

denomination trace.png

With the fake tokens landed on Gravity chain, the attacker moved to Ethereum. The Gravity Bridge contract (0xa4108aA1Ec4967F8b52220a4f7e94A8201F2D906) has a permissionless  deployERC20() function that anyone can call to register a Cosmos-originated token on Ethereum. The attacker's Ethereum address  0x73E95aE5f3b87e02D4547AFE86d0d466e9450d6B  called this function four times, once per token, passing a fabricated _cosmosDenom argument each time. Rather than passing the plain ibc/HASH that Gravity had assigned to each fake token, the attacker embedded the real Ethereum contract address of the corresponding custody token as a path segment inside the denom string. The USDC call, for example, passed:

1ibc/C92D312D79D9C44B6C6F94AF40FFCB30A334D87F08952D6ED9904E3E83A9F50C/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/transfer/channel-10/factory/osmo1m9athjzah02f2mnrgtcke7e5ya3zpvw8lccuss
2

The function deployed a fresh CosmosERC20 wrapper contract at 0x9a280B869A73b6fF812a599E40ABd8dbdFB738c2 and emitted ERC20DeployedEvent carrying the fabricated denom string verbatim. No validation was performed on what _cosmosDenom  contained.

Screenshot 2026-06-03 at 1.40.12 PM.png

Screenshot 2026-06-03 at 1.40.42 PM.png

Screenshot 2026-06-03 at 1.40.23 PM.png

Gravity validators watching Ethereum observed the event and submitted  MsgERC20DeployedClaim acknowledgement transactions to Gravity chain. handleErc20Deployed processed them and updated the denom-to-ERC20 registry. This is where the damage was done. Instead of mapping ibc/C92D... to the newly deployed wrapper 0x9a280B, the registry was written with the real USDC contract address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 as the value. The same poisoning was applied for USDT (0xdAC17F958D2ee523a2206206994597C13D831ec7), WETH (0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2), and PAXG (0x45804880De22913dAFE09f4980848ECE6EcbAf78).

Screenshot 2026-06-03 at 1.42.23 PM.png

With the registry now equating worthless fake tokens to real custody assets, the attacker submitted MsgBatchSendToEthClaim withdrawal transactions. DenomToERC20Lookup hit the poisoned entries and returned the real token addresses. Gravity produced outgoing batches 41572 through 41575, validators signed them, and relayers submitted them to the custody contract on Ethereum. The contract verified the signatures and called safeTransfer() for each asset to the attacker's wallet 0x7B582033061b96cC3F9421e73a749ED7C62da1F9, releasing 4,349,701 USDC, 434,072 USDT, 274.34 WETH, and 14.16 PAXG. Total damage: $5.4M.

Screenshot 2026-06-03 at 1.49.04 PM.png

Screenshot 2026-06-03 at 1.51.11 PM.png

Screenshot 2026-06-03 at 1.51.21 PM.pngScreenshot 2026-06-03 at 1.50.59 PM.png

Screenshot 2026-06-03 at 1.49.35 PM.png

Root Cause

The root cause is a combination of two failures one in the Ethereum contract and one in the Gravity chain module that together allowed an attacker-controlled denom string to corrupt the bridge's token registry.

The first failure is in deployERC20() on Gravity.sol. This function is permissionless by design, intended to let anyone bootstrap an ERC20 representation for a Cosmos-originated token on Ethereum. It accepts _cosmosDenom as a plain string parameter and emits it verbatim in ERC20DeployedEvent with zero validation on its contents:

1function deployERC20(
2    string calldata _cosmosDenom,
3    string calldata _name,
4    string calldata _symbol,
5    uint8 _decimals
6) external {
7    CosmosERC20 erc20 = new CosmosERC20(address(this), _name, _symbol, _decimals);
8    state_lastEventNonce = state_lastEventNonce + 1;
9    emit ERC20DeployedEvent(
10        _cosmosDenom,      // passed verbatim, no validation
11        address(erc20),
12        _name,
13        _symbol,
14        _decimals,
15        state_lastEventNonce
16    );
17}
18

A legitimate call would pass only the plain ibc/HASH 66 characters total. The attacker passed a fabricated multi-segment string that embedded the real USDC contract address 0xA0b86991... as a path component. The contract had no mechanism to reject it.

The second failure is in handleErc20Deployed on Gravity chain. This function processes accepted MsgERC20DeployedClaim messages and writes the denom-to-ERC20 registry mapping. The only check performed on claim.TokenContract is a format validation confirming it is a structurally valid Ethereum address:

1tokenAddress, err := types.NewEthAddress(claim.TokenContract)
2if err != nil {
3    return errorsmod.Wrap(err, "invalid token contract on claim")
4}
5

There is no check that claim.TokenContract is the contract actually deployed in the corresponding Ethereum event. There is no check that claim.TokenContract is not already a known ETH-originated custody token held in the bridge. The function that would have caught this ERC20ToDenomLookup exists in the same file and is called in handleSendToCosmos directly above, but was never called in handleErc20Deployed:

The on-chain evidence confirms the outcome of these two failures. The four MsgERC20DeployedClaim transactions on Gravity chain  786C..E49,  9DF3..A9D3881..499, and 14BC..A37 show token_contract set to the real custody token addresses rather than the freshly deployed wrapper. The mechanism by which the fabricated  _cosmosDenom string caused validators orchestrators to submit the real custody token address as token_contract rather than the wrapper address from topics[1] of the Ethereum event could not be fully determined from the available codebase and remains subject to a full postmortem from the Gravity Bridge team. What is confirmed is that deployERC20()  accepted the fabricated input without restriction, and handleErc20Deployed accepted and registered whatever token_contract value arrived in the claim without cross-checking it against the existing custody token registry.

Funds Flow After Attack

The attacker swapped the stolen assets into native ETH and has been continuously attempting to launder the funds through Tornado Cash via address 0xc8c71ae4261e55a66d9967f2ac252be4e669f562.

Screenshot 2026-06-02 at 5.25.21 PM.png

At the time of writing, however, the majority of the funds remain held in the attacker's EOA, 0x4d3ca32e687e871a58b78AcAc73bE59AC37C7A47.

Screenshot 2026-06-02 at 5.26.15 PM.png

Post-Attack Mitigation

The bridge was paused after the team acknowledged the incident.

Relevant Address and Transactions

Attacker EOAs

Attacker Gravity Chain Address

gravity1wy3cfsv4k5g0u8kxyfuyd9xm2r46l2djyu4nel

gravity1rdwckpx2p3mwu4k8xdz8vmd4x3mqx4v9kk6wdz

Asset Drain Transactions:

Gravity Bridge Ethereum Contract: 0xa4108aa1ec4967f8b52220a4f7e94a8201f2d906

Osmosis Address: osmo1m9athjzah02f2mnrgtcke7e5ya3zpvw8lccuss

Create Validator Transaction: F6AA34E8D01C55A5F1E38310E816DBB4BEE25B2FDB29ACD7ED1E25352BA62009

Deploy ERC20 Ethereum Transactions:

MsgBatchSendToEthClaim Transaction:

MsgERC20DeployedClaim Transactions (confirming the token mapping mismatch):

USDC: 786C41F0B9DA6EB8F59A1BC3299504AE47EFC4BBAB13673E391E307DB1BCEE49

USDT: 9DF358F96E2BDE91FA96D1CC1CBEBE42E5CA2DD2F5745CD2ADA2591751101A9D

WETH: 388112D6C4BEDD74D34157AEC689195B942A8EDC4F476C4230C638CDF399E499

PAXG: 14BC7A377F959973613473A5EFB623D4011AED86BE00D68B782DB6D6720F9A37

Conclusion

Gravity Bridge's $5.4M loss came down to one missing validation the bridge never checked whether a token being registered already existed as a real custody asset. A permissionless function accepted arbitrary input, validators attested to what they observed, and the registry wrote whatever arrived. Cross-chain bridges carry the heaviest trust assumptions in DeFi. Every input crossing a chain boundary is untrusted data and must be treated as such. The fix here was a single lookup that already existed in the codebase it just was never called in the right place.

Contents

Tell Us About Your Project
Subscribe to Newsletter
hashing bits image
Loading...
cta-bg

WE SECURE EVERYTHING YOU BUILD.

From day-zero risk mapping to exchange-ready audits — QuillAudits helps projects grow with confidence. Smart contracts, dApps, infrastructure, compliance — secured end-to-end.

QuillAudits Logo


ISO 27001
DeFi Security AllianceplumeUniswap FoundationAethiropt-collectivePolygon SPNBNB Chain Kickstart

All Rights Reserved. © 2026. QuillAudits - LLC