We do not have a reliable USD price for the recorded assets yet.
0x47ddb6a433b76117a98fbeab5320d8b67d468e31EthereumAn unprivileged Ethereum account drained the XBridge proxy's pre-existing STC inventory by using the bridge's public token-listing flow and then its public withdrawal flow. In block 19723701, the attacker paid the standard 0.15 ETH listing fee to register STC through the victim proxy; in block 19723706, the attacker used that registration to withdraw all 482589886903032631 raw STC units already held by proxy 0x47ddb6a433b76117a98fbeab5320d8b67d468e31.
The root cause was authorization design in XBridge itself. Victim listToken recorded the lister as the token's authorized withdrawer, and victim withdrawTokens later accepted that same registration-derived authorization to release proxy-held tokens, without requiring a corresponding depositor balance or token ownership proof.
XBridge's exploited asset holder was proxy 0x47ddb6a433b76117a98fbeab5320d8b67d468e31. The observed exploit traces label the relevant victim-side entrypoints as XBridge::listToken(tokenInfo,tokenInfo,bool), XBridge::tokenTax(address), and XBridge::withdrawTokens(address,address,uint256).
The original on-chain exploit used helper contracts 0x445d2656e557e19800b2a3b9be547db56ed3c8d4 and 0x899266243fd2b9a0426b58bd6d534c6b813ef27a, both deployed by attacker EOA 0x0cfc28d16d07219249c6d6d6ae24e7132ee4caa7. Auditor disassembly notes show the helper service is owner-gated and delegates into attacker implementation code, so those contracts were attacker-side wrappers, not victim protocol components.
Victim implementation 0x354cca2f55dde182d36fe34d673430e226a3cb8c was analyzed through disassembly. That analysis is the key validator-facing evidence because it ties the exploit to concrete victim storage writes, authorization checks, and transfer logic rather than to attacker wrapper behavior.
The exploit was an authorization failure in XBridge's token registration and withdrawal design. Victim listToken did more than publish metadata: it wrote the caller into a per-token authorization slot rooted at keccak256(token, 0xb1). Later, victim withdrawTokens loaded that same slot, required only that it matched CALLER, checked the token was not the bridge's wrapped-token case, and bounded the requested amount only by the proxy's live token balance.
That means token listing implicitly granted withdrawal authority over whatever balance the proxy already held for that token. No deposit-specific accounting or escrow ownership check existed between the lister and the drained inventory. Once STC was listed through the public fee path, the attacker became the authorized withdrawer for STC and could transfer out the proxy's entire pre-existing STC balance.
The exploit remained ACT after the attacker wrappers were stripped away. A fresh attacker can call the victim proxy directly, pay the standard listing fee, and invoke the same victim-side withdrawal path against public on-chain state.
Victim implementation notes tie the exploit to exact code regions:
Victim implementation 0x354cca... disassembly notes
- listToken selector 0x7c242a0f dispatches to body 0x17bc.
- PCs 0x1b26..0x1b3f compute keccak256(baseToken, 0xb1) and store CALLER.
- PCs 0x1bb7..0x1bd8 compute keccak256(correspondingToken, 0xb1) and store CALLER.
- PCs 0x1b92..0x1baa read fee-recipient slot 0x9a and forward CALLVALUE.
- withdrawTokens selector 0x5e35359e dispatches to body 0x0f64.
- PCs 0x0fcd..0x0fec compute keccak256(token, 0xb1), load the stored address, and compare it to CALLER.
- PCs 0x1009..0x1028 read balanceOf(address(this)).
- PCs 0x1065..0x106c require requestedAmount <= current proxy balance.
- PCs 0x1093..0x10c4 execute transfer(receiver, amount).
The listing transaction trace matches that victim code path. The attacker wrapper invoked victim listToken, the victim forwarded the 0.15 ETH fee, and TokenListed was emitted without any STC transfer into the proxy:
Exploit listing trace
XBridge::listToken{value: 150000000000000000}(...)
0x579ed0e3996e192Fcd64d85daEF7F985566DdE3E::fallback{value: 150000000000000000}()
emit TokenListed(baseToken: STC, baseTokenChain: 85936, correspondingToken: STC, correspondingTokenChain: 95838, isMintable: false, user: 0x899266...)
The withdrawal transaction trace then shows the victim reading protocol state and transferring the full proxy balance:
Exploit withdrawal trace
XBridge::tokenTax(STC) -> 0
SaitaChain::balanceOf(OwnedUpgradeabilityXBridgeProxy) -> 482589886903032631
XBridge::withdrawTokens(STC, 0x0cFC28..., 482589886903032631)
SaitaChain::transfer(0x0cFC28..., 482589886903032631)
emit TokenWithdrawn(user: 0x899266..., receiver: 0x0cFC28..., amount: 482589886903032631)
The invariant violation is precise: registration metadata was allowed to stand in for asset ownership. The victim code never linked STC withdrawal rights to a credited STC deposit, and therefore the proxy's unrelated pre-existing STC balance became withdrawable by any caller who first registered STC through the public fee path.
The adversary flow had two transactions.
First, tx 0xe09d350d8574ac1728ab5797e3aa46841f6c97239940db010943f23ad4acf7ae in block 19723701 called the attacker wrapper's deposit(address) entrypoint. That wrapper delegated into victim listToken, paid 0.15 ETH, and caused the victim to store caller-derived authorization for STC. No STC was deposited during this step.
Second, tx 0x903d88a92cbc0165a7f662305ac1bff97430dbcccaa0fe71e101e18aa9109c92 in block 19723706 called the wrapper's withdrawTokens(address,uint256) entrypoint. The wrapper resolved the victim proxy's full STC balance and invoked victim withdrawTokens, which transferred 482589886903032631 raw STC units from the proxy to attacker EOA 0x0cfc28d16d07219249c6d6d6ae24e7132ee4caa7.
The attacker wrappers matter only as orchestration. Their owner gate proves they were not the public exploit surface. The ACT surface was the victim proxy because the validator-side fork test reproduces the same result with a fresh attacker address calling the proxy directly.
The XBridge proxy lost its entire pre-existing STC balance on Ethereum mainnet. The measurable loss was:
482589886903032631 raw units, token decimals 9That corresponds to 482,589,886.903032631 STC. The loss was borne by the victim proxy inventory at 0x47ddb6a433b76117a98fbeab5320d8b67d468e31, and the drained balance was transferred to the attacker-controlled EOA.
0xe09d350d8574ac1728ab5797e3aa46841f6c97239940db010943f23ad4acf7ae0x903d88a92cbc0165a7f662305ac1bff97430dbcccaa0fe71e101e18aa9109c920x47ddb6a433b76117a98fbeab5320d8b67d468e31/workspace/session/artifacts/auditor/iter_2/victim_code_notes.md/workspace/session/artifacts/collector/seed/1/0x19ae49b9f38dd836317363839a5f6bfbfa7e319a/src/SaitaChain.sol0xa15d0280b91a239a448c0e58bd867aff4959461dc8cb676ffecd94d40d3fdeee0x33f19229b5382854e63b4d2d2d82164fcdc5b13a01505ea0d5d2df7e7574ab25