Summary
A direct user EOA call to ConnectorZEVM.send(...) on ZEVM lands with InboundParams.Sender set to the connector address itself, not the user EOA. When the outbound fails, the V1 revert path picks revertReceiver = InboundParams.Sender = ConnectorZEVM and calls ZetaConnectorZEVM.onRevert(zetaTxSenderAddress = ConnectorZEVM, ...). The connector's onRevert wraps the native ZETA into WZETA and transferFroms it to itself, so the user's refund is trapped on the connector contract.
Root cause
x/crosschain/keeper/evm_hooks.go:59-63 sets emittingContract := *msg.To, which is the connector address for a direct EOA call.
x/crosschain/keeper/evm_hooks.go:295 writes emittingContract.Hex() into MsgVoteInbound.Sender instead of using the ZetaSent event's own ZetaTxSenderAddress field (correctly set on-chain to msg.sender of the connector call).
x/crosschain/types/cctx.go:151 gates the RevertOptions.GetEVMRevertAddress() override to V2 only, so V1 has no user-supplied override path.
Fix
Replace emittingContract.Hex() with the event's ZetaTxSenderAddress.Hex() in ProcessZetaSentEvent (evm_hooks.go:295). The same emittingContract-as-Sender pattern exists in ProcessZRC20WithdrawalEvent (evm_hooks.go:207-226) and should be patched in parallel, using the corresponding event-side from field.
Add an e2e regression on LegacyWithdrawZeta asserting InboundParams.Sender == TxOrigin and zero WZETA delta on ConnectorZEVM post-revert.
Reachability
V1 is still wired in ProcessLogs (evm_hooks.go:103); V2 ZETA flows are gated off at v2_zevm_inbound.go:200-204, so V1 is the only live ZETA cross-chain path. Failure modes that reach the revert leg include restricted-address compliance, insufficient locked balance on the destination connector, dust limits, and paused chain params.
Recovery for trapped balances
Admin policy group 2 can patch the connector via MsgUpdateContractBytecode and sweep accumulated WZETA back to a treasury or to the historic cctx TxOrigin.
Severity
Medium. Self-victimizing (no third-party drain primitive), governance-recoverable, but reachable in production today on the only live legacy ZETA path.
References
Summary
A direct user EOA call to
ConnectorZEVM.send(...)on ZEVM lands withInboundParams.Senderset to the connector address itself, not the user EOA. When the outbound fails, the V1 revert path picksrevertReceiver = InboundParams.Sender = ConnectorZEVMand callsZetaConnectorZEVM.onRevert(zetaTxSenderAddress = ConnectorZEVM, ...). The connector'sonRevertwraps the native ZETA into WZETA andtransferFroms it to itself, so the user's refund is trapped on the connector contract.Root cause
x/crosschain/keeper/evm_hooks.go:59-63setsemittingContract := *msg.To, which is the connector address for a direct EOA call.x/crosschain/keeper/evm_hooks.go:295writesemittingContract.Hex()intoMsgVoteInbound.Senderinstead of using theZetaSentevent's ownZetaTxSenderAddressfield (correctly set on-chain tomsg.senderof the connector call).x/crosschain/types/cctx.go:151gates theRevertOptions.GetEVMRevertAddress()override to V2 only, so V1 has no user-supplied override path.Fix
Replace
emittingContract.Hex()with the event'sZetaTxSenderAddress.Hex()inProcessZetaSentEvent(evm_hooks.go:295). The sameemittingContract-as-Sender pattern exists inProcessZRC20WithdrawalEvent(evm_hooks.go:207-226) and should be patched in parallel, using the corresponding event-sidefromfield.Add an e2e regression on
LegacyWithdrawZetaassertingInboundParams.Sender == TxOriginand zero WZETA delta onConnectorZEVMpost-revert.Reachability
V1 is still wired in
ProcessLogs(evm_hooks.go:103); V2 ZETA flows are gated off atv2_zevm_inbound.go:200-204, so V1 is the only live ZETA cross-chain path. Failure modes that reach the revert leg include restricted-address compliance, insufficient locked balance on the destination connector, dust limits, and paused chain params.Recovery for trapped balances
Admin policy group 2 can patch the connector via
MsgUpdateContractBytecodeand sweep accumulated WZETA back to a treasury or to the historic cctxTxOrigin.Severity
Medium. Self-victimizing (no third-party drain primitive), governance-recoverable, but reachable in production today on the only live legacy ZETA path.
References
hackeproof/analysis/blockchain/report_ZCNode-255.md