Skip to content

Carry event-paid gasFee from GatewayZEVM V2 events into CCTX as refund cap #4586

@kingpinXD

Description

@kingpinXD

Summary

V2 inbound parsing in x/crosschain/types/inbound_parsing.go does not carry event.gasFee or event.protocolFlatFee from Withdrawn / WithdrawnAndCalled into MsgVoteInbound. CCTXGatewayObservers.InitiateOutbound recomputes UserGasFeePaid from the node-side median gas price (x/crosschain/keeper/cctx_gateway_observers.go:77) instead of trusting the amount the contract actually burned.

In production the two bases stay matched. MsgVoteGasPrice writes the crosschain-keeper store and the SystemContract gasPriceByChainId inside a single message handler (x/crosschain/keeper/msg_server_vote_gas_price.go:91-104), so any gateway.withdraw() within a block reads consistent values on both sides. Under matching prices the node value is gasPrice * gasLimit while the contract burns gasPrice * gasLimit + PROTOCOL_FLAT_FEE, so UserGasFeePaid is always <= the user-paid amount and refunds cannot exceed what was paid. Reported as HackenProof ZCNode-254 (report).

This issue tracks the defense-in-depth fix: persist the event-paid gasFee on the CCTX and clamp refundable gas-token credit at min(UserGasFeePaid, event.GasFee). Makes the protocol resilient against any future code path that updates the SystemContract independently of the node-side median.

Branch impact

  • release/v36 (mainnet): not affected. The refund path (feat: refund a portion of remaining unused tokens to user #3734) is not deployed there.
  • release/zetacore/v37 (testnet): same accounting model as main. Same atomic gas-price update protects against the PoC's divergence today. Defense-in-depth fix still worth applying.
  • main: same as v37.

Suggested fix

  1. Add optional event_gas_fee / event_protocol_flat_fee fields to MsgVoteInbound (treated as absent on legacy CCTXs).
  2. Carry the values from the parsed event in NewWithdrawalInbound and NewWithdrawAndCallInbound (x/crosschain/types/inbound_parsing.go:99-148, 203-249). Called does not carry a gasFee field, so this only applies to withdrawal flows.
  3. Persist on OutboundParams and use as a hard cap in refundUnusedGas (x/crosschain/keeper/msg_server_vote_outbound.go:144-251): usableRemainingFees = 95% * (min(UserGasFeePaid, EventGasFee) - outboundTxFeePaid).

Optional field, no state migration needed.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions