Assessment reports>ZetaChain>Threat Models>Flow: Observing incoming cross-chain transactions from EVM-compatible chains

Flow: Observing incoming cross-chain transactions from EVM-compatible chains

Observation and processing of incoming cross-chain transactions starts in the ExternalChainWatcher() function. This function runs the observeInTX() function every chainconfig.BlockTime seconds, up to a maximum of 24 seconds. For Ethereum, this would be 12 seconds.

The observeInTX() function has the bulk of the functionality. It first ensures that the chain the transaction is coming from has been whitelisted. All external chains are blacklisted by default. It then ensures to go back to the last confirmed block (i.e., it does not take the latest block on the tip of the chain as confirmed). This prevents potential issues where a chain reorganization would lead to a double spend.

It then looks for any emitted ZetaSent events from the last checked block to the latest confirmed block. It ensures to get emitted events only from the Connector contract, as otherwise anyone would be able to deploy their own contract and emit events of the same signature to reach this code path.

For each ZetaSent event, the code uses the PostSend() function to construct a MsgVoteOnObservedInboundTx message to broadcast to Zetacore. The coin type for the amount of coins being sent across chains is set to CoinType_Zeta for this case, which means ZetaSent events are only used to transfer ZETA across chains. This message will get picked up by the x/crosschain module. See for more information on what happens then.

Next, the code does the same thing for Deposited events from the ERC20Custody contract, except this time the coin type is set to CoinType_ERC20.

Finally, one other thing users can do is transfer native tokens to a designated TSS address. When they do this, the transfer is noted as a "gas deposit". This is how users pay for gas in cross-chain transactions, and observeInTX() handles this case for both EVM- and Klaytn-based chains. The logic is largely the same — a MsgVoteOnObservedInboundTx message is constructed with the coin type set to CoinType_Gas and then subsequently broadcasted.

The inputs that are controllable in this flow are emitted event parameters. Specifically, for the ZetaSent event, a user would be able to fully control the following parameters:

event ZetaSent( address sourceTxOriginAddress, // tx.origin address indexed zetaTxSenderAddress, // msg.sender uint256 indexed destinationChainId, // CONTROLLED bytes destinationAddress, // CONTROLLED uint256 zetaValueAndGas, // CONTROLLED uint256 destinationGasLimit, // CONTROLLED bytes message, // CONTROLLED bytes zetaParams // CONTROLLED );

We found one issue where, if the destinationChainId is set to a nonexistent chain, then a nil pointer dereference exception will be thrown inside observeInTX().

The code also specifically checks to see if destinationAddress is set to the ZETA token contract address. If it is, the code logs a potential attack attempt, but it continues to run normally otherwise.

For the Deposited event, a user would be able to fully control the following parameters:

event Deposited( bytes recipient, // CONTROLLED IERC20 asset, // Partially controlled, must be whitelisted uint256 amount, // CONTROLLED, but must own at least this amount of tokens bytes message // CONTROLLED );

None of the parameters are checked or used in the code. They are simply passed into the MsgVoteOnObservedInboundTx message that is later broadcasted.

For more information on how these parameters are used in the x/crosschain module, please see .

Zellic © 2024Back to top ↑