Skip to content

Commit 6f997d9

Browse files
committed
fix(sdk-core): add EVM staking types to TSS signing bypass list
BNB/BSC hot staking breaks after WAL-375 fix because the staking wallet calls signTransaction without txParams, leaving effectiveTxParams.type undefined. resolveEffectiveTxParams then falls through to the guard and throws InvalidTransactionError for all staking operations. Fix: - Add EVM staking intent types (stakingVote, stakingLock, stakingActivate, stakingUnvote, stakingUnlock, stakingWithdraw, stakingDeactivate) to NO_RECIPIENT_TX_TYPES in recipientUtils.ts - Fall back to intent.intentType when txParams.type is absent, and propagate the resolved type into effectiveTxParams so downstream verifyTssTransaction callers can use it - Mirror staking types in abstractEthLikeNewCoins.ts verifyTssTransaction bypass list - Remove stale verifyTssTransaction override from bsc.ts (was missing consolidate, bridgeFunds, enableToken, customTx vs parent, and all staking types); parent now handles BSC correctly Refs: WAL-756 Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com> TICKET: WAL-756
1 parent 6c87b40 commit 6f997d9

4 files changed

Lines changed: 79 additions & 42 deletions

File tree

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3118,6 +3118,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
31183118
'bridgeFunds',
31193119
'enableToken',
31203120
'customTx',
3121+
// EVM staking (BSC/BNB, ETH, CELO) — mirrors NO_RECIPIENT_TX_TYPES in recipientUtils.ts
3122+
'stakingLock',
3123+
'stakingVote',
3124+
'stakingActivate',
3125+
'stakingUnvote',
3126+
'stakingUnlock',
3127+
'stakingWithdraw',
3128+
'stakingDeactivate',
31213129
].includes(txParams.type))
31223130
)
31233131
) {

modules/sdk-coin-bsc/src/bsc.ts

Lines changed: 1 addition & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { BaseCoin, BitGoBase, common, MPCAlgorithm, MultisigType, multisigTypes } from '@bitgo/sdk-core';
22
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
3-
import {
4-
AbstractEthLikeNewCoins,
5-
recoveryBlockchainExplorerQuery,
6-
VerifyEthTransactionOptions,
7-
} from '@bitgo/abstract-eth';
3+
import { AbstractEthLikeNewCoins, recoveryBlockchainExplorerQuery } from '@bitgo/abstract-eth';
84
import { TransactionBuilder } from './lib';
95

106
export class Bsc extends AbstractEthLikeNewCoins {
@@ -54,34 +50,4 @@ export class Bsc extends AbstractEthLikeNewCoins {
5450
const explorerUrl = common.Environments[this.bitgo.getEnv()].bscscanBaseUrl;
5551
return await recoveryBlockchainExplorerQuery(query, explorerUrl as string, apiToken);
5652
}
57-
58-
/**
59-
* Verify if a tss transaction is valid
60-
*
61-
* @param {VerifyEthTransactionOptions} params
62-
* @param {TransactionParams} params.txParams - params object passed to send
63-
* @param {TransactionPrebuild} params.txPrebuild - prebuild object returned by server
64-
* @param {Wallet} params.wallet - Wallet object to obtain keys to verify against
65-
* @returns {boolean}
66-
*/
67-
async verifyTssTransaction(params: VerifyEthTransactionOptions): Promise<boolean> {
68-
const { txParams, txPrebuild, wallet } = params;
69-
if (
70-
!txParams?.recipients &&
71-
!(
72-
txParams.prebuildTx?.consolidateId ||
73-
(txParams.type && ['acceleration', 'fillNonce', 'transferToken', 'tokenApproval'].includes(txParams.type))
74-
)
75-
) {
76-
throw new Error(`missing txParams`);
77-
}
78-
if (!wallet || !txPrebuild) {
79-
throw new Error(`missing params`);
80-
}
81-
if (txParams.hop && txParams.recipients && txParams.recipients.length > 1) {
82-
throw new Error(`tx cannot be both a batch and hop transaction`);
83-
}
84-
85-
return true;
86-
}
8753
}

modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,31 @@ import { PopulatedIntent, TxRequest } from './baseTypes';
66
* Transaction types that legitimately carry no explicit recipients.
77
* verifyTransaction handles no-recipient validation for these internally.
88
* Mirrors the bypass list in abstractEthLikeNewCoins.ts verifyTssTransaction.
9+
*
10+
* ECDSA types: acceleration, fillNonce, transferToken, tokenApproval, consolidate,
11+
* bridgeFunds, enableToken, customTx
12+
* EVM staking (BSC/BNB, ETH, CELO): stakingLock, stakingVote, stakingActivate,
13+
* stakingUnvote, stakingUnlock, stakingWithdraw, stakingDeactivate
914
*/
1015
export const NO_RECIPIENT_TX_TYPES = new Set([
16+
// ECDSA types
1117
'acceleration',
1218
'fillNonce',
1319
'transferToken',
1420
'tokenApproval',
1521
'consolidate',
1622
'bridgeFunds',
1723
'enableToken',
18-
'customTx', // DeFi/WalletConnect smart contract interactions have no traditional recipients
24+
'customTx',
25+
26+
// EVM staking (BSC/BNB, ETH, CELO) — contract calls with no traditional txParams recipients
27+
'stakingLock',
28+
'stakingVote',
29+
'stakingActivate',
30+
'stakingUnvote',
31+
'stakingUnlock',
32+
'stakingWithdraw',
33+
'stakingDeactivate',
1934
]);
2035

2136
/**
@@ -43,7 +58,16 @@ export function resolveEffectiveTxParams(
4358
recipients: txParams?.recipients?.length ? txParams.recipients : intentRecipients,
4459
};
4560

46-
if (!effectiveTxParams.recipients?.length && !NO_RECIPIENT_TX_TYPES.has(effectiveTxParams.type ?? '')) {
61+
// Fall back to intent.intentType when txParams.type is not explicitly set.
62+
// Staking wallets call signTransaction without txParams, so the type lives only in the intent.
63+
const txType = effectiveTxParams.type ?? (txRequest.intent as PopulatedIntent)?.intentType ?? '';
64+
65+
// Propagate the resolved type so downstream callers (e.g. verifyTssTransaction) can use it.
66+
if (!effectiveTxParams.type && txType) {
67+
effectiveTxParams.type = txType;
68+
}
69+
70+
if (!effectiveTxParams.recipients?.length && !NO_RECIPIENT_TX_TYPES.has(txType)) {
4771
throw new InvalidTransactionError(
4872
'Recipient details are required to verify this transaction before signing. Pass txParams with at least one recipient.'
4973
);

modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import * as assert from 'assert';
55
const getModule = () => require('../../../../../src/bitgo/utils/tss/recipientUtils');
66

77
function makeTxRequest(
8-
intentRecipients?: { address: { address: string }; amount: { value: string }; data?: string }[]
8+
intentRecipients?: { address: { address: string }; amount: { value: string }; data?: string }[],
9+
intentType = 'payment'
910
): any {
1011
return {
1112
txRequestId: 'test-req-id',
12-
intent: intentRecipients ? { intentType: 'contractCall', recipients: intentRecipients } : { intentType: 'payment' },
13+
intent: intentRecipients ? { intentType: 'contractCall', recipients: intentRecipients } : { intentType },
1314
transactions: [],
1415
unsignedTxs: [],
1516
state: 'pendingUserSignature',
@@ -23,9 +24,10 @@ function makeTxRequest(
2324

2425
describe('recipientUtils', function () {
2526
describe('NO_RECIPIENT_TX_TYPES', function () {
26-
it('contains exactly the 8 expected exempted types', function () {
27+
it('contains all expected exempted types', function () {
2728
const { NO_RECIPIENT_TX_TYPES } = getModule();
2829
const expected = [
30+
// ECDSA
2931
'acceleration',
3032
'fillNonce',
3133
'transferToken',
@@ -34,6 +36,14 @@ describe('recipientUtils', function () {
3436
'bridgeFunds',
3537
'enableToken',
3638
'customTx',
39+
// EVM staking
40+
'stakingLock',
41+
'stakingVote',
42+
'stakingActivate',
43+
'stakingUnvote',
44+
'stakingUnlock',
45+
'stakingWithdraw',
46+
'stakingDeactivate',
3747
];
3848
expected.forEach((t) => assert.ok(NO_RECIPIENT_TX_TYPES.has(t), `${t} should be in NO_RECIPIENT_TX_TYPES`));
3949
assert.strictEqual(NO_RECIPIENT_TX_TYPES.size, expected.length);
@@ -105,8 +115,15 @@ describe('recipientUtils', function () {
105115
'tokenApproval',
106116
'consolidate',
107117
'bridgeFunds',
108-
'enableToken', // TSS wallets do not populate recipients for token enablement
109-
'customTx', // DeFi/WalletConnect smart contract interactions have no traditional recipients
118+
'enableToken',
119+
'customTx',
120+
'stakingLock',
121+
'stakingVote',
122+
'stakingActivate',
123+
'stakingUnvote',
124+
'stakingUnlock',
125+
'stakingWithdraw',
126+
'stakingDeactivate',
110127
];
111128

112129
NO_RECIPIENT_TYPES.forEach((type) => {
@@ -117,5 +134,27 @@ describe('recipientUtils', function () {
117134
result.type.should.equal(type);
118135
});
119136
});
137+
138+
it('allows empty recipients when intent.intentType is a staking type (no txParams passed)', function () {
139+
const { resolveEffectiveTxParams } = getModule();
140+
const txRequest = makeTxRequest(undefined, 'stakingVote');
141+
// Simulate staking wallet: signTransaction called with no txParams
142+
const result = resolveEffectiveTxParams(txRequest, undefined);
143+
result.type.should.equal('stakingVote');
144+
});
145+
146+
it('propagates intent.intentType into effectiveTxParams.type when txParams.type is absent', function () {
147+
const { resolveEffectiveTxParams } = getModule();
148+
const txRequest = makeTxRequest(undefined, 'stakingLock');
149+
const result = resolveEffectiveTxParams(txRequest, {});
150+
result.type.should.equal('stakingLock');
151+
});
152+
153+
it('does not override txParams.type when already set', function () {
154+
const { resolveEffectiveTxParams } = getModule();
155+
const txRequest = makeTxRequest(undefined, 'stakingVote');
156+
const result = resolveEffectiveTxParams(txRequest, { type: 'acceleration' });
157+
result.type.should.equal('acceleration');
158+
});
120159
});
121160
});

0 commit comments

Comments
 (0)