Skip to content

Solana outbound TSS preimage missing writable-flag binding and length prefixes (ZCNode-267 + ZCNode-269) #4604

@kingpinXD

Description

@kingpinXD

Summary

Two distinct but co-located TSS preimage bugs in the Solana outbound execute path. Both let an attacker replay a valid TSS signature against a different effective execution. Same code surface (Go: pkg/contracts/solana/gateway_message.go; Anchor: programs/gateway/src/utils/validate_message_hash.rs), same migration cost (paused/drained/redeploy of the Solana gateway), so worth fixing in one coordinated release.

Bug 1 — Missing is_writable bit (HackenProof ZCNode-267)

The Go-side MsgExecute.Hash() / MsgExecuteSPL.Hash() and the Anchor-side validate_message_hash only hash account.key() for each remaining account. The is_writable bit is dropped on both sides. An attacker who observes a TSS-signed Solana outbound before its nonce is consumed can replay it with the same pubkeys but flipped readonly→writable, and the gateway will CPI into the destination program with the escalated privileges. is_signer is hardcoded to false by prepare_account_metas, so writability is the only escalation surface.

Pinned commit (Anchor): cc8b0e2 / PR #131.

Bug 2 — Length / count prefix ambiguity (HackenProof ZCNode-269)

Both sides concatenate ... || data || pubkey_1 || pubkey_2 || ... with no length prefix on data and no count prefix on remaining_accounts. Every pubkey is 32 bytes, so any 32-byte-aligned shift of the data / remaining_accounts boundary produces a byte-identical preimage. A single TSS signature is valid for many distinct (data, remaining_accounts) tuples.

Five constraints limit blast radius against the reference connected example program (PDA-constrained slot 0, hardcoded random_wallet target, UTF-8-gated data, atomic rollback on CPI error, is_signer = false), but the cryptographic primitive (TSS signature does not uniquely bind the account list) is real and the attack surface against user-deployed connected programs with looser slot constraints or wider input acceptance remains open.

Pinned commit (Anchor): 0852ac80 / PR #131.

Code links (shared surface)

  • zeta-node/pkg/contracts/solana/gateway_message.go:381-385MsgExecute.Hash() tail
  • zeta-node/pkg/contracts/solana/gateway_message.go:722-726MsgExecuteSPL.Hash() tail
  • zeta-node/pkg/contracts/solana/gateway_message.go:373to/destination_program hashed at fixed offset (bound)
  • protocol-contracts-solana/programs/gateway/src/utils/validate_message_hash.rs:30-38 — verifier loop
  • protocol-contracts-solana/programs/gateway/src/instructions/execute.rs:57-65handle_sol_common wiring
  • protocol-contracts-solana/programs/gateway/src/utils/prepare_account_metas.rs:8-31is_signer = false enforced (already correct)

Fix

Bundle both fixes into one Anchor + zetaclient release. Pause the Solana gateway, drain in-flight Execute/ExecuteSPL CCTXs under the old layout, deploy the Anchor upgrade with the new preimage layout, cut a zetaclient release with the matching Go change, roll the zetaclient fleet, then unpause.

New preimage layout (both Go and Anchor must match exactly):

  1. Length-prefix data: emit data_len (u32 BE) || data instead of just data.
  2. For each remaining account, emit pubkey(32B) || is_writable(1B) instead of just pubkey(32B). The is_signer byte is unnecessary since prepare_account_metas already forces it to false on the Anchor side.

Alternative: replace the ad-hoc concatenation with a Borsh-serialized typed struct on both sides, which length-prefixes Vec<u8> and Vec<Pubkey> automatically and eliminates this bug class for any future variable-length field added to the preimage. Longer-term hardening but bigger diff.

Severity

Each bug is independently High (cryptographic flaw in TSS-signed message construction, no permission required to weaponize beyond observing or constructing a Solana outbound, fix requires coordinated redeploy). One combined PR keeps the migration overhead bounded to a single pause/drain cycle.

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