Skip to content

Bitcoin inscription override uses wrong Vin index, forges MsgVoteInbound.Sender #4603

Description

@kingpinXD

Summary

The Bitcoin inbound observer hard-codes tx.Vin[0].Txid when resolving the sender for inscription-memo deposits, but tryExtractInscription accepts an inscription on any input. When the inscription rides on Vin[k>0], the sender lookup walks back through a completely unrelated previous transaction the attacker hand-picks, so event.FromAddress resolves to a victim address while event.MemoBytes is the attacker's memo. The forged address propagates into MsgVoteInbound.Sender / TxOrigin and from there into cctx.InboundParams.Sender, the BTC revert receiver, and gatewayzevm.MessageContext.{Sender, SenderEVM} exposed to universal apps.

Root cause

// zetaclient/chains/bitcoin/observer/witness.go:111-118
} else if candidate = tryExtractInscription(tx, logger); candidate != nil {
    memo = candidate
    // override the sender address with the initiator of the inscription's commit tx
    if fromAddress, err = bitcoinClient.GetTransactionInitiator(ctx, tx.Vin[0].Txid); err != nil {
        return nil, errors.Wrap(err, "unable to get inscription initiator")
    }
}

tryExtractInscription (lines 200-229) returns the memo but discards which input carried it. The override then assumes the inscription must be on Vin[0]. GetTransactionInitiator walks one hop back (tx -> Vin[0] -> that prev tx's Vin[0] -> spender pkScript), so the attacker only needs Vin[0] to spend any UTXO whose parent tx had the victim at TxIn[0] — essentially any output of any tx the victim ever signed.

The existing test TestGetBtcEventWithWitness/decode_inscription_ok works around the bug rather than exposing it — witness_test.go:217 rewrites tx.Vin[0].Txid = preHash precisely to feed the override the right txid that the production code would otherwise miss.

Impact

This is a sender-forgery primitive, not a fund-extraction primitive against the protocol. The chain does not lose custody. Concrete impacts:

  1. Compliance bypass (event.go:77): ContainRestrictedAddress checks the spoofed FromAddress, so a sanctioned spender can launder through the bridge by pinning Sender to a clean victim.
  2. msg.sender impersonation in ZEVM: MessageContext.Sender / SenderEVM is propagated to universal apps. Any user-deployed app that gates on msg.sender can be tricked into acting "as the victim". Protocol-deployed contracts (Gateway, ZRC20, SystemContract, ZetaConnector ZEVM) do not gate privileged behavior on this field, so they are unaffected.
  3. Revert-path griefing: For BTC inbounds, AddRevertOutbound (cctx.go:150-156) defaults revertReceiver = InboundParams.Sender unless the memo's RevertOptions.RevertAddress is set. An attacker who omits the override and designs the deposit to revert ends up gifting their BTC to the victim's address. Self-victimizing, no chain loss.

Cost to the attacker: inscription fees (~$15) plus the BTC deposit, which they only get back via the non-revert path.

Fix

Make tryExtractInscription return the input index it found the memo on, and use tx.Vin[i].Txid in the GetTransactionInitiator call. ~5 LOC.

// tryExtractInscription(tx, logger) -> (memo []byte, vinIndex int, found bool)
} else if candidate, vinIndex, ok := tryExtractInscription(tx, logger); ok {
    memo = candidate
    if fromAddress, err = bitcoinClient.GetTransactionInitiator(ctx, tx.Vin[vinIndex].Txid); err != nil {
        return nil, errors.Wrap(err, "unable to get inscription initiator")
    }
}

Replace the workaround at witness_test.go:215-219 with a real Vin[0]-inscription fixture, and add a regression test placing the inscription on Vin[1] with a different Vin[0] funder, asserting the event's FromAddress tracks the inscription's commit tx rather than Vin[0]'s parent.

While in the area, worth checking whether GetTransactionInitiator's "spender of the previous tx's first input" definition is actually the right semantic for an ordinal inscription, or whether it should be the funder of the commit tx itself.

Backport to v36 / v37 / v38 — same code, same bug.

History

Introduced by PR #3964 (commit f26ceaa2c4ae7001136d5985c4bc1b4e9f089232, 2025-06-16). The PR's intent was correct ("use the commit tx initiator address for Bitcoin inscription based inbound") but the implementation conflated "the inscription's commit tx" with "Vin[0]'s parent tx".

Severity

Medium. Permissionless integrity violation with concrete reach (compliance bypass and msg.sender impersonation in universal apps), no observer or signer compromise required, no protocol fund extraction.

References

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