ZetaChain Gateway Hack Analysis
Overview
On April 26, 2026, ZetaChain (@zetachain), a universal cross-chain L1 that bridges EVM chains, Bitcoin, Solana, Sui, and TON, suffered an exploit on its GatewayEVM cross-chain bridge contract. The drain window ran from roughly 12:51 to 23:00 UTC, and ZetaChain disclosed the incident publicly on April 27, 2026. The attacker drained approximately ~$318K of stablecoins (USDC and USDT) across four destination chains (Ethereum, BSC, Base, Arbitrum) from three victim wallets in 9 cross-chain calls. After DEX swaps to ETH and bridging back to Ethereum, the attacker consolidated 139.01439 ETH (~$318,977 USD) into a profits wallet 0x67107480… at Apr 27 14:08:35 UTC via tx 0x34c996cb…. ZetaChain paused mainnet cross-chain transactions and stated that no end-user funds were impacted.
The attacker exploited a missing access control bug in GatewayZEVM.call() on ZetaChain combined with an unrestricted arbitrary call sink in GatewayEVM.execute() on Ethereum. The protocol’s TSS validators co-sign destination-chain transactions based on events emitted on ZetaChain. By controlling the source event payload, the attacker caused the TSS to broadcast IERC20.transferFrom(victim, attacker, amount) calls signed by the protocol itself.
Smart Contract Hack Overview
- Attacker EOA (controls both Ethereum and ZetaChain):
0x00467f5921f1a343b96b9bf71ae7e9054ae72ea4 - Currently stolen funds parked at:
0x67107480FF880A876b7aA0C6CDc3ad92dC4a998a - Exploit Contract (ZetaChain):
0xd9dbEec028C12D2dA09a05C9d26709c0Ec722BC1 - GatewayZEVM Proxy (ZetaChain):
0xfEDD7A6e3Ef1cC470fbfbF955a22D793dDC0F44E, implementation0xEf195bf421658EA31dbA4Cbf598aF7f9D8791202 - GatewayEVM Proxy (Ethereum):
0x48B9AACC350b20147001f88821d31731Ba4C30ed, implementation0x1FfF55ccf855212f6b5530c468b44f9a5246572E - TSS Hot Signer (Ethereum):
0x70e967acFcC17c3941E87562161406d41676FD83 - Victim #1 (primary, drained on 4 chains):
0x8F1AA4DC8EFDB3FAA5060179E90EC3572FF86E38 - Victim #2 (Ethereum USDC):
0x0cE7f583F4B8a9Dc6942F8b61BE60b7B9dAc9832 - Victim #3 (BSC USDT):
0x7c9e7bb0e823fe08251064420116096b418d7060 - Cross-chain Drain Matrix:The 9 drains were grouped by destination chain. Each
Calledevent on ZetaChain produced a corresponding TSS-signedexecute()transaction on the destination. - Ethereum Txns (3 drains, ~$53,065 total)
- 41,018.20 USDC from Victim #1, ZetaChain tx
0x49d487…, Ethereum tx0x40bcbd60…(Apr 26 18:11:35 UTC) - 9,849.91 USDT from Victim #1, ZetaChain tx
0xdaa19f99…, Ethereum tx0x81fc9b24…(Apr 26 20:13:59 UTC) - 2,198.26 USDC from Victim #2, ZetaChain tx
0xead5e1…, Ethereum tx0xc24f3068…(Apr 26 23:00:35 UTC)
- 41,018.20 USDC from Victim #1, ZetaChain tx
- BSC Txns (3 drains, ~$70,547 total)
- Base Txns (1 drain, ~$110,341 total)
- ~110,341 USDC from Victim #1, ZetaChain tx
0x202581…
- ~110,341 USDC from Victim #1, ZetaChain tx
- Arbitrum Txns (2 drains, ~$99,672 total)
Total: ~$333,625 across 4 destination chains, 5 stablecoins, 3 victim wallets.
Final consolidation tx 0x34c996cb…dbc70a9 at Apr 27 14:08:35 UTC moved 139.01439105 ETH (~$318,977 USD) from the attacker EOA to the profits wallet 0x67107480…. The roughly $15K gap between the $333K drained at face value and the $319K netted in ETH is probably DEX slippage and bridging fees.
Decoding the Smart Contract Vulnerability
- The root cause is a chain of three independent defects across the source-side
GatewayZEVMcontract on ZetaChain, the destination-sideGatewayEVMcontract on Ethereum, and a pre-existing trust assumption about ERC20 approvals. Removing any one of the three would have prevented the attack. - Defect 1:
GatewayZEVM.call()is unauthenticated: Vulnerablecall()inGatewayZEVM.sol.
function call(
bytes memory receiver,
address zrc20,
bytes calldata message,
CallOptions calldata callOptions,
RevertOptions calldata revertOptions
)
external
whenNotPaused
{
if (callOptions.gasLimit < MIN_GAS_LIMIT) revert InsufficientGasLimit();
if (message.length + revertOptions.revertMessage.length > MAX_MESSAGE_SIZE)
revert MessageSizeExceeded();
_call(receiver, zrc20, message, callOptions, revertOptions);
}
GatewayZEVM.call()isexternal whenNotPausedwith noonlyRole, noonlyProtocol, and no msg.sender allow-list. Any account, including a freshly deployed exploit contract, can invoke it. The validation is shape-only: a minimum gas limit and a maximum message size. There is no constraint onreceiver(which becomes the destination contract on the remote chain), no constraint onmessage(which becomes raw calldata forwarded by the gateway), andCallOptions.IsArbitraryCallis taken straight from the caller. After charging a small ZRC20 gas fee,_call(...)emits aCalled(msg.sender, zrc20, receiver, message, callOptions, revertOptions)event that ZetaChain’s relayer treats as a legitimate cross-chain message.- Defect 2: Arbitrary call branch in
GatewayEVM.execute(): Vulnerableexecute()inGatewayEVM.sol.
/// @notice Executes a call to a destination address without ERC20 tokens.
/// @dev This function can only be called by the TSS address and it is payable.
function execute(
MessageContext calldata messageContext,
address destination,
bytes calldata data
)
external
payable
nonReentrant
onlyRole(TSS_ROLE)
whenNotPaused
returns (bytes memory)
{
if (destination == address(0)) revert ZeroAddress();
bytes memory result;
// if sender is provided in messageContext call is authenticated and target is Callable.onCall
// otherwise, call is arbitrary
if (messageContext.sender == address(0)) {
result = _executeArbitraryCall(destination, data);
} else {
result = _executeAuthenticatedCall(messageContext, destination, data);
}
emit Executed(destination, msg.value, data);
return result;
}
GatewayEVM.execute()on the destination chain branches onmessageContext.sender == address(0). When the attacker setsIsArbitraryCall = trueon the source side, the relayer constructsmessageContext.sender = address(0)for the destination call, which routes execution into the_executeArbitraryCallbranch. TheonlyRole(TSS_ROLE)check protects the function from unauthorized callers, but the TSS itself is the caller, because it just legitimately signed the attacker’s CCTX._executeArbitraryCall(), the arbitrary external call sink.
/// @dev Private function to execute an arbitrary call to a destination address.
function _executeArbitraryCall(
address destination,
bytes calldata data
) private returns (bytes memory) {
_revertIfOnCallOrOnRevert(data);
(bool success, bytes memory result) = destination.call{ value: msg.value }(data);
if (!success) revert ExecutionFailed();
return result;
}
- The only filter on the arbitrary path is
_revertIfOnCallOrOnRevert(data), which is a deny-list that blocks only theonCallandonRevert4-byte function selectors. It does not block ERC20 selectors such astransferFrom (0x23b872dd),approve (0x095ea7b3),transfer (0xa9059cbb), orpermit (0xd505accf). Oncedestination.call(data)runs against a token contract with attacker-supplied calldata, any external account that previously granted an allowance toGatewayEVMbecomes drainable. - Defect 3: Outstanding ERC20 approvals to
GatewayEVM: Defect 3 is operational rather than code: ZetaChain’s internal team wallets had outstanding ERC20 allowances toGatewayEVM(a normal precondition for cross-chain deposits). Without those allowances,transferFrom(victim, attacker, amount)would have reverted onallowance == 0. The bug therefore converts an “arbitrary external call from the TSS” primitive into a free ERC20 drain against precisely those addresses that had previously approved the gateway.
Attack Sequence

Fig 1. Attack flow across ZetaChain and the destination EVM chains.
- The attacker deployed an exploit contract on ZetaChain at
0xd9dbEec0…22BC1. For each target ERC20 token T, the contract paid a small ZRC20 gas fee, encoded the calldatatransferFrom(victim, attacker_eoa, amount)(selector0x23b872dd), and calledGatewayZEVM.call(receiver=T, zrc20=ZRC20_<chain>, message=<transferFrom calldata>, callOptions={IsArbitraryCall: true, ...}, revertOptions={...}). One such event is captured in ZetaChain transaction0xdaa19f99…ddc4521. - ZetaChain’s observer (zetaclient) parsed the
Calledevent, voted it into a CCTX withIsArbitraryCall=truepropagated verbatim, and the TSS validator network produced a signature overGatewayEVM.execute(messageContext={sender: address(0), ...}, destination=T, data=<transferFrom calldata>). - The TSS hot signer
0x70e967ac…6FD83broadcast the signed transaction to Ethereum.GatewayEVM.execute()passedonlyRole(TSS_ROLE), branched into_executeArbitraryCall(T, data), the_revertIfOnCallOrOnRevertfilter let thetransferFromselector through, andT.call(transferFrom(victim, attacker, amount))succeeded because the victim wallet’s allowance to the gateway was already in place. - The drain repeated 9 times across Ethereum, BSC, Base, and Arbitrum. Total gross drain was roughly $333K in stablecoins.
- ZetaChain detected the attack, blocked the vector, and suspended cross-chain transactions on mainnet as a precaution. They also issued a public alert advising every user who had ever interacted with
GatewayEVMon any EVM chain. As a precaution for users, its recommended to revoke the token approvals. The official ZetaChain statement can be found here.

Mitigation and Best Practices
- Add caller authentication so that the resulting
messageContext.senderon the destination chain reflects the actual zEVMmsg.sender(or a strict hash thereof), eliminating the attacker-controlledIsArbitraryCall=truepath entirely. The “arbitrary call” mode should only be reachable from protocol-internal callers (onlyProtocol), never from external accounts. - In
_executeArbitraryCall, permit onlyCallable.onCall.selectorandRevertable.onRevert.selector. The cleaner fix is to remove the arbitrary call branch entirely and require all destination calls to go through_executeAuthenticatedCall, which expects the destination to implement theCallable.onCallinterface. - Never grant indefinite max allowances to bridge contracts from privileged or treasury wallets.
- Use permit-based flows where possible, set narrow allowance windows, and monitor
Approvalevents on bridge contracts as a tripwire. - Users who have ever interacted with
GatewayEVMon any EVM chain (Ethereum, Arbitrum, Base, BNB, Polygon, Avalanche) should revoke their approvals using a tool like Revoke.cash. - Add real-time monitoring for cross-chain calls where
messageContext.sender == address(0)anddestinationis a known ERC20 contract. This pattern is a high-confidence exploit fingerprint and can trigger an emergency pause via the protocol’s pauser role. - To prevent such vulnerabilities, the best Smart Contract auditors must examine the Smart Contracts for logical issues. We at CredShields provide smart contract security and end-to-end security of web applications and externally exposed networks. Our public audit reports can be found on https://github.com/Credshields/audit-reports. Schedule a call at https://credshields.com/
- Scan your Solidity contracts against the latest common security vulnerabilities with 494+ detections at SolidityScan.

Fig: SolidityScan — Smart Contract Vulnerability Scanner
Conclusion:
SolidityScan is an advanced smart contract scanning tool that discovers vulnerabilities and reduces risks in code. Request a security audit with us, and we will help you secure your smart contracts. Signup for a free trial at https://solidityscan.com/signup
Follow us on our Social Media for Web3 security-related updates.