ZetaChain Cross-Chain Call Security Incident Analysis: How a GatewayZEVM Validation Flaw Amplified Approved Asset Risk

ZetaChain exposed a critical validation gap during a cross-chain call incident: an attacker could craft a malicious message, have it signed by TSS, and trigger arbitrary contract execution on the destination chain, ultimately transferring user assets that had previously been approved to the Gateway. This article breaks down the attack path, core code paths, and the practical risk boundary. Keywords: ZetaChain, cross-chain bridge security, smart contract vulnerability.

Technical Specifications Snapshot

Parameter Details
Project ZetaChain
Architecture Type Layer 1 with native cross-chain custody and message passing
Relevant Language Solidity
Key Protocols / Mechanisms PoS, TSS, MPC, EVM cross-chain calls
Involved Contracts GatewayZEVM, GatewayEVM, ERC20Custody
Target Chain Ethereum (verified in this analysis)
Core Dependencies TSS threshold signing, ZetaClient, CrossChain module
GitHub Stars Not provided in the source material
Reference Source Code protocol-contracts-evm/GatewayZEVM.sol

This incident was fundamentally caused by the combination of arbitrary cross-chain call capability and lingering historical approvals

ZetaChain’s design allows users to initiate cross-chain messages from ZEVM. Validators then read the CCTX, complete threshold signing through TSS, and broadcast the destination transaction to an external chain for execution. This model emphasizes general-purpose interoperability, but it also requires extremely strict input validation at the entry point.

The core issue in this incident was not a compromised TSS key, nor deliberate validator misconduct. Instead, the source-chain side imposed overly weak restrictions on cross-chain message contents. An attacker only needed to submit a message that was syntactically valid but semantically malicious to invoke a target contract on Ethereum through the protocol’s official cross-chain execution path.

The attack flow can be reconstructed as a clear cross-chain execution chain

  1. The attacker calls GatewayZEVM.call() on ZetaChain.
  2. receiver is set to the USDT contract address on Ethereum.
  3. message is crafted as the calldata for transferFrom().
  4. Validators read the CCTX and complete threshold signing with TSS.
  5. ZetaClient broadcasts the signed transaction to Ethereum.
  6. On Ethereum, GatewayEVM.execute() executes the call directly and transfers assets from users who had previously granted approval.
// Pseudocode: illustration of the attack path
GatewayZEVM.call(
    receiver,   // USDT contract address on the target chain
    message,    // Carefully crafted transferFrom calldata
    revertOpt   // Revert parameters
);

The key point in this call path is that the attacker does not need to control the victim account. The victim only needs to have historically approved an allowance to the Gateway or a related execution address.

On-chain traces are already sufficient to prove the source-chain construction and destination-chain execution model

The source material provides two key transactions: one on ZetaChain where the malicious request was initiated, and one on Ethereum where the execution occurred. The first shows how the malicious cross-chain request was created, and the second shows the resulting asset transfer.

image AI Visual Insight: This image shows the ZetaChain-side call details for GatewayZEVM.call(). The critical detail is that receiver is set to the USDT contract on the destination chain, while message carries encoded function call data. This demonstrates that the cross-chain message is not restricted to a fixed template and can carry arbitrary calldata, which significantly expands the attack surface.

From a security analysis perspective, the most important evidence in this type of screenshot is not simply that the call succeeded, but that the call parameters were arbitrary. Once a protocol fails to constrain whitelisted contracts, function selectors, or call semantics, the cross-chain gateway can degrade into a general-purpose execution proxy.

image AI Visual Insight: This image shows the execution result of the destination-chain transaction on Ethereum, where USDT was moved out of the target address via transferFrom. Technically, this means the on-chain transaction sent by TSS on behalf of the gateway was recognized by the token contract as a legitimate caller with allowance-spending authority.

This means the attack surface covered every account that had ever approved the gateway

If users had previously granted ERC-20 allowances to GatewayEVM, a custody contract, or its effective execution address, an attacker could consume that allowance through malicious calldata. The risk was not limited to a single victim. It applied to the full set of previously approved accounts.

Insufficient input validation in GatewayZEVM was the direct root cause of this incident

Although the deployed browser-visible code was not open sourced, the original material already mapped the issue to GatewayZEVM.sol on GitHub. The analysis shows that the call() function itself does not enforce access control, meaning any user can initiate a cross-chain call. That alone is not a vulnerability. The problem is that the surrounding constraints were too weak.

Before emitting the relevant event, call() mainly performs two categories of checks: validateCallAndRevertOptions and validateReceiver. The former only validates gas limits and message size. The latter only validates whether receiver is a contract address.

image AI Visual Insight: This image highlights the logic around GatewayZEVM.call(). The real issue is not the function’s visibility, but the fact that its security boundary for downstream execution messages is too narrow: the system accepts any caller, any target contract, and any function data, while performing only structural validation rather than business-semantic validation.

image AI Visual Insight: This image shows that validateCallAndRevertOptions focuses on gas and message length, which suggests the developers were more concerned with execution feasibility and resource usage than whether the call content represented unauthorized behavior. This pattern of validating shape rather than intent is common in cross-chain message systems.

image AI Visual Insight: This image shows that validateReceiver only requires receiver to be a contract address. It does not establish a target contract allowlist or restrict function selectors, asset types, or approval relationships. As a result, any ERC-20 contract could become an attack target.

function validateReceiver(address receiver) internal view {
    // Only verify that the destination is a contract address
    if (receiver.code.length == 0) {
        revert InvalidReceiver();
    }
}

This kind of validation can only prevent messages from being sent to an EOA. It cannot stop a malicious call from targeting a token contract capable of processing transferFrom.

Ethereum GatewayEVM executed the cross-chain message directly, creating the final dangerous closure

On the destination chain, GatewayEVM.execute() is restricted by TSS_ROLE, which means a normal attacker cannot call this entry point directly on Ethereum. However, the attacker does not need direct access. They only need to craft a message on the source chain that validators will accept.

More importantly, once execution enters _executeArbitraryCall(), the system directly calls the target contract with the provided calldata. In other words, as long as the message is treated as a legitimate cross-chain result, the destination chain faithfully executes its semantics.

image AI Visual Insight: This image shows the permission boundary of execute(): externally, only TSS can call it, which means the destination-chain entry point itself is not an exposed bug surface. But this also proves the inverse: once source-chain message review fails, TSS authority automatically elevates a malicious request into a trusted transaction.

image AI Visual Insight: This image shows that _executeArbitraryCall() directly executes the destination address and calldata supplied through the cross-chain message. In practice, this makes it a general-purpose call proxy driven by cross-chain messages. Without function-level policy controls, it naturally amplifies risks around allowances, approve, and custodied assets.

function _executeArbitraryCall(address target, bytes memory data) internal {
    // Directly call the target contract
    (bool ok, ) = target.call(data);
    require(ok, "call failed"); // Revert the entire execution if the call fails
}

This pseudocode captures the essence of the issue: the destination chain does not apply a second layer of constraints to determine whether the call should actually be executed.

The remediation direction must shift from executable arbitrary messages to provable safe intent

First, source-chain call() should not validate only format. It should also validate semantics. At a minimum, it should restrict target contract allowlists, function selector allowlists, and call policies for sensitive token contracts.

Second, the destination-chain execution layer should add a policy gateway rather than unconditionally using target.call(data). Practical controls include per-application capabilities, function-level ACLs, allowance risk detection, and default denial for high-risk selectors such as transferFrom, approve, and permit.

A minimal defensive policy could be designed like this

function isAllowed(address target, bytes4 selector) internal view returns (bool) {
    // Only allow whitelisted targets and whitelisted functions
    return whitelistTarget[target] && whitelistSelector[selector];
}

This kind of policy does not cover every complex scenario, but it is sufficient to block the attack path shown in this article: direct calls to an ERC-20 contract using malicious calldata.

The conclusion is that cross-chain protocols cannot build generality on top of unconstrained execution

This ZetaChain incident exposed a classic cross-chain design failure: the source chain did not constrain message semantics, the destination chain treated signed messages as trusted execution instructions, and historical approvals ultimately became attack fuel. The exploitation threshold was not especially high, but the blast radius was extremely broad.

For developers, the most important lesson is this: a cross-chain message is not just a data packet. It is remote execution authority. As long as a system supports arbitrary calls, it must treat allowlists, function policies, approval isolation, and rollback governance as first-class security controls.

FAQ

1. Why was the attacker able to transfer someone else’s USDT?

Because the target user may have previously granted an allowance to the Gateway or its execution address. After the attacker constructed transferFrom calldata, the gateway executed it on Ethereum, and the token contract treated it as a legitimate allowance-spending call.

2. Was this caused by a leaked TSS private key?

No. The available evidence more strongly supports the conclusion that the protocol executed a malicious but structurally valid cross-chain message exactly as designed. In this incident, TSS acted as a trusted executor, not as a stolen attack tool.

3. Does this issue affect only Ethereum?

No. Any EVM destination chain using a similar Gateway execution model may face the same class of risk, as long as users have previously granted token approvals to the relevant addresses.

Core Summary: This article reconstructs and analyzes the ZetaChain security incident on 2026-04-28, focusing on the cross-chain execution path between GatewayZEVM and Ethereum GatewayEVM. It explains how an attacker exploited permissive cross-chain message validation to invoke transferFrom against previously approved users, and summarizes the root cause, impact scope, and remediation priorities.