HACK ANALYSIS 9 min read

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

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 GatewayZEVM contract on ZetaChain, the destination-side GatewayEVM contract 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: Vulnerable call() in GatewayZEVM.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() is external whenNotPaused with no onlyRole, no onlyProtocol, 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 on receiver (which becomes the destination contract on the remote chain), no constraint on message (which becomes raw calldata forwarded by the gateway), and CallOptions.IsArbitraryCall is taken straight from the caller. After charging a small ZRC20 gas fee, _call(...) emits a Called(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(): Vulnerable execute() in GatewayEVM.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 on messageContext.sender == address(0). When the attacker sets IsArbitraryCall = true on the source side, the relayer constructs messageContext.sender = address(0) for the destination call, which routes execution into the _executeArbitraryCall branch. The onlyRole(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 the onCall and onRevert 4-byte function selectors. It does not block ERC20 selectors such as transferFrom (0x23b872dd), approve (0x095ea7b3), transfer (0xa9059cbb), or permit (0xd505accf). Once destination.call(data) runs against a token contract with attacker-supplied calldata, any external account that previously granted an allowance to GatewayEVM becomes drainable.
  • Defect 3: Outstanding ERC20 approvals to GatewayEVM: Defect 3 is operational rather than code: ZetaChain’s internal team wallets had outstanding ERC20 allowances to GatewayEVM (a normal precondition for cross-chain deposits). Without those allowances, transferFrom(victim, attacker, amount) would have reverted on allowance == 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.

  1. 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 calldata transferFrom(victim, attacker_eoa, amount) (selector 0x23b872dd), and called GatewayZEVM.call(receiver=T, zrc20=ZRC20_<chain>, message=<transferFrom calldata>, callOptions={IsArbitraryCall: true, ...}, revertOptions={...}). One such event is captured in ZetaChain transaction 0xdaa19f99…ddc4521.
  2. ZetaChain’s observer (zetaclient) parsed the Called event, voted it into a CCTX with IsArbitraryCall=true propagated verbatim, and the TSS validator network produced a signature over GatewayEVM.execute(messageContext={sender: address(0), ...}, destination=T, data=<transferFrom calldata>).
  3. The TSS hot signer 0x70e967ac…6FD83 broadcast the signed transaction to Ethereum. GatewayEVM.execute() passed onlyRole(TSS_ROLE), branched into _executeArbitraryCall(T, data), the _revertIfOnCallOrOnRevert filter let the transferFrom selector through, and T.call(transferFrom(victim, attacker, amount)) succeeded because the victim wallet’s allowance to the gateway was already in place.
  4. The drain repeated 9 times across Ethereum, BSC, Base, and Arbitrum. Total gross drain was roughly $333K in stablecoins.
  5. 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 GatewayEVM on 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.sender on the destination chain reflects the actual zEVM msg.sender (or a strict hash thereof), eliminating the attacker-controlled IsArbitraryCall=true path entirely. The “arbitrary call” mode should only be reachable from protocol-internal callers (onlyProtocol), never from external accounts.
  • In _executeArbitraryCall, permit only Callable.onCall.selector and Revertable.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 the Callable.onCall interface.
  • 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 Approval events on bridge contracts as a tripwire.
  • Users who have ever interacted with GatewayEVM on 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) and destination is 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.

SolidityScan — LinkedIn | Twitter | Telegram | Discord