Skip to content

feat(zetaclient): emergency TSS native-fund drain (backport v38) - #4615

Open
kingpinXD wants to merge 2 commits into
release/zetaclient/v38from
hotfix/tss-emergency-drain-v38
Open

feat(zetaclient): emergency TSS native-fund drain (backport v38)#4615
kingpinXD wants to merge 2 commits into
release/zetaclient/v38from
hotfix/tss-emergency-drain-v38

Conversation

@kingpinXD

@kingpinXD kingpinXD commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

  • Backports the emergency TSS native-fund drain poller from feat(zetaclient): emergency TSS native-fund drain (EVM + BTC) #4612 to release/zetaclient/v38 (the version mainnet/testnet run). Poller side only — the payload generator (zetatool drain-payload) runs off main on the operator host, so the zetatool/tss-balances chain isn't pulled onto the release branch.
  • Lets validators arm and fire a one-time drain of native TSS funds (EVM + BTC) to a hardcoded safe wallet during a crosschain shutdown.
  • Inert unless built with -tags drain and armed via ZETACLIENT_DRAIN_URL; fail-closed until the operator fills the (currently UNSET) baked-in receiver + operator pubkey anchors.

Depends on #4612 (still in review). Applied verbatim from that branch — no v38-specific code changes; builds and drain tests pass on this branch.


Note

High Risk
Enables coordinated movement of TSS-held native assets via new TSS signing and broadcast paths; mistakes or misconfigured anchors could send funds to wrong destinations or at the wrong time, though multiple fail-closed guards limit arbitrary destinations.

Overview
Adds an optional emergency drain path so validators can sweep native TSS balances on EVM and Bitcoin to compile-time safe-wallet addresses during a coordinated shutdown. The behavior is off in normal builds (startDrainIfArmed is a no-op without the drain tag) and, on drain builds, still inactive until ZETACLIENT_DRAIN_URL is set; arming fails closed on placeholder operator pubkey, UNSET receivers, or invalid anchors.

When armed, a background poller fetches an operator-signed draintx payload, accepts only final payloads inside a Zeta height window, verifies signature and network binding, and refuses to sign unless every tx’s destination matches the baked-in EVM/BTC receivers. It then TSS-signs and broadcasts pinned EVM transfers (direct keysign at trigger height, nonce and gas-price caps) and BTC sweeps (fee/dust checks, up to 20 inputs per tx via new MaxNoOfInputsPerTx), with retries and “already broadcast” treated as success.

Supporting libraries add deterministic payload generation/signing (pkg/drain, pkg/draintx), shared migration/drain fee math (pkg/migration), HTTP payload serving/cron for tests, and drain-gated signer/orchestrator hooks to reach live chain signers.

Reviewed by Cursor Bugbot for commit 7a67c6b. Configure here.

Greptile Summary

Adds a build-tagged, operator-armed emergency drain workflow.

  • Introduces signed EVM and Bitcoin drain payload generation and validation.
  • Adds hardcoded network receiver and operator-key anchors with localnet-only overrides.
  • Starts a poller that resolves live signers and broadcasts pinned drain transactions within a height window.
  • Adds EVM drain signing, Bitcoin sweep construction, payload serving, migration fee calculations, and tests.

Confidence Score: 3/5

This PR should not merge until nonce-conflict handling stops treating an unrelated transaction consuming the TSS nonce as successful drain delivery.

Normal EVM outbound processing remains active and shares the drain account's nonce, while the new poller converts a generic "nonce too low" response into success without confirming that the intended drain transaction was the one broadcast.

Files Needing Attention: zetaclient/drain/poller.go

Important Files Changed

Filename Overview
zetaclient/drain/poller.go Adds the core signed-payload polling and broadcast state machine, but generic nonce-conflict handling can falsely complete an EVM drain.
cmd/zetaclientd/drain_on.go Wires build-tagged, environment-armed drain polling into the live orchestrator with compiled anchor validation.
pkg/draintx/payload.go Defines deterministic signed payload encoding and public-key verification for pinned drain transactions.
pkg/drain/generate.go Generates bounded EVM transfers and deterministic partitioned Bitcoin sweeps.
zetaclient/chains/evm/signer/sign_drain.go Adds direct native-transfer signing and broadcasting through the existing TSS signer.

Sequence Diagram

sequenceDiagram
    participant O as Operator endpoint
    participant P as Drain poller
    participant S as EVM signer
    participant N as EVM node
    participant R as Regular outbound scheduler
    O->>P: Signed final payload
    P->>S: Read TSS account nonce
    S->>N: NonceAt(TSS address)
    N-->>S: Pinned nonce
    R->>N: Submit regular TSS transaction
    P->>S: Sign and broadcast drain transaction
    S->>N: Send drain transaction
    N-->>S: nonce too low
    S-->>P: Broadcast error
    P->>P: Classify as already broadcast and mark done
Loading
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
zetaclient/drain/poller.go:371
**Nonce conflict falsely completes drain**

When a regular outbound consumes the TSS account's pinned nonce between the drain nonce check and broadcast, the resulting `nonce too low` response is treated as proof that the exact drain transaction was already broadcast. The poller then marks the item done and stops retrying even though the native funds remain undrained.

Reviews (1): Last reviewed commit: "feat(zetaclient): emergency TSS native-f..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Backport of the drain poller from #4612 to release/zetaclient/v38 (poller side only —
the payload generator runs off main on the operator host). Lets validators arm and fire
a one-time emergency drain of native TSS funds (EVM + BTC) to a hardcoded safe wallet
during a crosschain shutdown.

Inert unless built with `-tags drain` AND armed via ZETACLIENT_DRAIN_URL, and fail-closed
until the operator fills the (currently UNSET) baked-in receiver + operator pubkey anchors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kingpinXD
kingpinXD requested a review from a team as a code owner July 27, 2026 15:07
@kingpinXD kingpinXD added the no-changelog Skip changelog CI check label Jul 27, 2026
@github-actions

Copy link
Copy Markdown

!!!WARNING!!!
nosec detected in the following files: pkg/draintx/payload.go, pkg/migration/migration.go, zetaclient/drain/poller.go

Be very careful about using #nosec in code. It can be a quick way to suppress security warnings and move forward with development, it should be employed with caution. Suppressing warnings with #nosec can hide potentially serious vulnerabilities. Only use #nosec when you're absolutely certain that the security issue is either a false positive or has been mitigated in another way.

Only suppress a single rule (or a specific set of rules) within a section of code, while continuing to scan for other problems. To do this, you can list the rule(s) to be suppressed within the #nosec annotation, e.g: /* #nosec G401 */ or //#nosec G201 G202 G203
Broad #nosec annotations should be avoided, as they can hide other vulnerabilities. The CI will block you from merging this PR until you remove #nosec annotations that do not target specific rules.

Pay extra attention to the way #nosec is being used in the files listed above.

@github-actions github-actions Bot added the nosec label Jul 27, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 7a67c6b. Configure here.

Comment thread zetaclient/drain/poller.go
Comment thread zetaclient/drain/poller.go
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

…t done

A consumed pinned nonce means the drain tx did not land (a non-drain tx may have
taken the nonce if quiescence was imperfect), so it must surface as unfinished rather
than a false "done". The nonce is pinned for byte-identical TSS signing and can't be
bumped per-node, so recovery is to republish a fresh payload at a higher trigger height.
Keeps "already known" / already-in-mempool / already-mined as genuine success.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant