Skip to content

bitcore-wallet-service: createTx defaults coin from the wallet but not chain, so Polygon proposals are stored with chain: 'eth' and fail verification #4242

Description

@nitsujlangston

What happens

packages/bitcore-wallet-service/src/lib/server.ts, in the createTx flow:

opts.coin = opts.coin || wallet.coin;     // coin defaulted from the wallet
                                          // chain is not

and shortly after:

chain: opts.chain?.toLowerCase() || Utils.getChain(opts.coin),

A client that omits chain therefore falls through to Utils.getChain(coin):

if (Constants.BITPAY_SUPPORTED_ETH_ERC20.includes(normalizedChain) ||
    !Constants.CHAINS.includes(normalizedChain)) {
  normalizedChain = 'eth';
}

matic appears in both CHAINS and BITPAY_SUPPORTED_ETH_ERC20, and the ERC-20 branch is checked first, so getChain('matic') === 'eth'. The proposal is stored as chain: 'eth', coin: 'matic' on a wallet whose own record says chain: 'matic'.

matic is the only entry where this happens — every other chain in CHAINS returns itself — so the bug is invisible on btc, eth, arb, base, op, xrp and the rest.

Why it matters

Verifier.checkTxProposalSignature derives the creator's copayer id from the proposal's chain:

const chain = txp.chain?.toLowerCase() || Utils.getChain(txp.coin);
const creatorKeys = credentials.publicKeyRing.find(
  item => Utils.xPubToCopayerId(chain, item.xPubKey) === txp.creatorId);

The copayer ids in the key ring were derived with matic, so the lookup with eth matches nothing, checkTxProposalSignature returns false, and the client raises:

bwc.ErrorSERVER_COMPROMISED: Server response could not be verified.

getTxProposals verifies every pending proposal and throws if any one fails, so a single mis-stamped proposal makes the wallet unusable rather than just that proposal unusable. In bitcore-cli this includes the main menu, so the wallet cannot be opened even to delete the offending proposal — it has to be removed through the API, or by waiting out DELETE_LOCKTIME if you are not its creator.

Reproduction

  1. Create a 2-of-2 TSS wallet on matic.
  2. Create a proposal through a client that does not send chainbitcore-cli's txpParams in packages/bitcore-cli/src/commands/transaction.ts does not include it.
  3. Fetch it: the stored proposal has chain: 'eth', coin: 'matic', and Verifier.checkTxProposal returns false.

Adding chain to that client's txpParams works around it, which confirms the path.

Suggested fixes

  1. Default it alongside coin in createTx: opts.chain = opts.chain || wallet.chain; — other call sites in the same file already do the equivalent (opts.chain = opts.coin for stored clients, and opts.chain || opts.coin || Defaults.COIN elsewhere).
  2. Separately, getChain should not treat a first-class chain as an ERC-20 — checking CHAINS before BITPAY_SUPPORTED_ETH_ERC20 would make getChain('matic') === 'matic', though that is a behaviour change worth considering on its own.
  3. Optionally, have getTxProposals reject only the proposals that fail verification rather than the whole response, so one bad proposal cannot lock a client out of its wallet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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