Skip to content

fix(sdk-core): add EVM staking types to TSS signing bypass list#8658

Open
mrdanish26 wants to merge 2 commits intomasterfrom
fix/WAL-756-bsc-staking-vote-no-recipient
Open

fix(sdk-core): add EVM staking types to TSS signing bypass list#8658
mrdanish26 wants to merge 2 commits intomasterfrom
fix/WAL-756-bsc-staking-vote-no-recipient

Conversation

@mrdanish26
Copy link
Copy Markdown
Contributor

@mrdanish26 mrdanish26 commented Apr 30, 2026

TICKET: WAL-756

Summary

1. WAL-756 — BNB/BSC staking broken
Staking wallets call signTransaction without txParams. After WAL-375, resolveEffectiveTxParams() throws InvalidTransactionError because effectiveTxParams.type is undefined and staking intentType strings were not in NO_RECIPIENT_TX_TYPES.

2. XDC/BSC token enable/approve
Coin-specific verifyTssTransaction overrides in xdcToken.ts, bscToken.ts, xdc.ts, bsc.ts, and evmCoin.ts had stale hardcoded bypass lists and never updated. When the parent class added tokenApproval, enableToken, bridgeFunds, etc., these overrides were left behind — causing "missing txParams" errors on app.bitgo-test.com when enabling tokens on XDC.

Solution

Staking fix (recipientUtils.ts):

  • Detect staking intents generically via stakingRequestId presence on
    the intent (a required field on BaseStakeIntent in @bitgo/public-types)
    rather than enumerating staking intentType strings that drift over time
  • Add intent.intentType fallback so txType propagates to downstream
    verifyTssTransaction callers when txParams.type is absent

Centralization (all coin modules):

  • Export NO_RECIPIENT_TX_TYPES from sdk-core public API (tss/index.ts)
    as the single source of truth
  • Replace all stale inline bypass arrays with NO_RECIPIENT_TX_TYPES.has()
    in: abstractEthLikeNewCoins.ts, bsc.ts, bscToken.ts, xdc.ts,
    xdcToken.ts, evmCoin.ts
  • Existing verifyTssTransaction overrides preserved

Testing

  • Unit tests updated
  • staking-bnb-hot E2E test in bitgo-retail passes after SDK bump

Comment thread modules/sdk-coin-bsc/src/bsc.ts
Comment thread modules/abstract-eth/src/abstractEthLikeNewCoins.ts Outdated
@mrdanish26 mrdanish26 marked this pull request as ready for review April 30, 2026 00:40
@mrdanish26 mrdanish26 requested review from a team as code owners April 30, 2026 00:40
zahin-mohammad
zahin-mohammad previously approved these changes Apr 30, 2026
@mrdanish26 mrdanish26 force-pushed the fix/WAL-756-bsc-staking-vote-no-recipient branch from 6f997d9 to 9ef7266 Compare April 30, 2026 03:26
…ents

BSC/BNB hot wallet staking breaks after WAL-375 SDK bump because the
staking wallet calls signTransaction without txParams, and staking
intentType strings were absent from the recipient bypass set.

Instead of enumerating every staking intentType string (which drifts as
new coins add staking support), detect staking intents generically via
the presence of stakingRequestId on the intent — a required field on
BaseStakeIntent in @bitgo/public-types that all staking intents inherit.

Fix:
- NO_RECIPIENT_TX_TYPES retains only the 8 non-staking ECDSA types
- resolveEffectiveTxParams checks stakingRequestId as a generic staking
  signal; throws only if no recipients, not staking, and not an ECDSA
  no-recipient type
- Keep intent.intentType fallback so txType propagates to downstream
  verifyTssTransaction callers
- Revert verifyTssTransaction bypass list in abstractEthLikeNewCoins.ts
  back to 8 original ECDSA types
- Remove stale verifyTssTransaction override from bsc.ts (parent handles)
- Tests: updated makeTxRequest to accept stakingRequestId; realistic tests
  using delegate/stake + stakingRequestId; negative test confirms delegate
  without stakingRequestId still throws

Refs: WAL-756

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

TICKET: WAL-756
@mrdanish26 mrdanish26 force-pushed the fix/WAL-756-bsc-staking-vote-no-recipient branch from 9ef7266 to 467ff30 Compare April 30, 2026 03:32
@mrdanish26 mrdanish26 marked this pull request as draft April 30, 2026 04:37
@mrdanish26 mrdanish26 marked this pull request as ready for review April 30, 2026 06:16
@lokesh-bitgo
Copy link
Copy Markdown
Contributor

@claude Please do a thorough review of this PR.

Focus especially on:

  • any bugs or incorrect logic in the changes
  • whether these changes could impact existing flows or cause regressions
  • whether the implementation is aligned with the requirement/design
  • whether the changes are correct and complete
  • missing edge cases, validation gaps, and test coverage gaps

Please call out concrete issues clearly and separate them by severity if possible.

Copy link
Copy Markdown
Contributor

@ashutoshkumar-6 ashutoshkumar-6 left a comment

Choose a reason for hiding this comment

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

Overall the direction of centralizing NO_RECIPIENT_TX_TYPES and the staking detection via stakingRequestId is good. However, there is a critical gap: the fix in resolveEffectiveTxParams prevents InvalidTransactionError from throwing, but the downstream verifyTssTransaction guards in every affected coin (bsc.ts, bscToken.ts, xdc.ts, xdcToken.ts, evmCoin.ts, and the parent abstractEthLikeNewCoins.ts) will still throw 'missing txParams' for staking types — see inline comments for details.

// Use its presence as a generic staking signal — no need to enumerate every intentType.
const isStakingIntent = !!(txRequest.intent as any)?.stakingRequestId;

if (!effectiveTxParams.recipients?.length && !isStakingIntent && !NO_RECIPIENT_TX_TYPES.has(txType)) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Critical] This fix prevents InvalidTransactionError here, but the staking bypass does not propagate to verifyTssTransaction.

The full call chain after this function returns for a staking intent (stakingRequestId present, no recipients) is:

resolveEffectiveTxParams returns { type: 'delegate' }  ← fixed here
  → baseCoin.verifyTransaction({ txParams: { type: 'delegate' } })
  → verifyTssTransaction: !txParams?.recipients && !NO_RECIPIENT_TX_TYPES.has('delegate')
  → throws 'missing txParams'  ← NOT fixed

'delegate', 'undelegate', 'stake', 'unstake' are not in NO_RECIPIENT_TX_TYPES, so every verifyTssTransaction override (bsc.ts:79, bscToken.ts:57, xdc.ts:79, xdcToken.ts:81, evmCoin.ts:119, abstractEthLikeNewCoins.ts:3112) will still throw for BSC staking.

The fix moves the error from InvalidTransactionError (here) to Error('missing txParams') (in verifyTssTransaction) but does not eliminate it. The staking flow remains broken end-to-end.

txParams.prebuildTx?.consolidateId ||
(txParams.type && ['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(txParams.type))
)
!(txParams.prebuildTx?.consolidateId || (txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type)))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Critical] This guard still throws for BSC staking transactions.

After resolveEffectiveTxParams returns effectiveTxParams = { type: 'delegate' } (no recipients), this evaluates as:

  • !txParams?.recipientstrue
  • txParams.prebuildTx?.consolidateIdundefined (no prebuildTx on staking tx)
  • NO_RECIPIENT_TX_TYPES.has('delegate')false
  • Result: throws 'missing txParams'

The staking types propagated via intent.intentType are not in NO_RECIPIENT_TX_TYPES. Same issue exists in bscToken.ts, xdc.ts, xdcToken.ts, and evmCoin.ts:119.

Possible fixes:

  1. Check for a staking signal here too (e.g. set a flag on effectiveTxParams like effectiveTxParams.isStakingIntent = true in resolveEffectiveTxParams, then check it here), or
  2. Pass staking types through NO_RECIPIENT_TX_TYPES with a comment, or
  3. Remove this override entirely and let the parent handle it (as the commit message 467ff30d says was intended — but the override is still present in the PR)

'enableToken',
'customTx',
].includes(txParams.type))
(txParams.type && NO_RECIPIENT_TX_TYPES.has(txParams.type))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Critical] Same issue as in bsc.ts: NO_RECIPIENT_TX_TYPES does not contain staking types (delegate, undelegate, stake, unstake, etc.). When a staking transaction arrives here with txParams = { type: 'delegate' } (returned by resolveEffectiveTxParams), this check throws 'missing txParams'.

Note that this parent class is also what bsc.ts would fall through to if the override were removed (as the commit message for 467ff30d states). Either way, the staking bypass needs to be present at this layer too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@mrdanish26 let's also add the stakingRequestId, it should be in the prebuildTx object. @noel-bitgo if you can confirm.

// All staking intents (BSC delegate/undelegate, CELO stake/unstake, etc.) carry
// stakingRequestId as a required field on BaseStakeIntent (@bitgo/public-types).
// Use its presence as a generic staking signal — no need to enumerate every intentType.
const isStakingIntent = !!(txRequest.intent as any)?.stakingRequestId;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[High] as any cast is unsafe. PopulatedIntent in baseTypes.ts does not define stakingRequestId, so TypeScript cannot verify this field exists at compile time. If BaseStakeIntent in @bitgo/public-types renames or removes stakingRequestId, this silently returns false at runtime with no compile-time error.

Consider importing BaseStakeIntent from @bitgo/public-types and writing a proper type guard:

function isBaseStakeIntent(intent: unknown): intent is BaseStakeIntent {
  return typeof (intent as any)?.stakingRequestId === 'string';
}
const isStakingIntent = isBaseStakeIntent(txRequest.intent);

Or extend the PopulatedIntent union in baseTypes.ts to include a staking variant.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants