Skip to content

[Security] Committee signature replay across deployments: updateOwners digest lacks contract-address & chain-id binding #437

Description

@cilokesteh

[Bug Bounty] Committee signature replay across deployments — updateOwners digest lacks contract-address & chain-id binding

Summary

MultiSigVerifier.updateOwners verifies committee signatures over a digest that binds neither the deploying contract's address nor the chain id:

// MultiSigVerifier.sol:58
bytes32 digest = keccak256(abi.encodePacked(nonce, newOwners, newRequired));

Every other signature-gated message in this codebase is domain-bound to address(this) (all Gateway typehashes at Gateway.sol:42-48, and CommitteeManagement.getNoncedDigest at CommitteeManagement.sol:277-280). updateOwners — the function that controls who the committee is — is the single exception. This makes signatures collected in one deployment context valid in another, enabling cross-deployment and cross-chain replay that transfers full committee control to an attacker.

A working Foundry proof-of-concept is included (test/MultiSigReplayPoC.t.sol, passes on forge 1.7.1 / solc 0.8.28 against this repo's code unmodified).

Affected component

  • src/MultiSigVerifier.sol:54-66 (updateOwners) — inherited and exposed by the deployed CommitteeManagement proxy (CommitteeManagement.sol:14), which gates all of Gateway's privileged flows.

Proof of concept

The PoC deploys two independent MultiSigVerifier instances with identical initial committees (the common redeploy/ops pattern) and proves:

  1. Signatures collected "for deployment A" are byte-for-byte identical to signatures that verify on deployment B (because the signed message does not depend on the target contract);
  2. Replaying them on B succeeds, installing an attacker-chosen owner set with threshold 1 — full committee takeover of the second deployment.

Result:

$ forge test --match-contract MultiSigReplayPoC -vv
[PASS] test_crossDeploymentReplay_takeover() (gas: 193835)
Suite result: ok. 1 passed; 0 failed

Exploit scenarios

A. Cross-deployment replay (same chain).
Any two deployments sharing an owner set while one still has nonce == 0 accept the same signature set. Redeploys after testnet iterations, fresh deployments for audits/staging, or deployments by a different team member all satisfy this.

B. Cross-chain replay (testnet → mainnet).
No digest anywhere in this system includes block.chainid (this also applies to getNoncedDigest). Node operator keys derive from a single BITVM_SECRET environment value (bitvm2-node node/src/env.rs:187-196), so the same EOA keys are routinely used across networks. A signature set gathered for a mainnet owner-update can be replayed against the testnet deployment (or vice versa) as long as the receiving contract has not consumed its nonce for that digest — and since digests are not chain-bound, the receiving chain cannot distinguish them.

Security impact

An attacker who obtains one legitimate quorum signature set over an updateOwners payload (e.g. during a routine rotation on any deployment/chain) can replay it to seize the committee on any other deployment/chain sharing the same owner set and unconsumed nonce. Committee control yields: mint authority over PegBTC via Gateway flows, ability to slash arbitrary operators, cancel withdrawals, and finalize withdrawals — i.e. full control of bridge fund custody policy.

Severity: Medium on code-level evidence alone; High under the realistic key-reuse operational condition above (single BITVM_SECRET per operator).

Recommended fix

Bind the digest the same way the rest of the system does:

bytes32 typeHash = keccak256("UPDATE_OWNERS(address[] newOwners,uint256 newRequired,uint256 nonce)");
bytes32 digest = keccak256(abi.encode(
    typeHash,
    address(this),
    block.chainid,
    nonce,
    keccak256(abi.encode(newOwners)),
    newRequired
));

Optionally also route through executed[noncedHash] consumption (as executeNoncedSignatures does) instead of a bare incrementing nonce.


Submitted as part of the GOAT BitVM3 Bug Bounty Campaign. Happy to provide the full PoC file or additional traces.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions