[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:
- 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);
- 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.
[Bug Bounty] Committee signature replay across deployments —
updateOwnersdigest lacks contract-address & chain-id bindingSummary
MultiSigVerifier.updateOwnersverifies committee signatures over a digest that binds neither the deploying contract's address nor the chain id:Every other signature-gated message in this codebase is domain-bound to
address(this)(all Gateway typehashes at Gateway.sol:42-48, andCommitteeManagement.getNoncedDigestat 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 deployedCommitteeManagementproxy (CommitteeManagement.sol:14), which gates all of Gateway's privileged flows.Proof of concept
The PoC deploys two independent
MultiSigVerifierinstances with identical initial committees (the common redeploy/ops pattern) and proves:Result:
Exploit scenarios
A. Cross-deployment replay (same chain).
Any two deployments sharing an owner set while one still has
nonce == 0accept 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 togetNoncedDigest). Node operator keys derive from a singleBITVM_SECRETenvironment value (bitvm2-nodenode/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
updateOwnerspayload (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:
Optionally also route through
executed[noncedHash]consumption (asexecuteNoncedSignaturesdoes) 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.