Skip to content

Commit acd7712

Browse files
fix: updating recovery tx building for EIP-155 txs
Ticket: WIN-6260
1 parent aae1787 commit acd7712

File tree

6 files changed

+48
-41
lines changed

6 files changed

+48
-41
lines changed

modules/abstract-eth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@bitgo/statics": "^55.3.0",
4747
"@ethereumjs/common": "^2.6.5",
4848
"@ethereumjs/tx": "^3.3.0",
49+
"@ethereumjs/rlp": "^4.0.0",
4950
"@metamask/eth-sig-util": "^5.0.2",
5051
"bignumber.js": "^9.1.1",
5152
"bn.js": "^5.2.1",

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
4242
import { bip32 } from '@bitgo/secp256k1';
4343
import {
4444
BaseCoin as StaticsBaseCoin,
45+
CoinFeature,
4546
CoinMap,
4647
coins,
4748
EthereumNetwork as EthLikeNetwork,
@@ -50,12 +51,13 @@ import {
5051
import type * as EthLikeCommon from '@ethereumjs/common';
5152
import type * as EthLikeTxLib from '@ethereumjs/tx';
5253
import { FeeMarketEIP1559Transaction, Transaction as LegacyTransaction } from '@ethereumjs/tx';
54+
import { RLP } from '@ethereumjs/rlp';
5355
import { SignTypedDataVersion, TypedDataUtils, TypedMessage } from '@metamask/eth-sig-util';
5456
import { BigNumber } from 'bignumber.js';
5557
import BN from 'bn.js';
5658
import { randomBytes } from 'crypto';
5759
import debugLib from 'debug';
58-
import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
60+
import { addHexPrefix, bufArrToArr, stripHexPrefix } from 'ethereumjs-util';
5961
import Keccak from 'keccak';
6062
import _ from 'lodash';
6163
import secp256k1 from 'secp256k1';
@@ -482,8 +484,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
482484
replayProtectionOptions?: ReplayProtectionOptions
483485
): EthLikeCommon.default {
484486
// if eip1559 params are specified, default to london hardfork, otherwise,
485-
// default to tangerine whistle to avoid replay protection issues
486-
const defaultHardfork = !!eip1559 ? 'london' : optionalDeps.EthCommon.Hardfork.TangerineWhistle;
487+
// default to petersburg to avoid replay protection issues
488+
const defaultHardfork = !!eip1559 ? 'london' : optionalDeps.EthCommon.Hardfork.Petersburg;
487489
const ethLikeCommon = AbstractEthLikeNewCoins.getCustomChainCommon(replayProtectionOptions?.chain as number);
488490
ethLikeCommon.setHardfork(replayProtectionOptions?.hardfork ?? defaultHardfork);
489491
return ethLikeCommon;
@@ -1109,6 +1111,15 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11091111
if (params.recoveryDestination === undefined || !this.isValidAddress(params.recoveryDestination)) {
11101112
throw new Error('invalid recoveryDestination');
11111113
}
1114+
1115+
if (!this.staticsCoin?.features.includes(CoinFeature.EIP1559)) {
1116+
if (params.eip1559) {
1117+
throw new Error('Invalid fee params. EIP1559 not supported');
1118+
}
1119+
if (params.replayProtectionOptions?.hardfork === 'london') {
1120+
throw new Error('Invalid replayProtection options. Cannot use the hardfork "london" for this chain');
1121+
}
1122+
}
11121123
}
11131124

11141125
/**
@@ -2034,7 +2045,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
20342045
let lastScanIndex = 0;
20352046

20362047
for (let i = 0; i < req.length; i++) {
2037-
const MPC = new Ecdsa();
20382048
const transaction = req[i]?.txRequest?.transactions?.[0]?.unsignedTx as unknown as UnsignedTransactionTss;
20392049
if (!req[i].ovc || !req[i].ovc[0].ecdsaSignature) {
20402050
throw new Error('Missing signature(s)');
@@ -2056,9 +2066,6 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
20562066
s: shares[2],
20572067
y: shares[3],
20582068
} as unknown as ECDSAMethodTypes.Signature;
2059-
const signatureHex = Buffer.from(signature.toString(), 'hex');
2060-
const txBuilder = this.getTransactionBuilder(getCommon(this.getNetwork() as EthLikeNetwork));
2061-
txBuilder.from(transaction.serializedTxHex as string);
20622069

20632070
if (!transaction.coinSpecific?.commonKeyChain) {
20642071
throw new Error(`Missing common keychain for transaction at index ${i}`);
@@ -2071,29 +2078,23 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
20712078
throw new Error(`Missing common key chain for transaction at index ${i}`);
20722079
}
20732080

2074-
const derivationPath = transaction.derivationPath ?? 'm/0';
2075-
const derivedCommonKeyChain = MPC.deriveUnhardened(String(commonKeyChain), String(derivationPath));
2076-
const derivedPublicKey = new KeyPairLib({ pub: derivedCommonKeyChain.slice(0, 66) });
2077-
txBuilder.addSignature({ pub: derivedPublicKey.getKeys().pub }, signatureHex);
20782081
const ethCommmon = AbstractEthLikeNewCoins.getEthLikeCommon(
20792082
transaction.eip1559,
20802083
transaction.replayProtectionOptions
20812084
);
2082-
let unsignedTx;
2085+
let unsignedTx: EthLikeTxLib.FeeMarketEIP1559Transaction | EthLikeTxLib.Transaction;
20832086
if (transaction.eip1559) {
2084-
unsignedTx = await FeeMarketEIP1559Transaction.fromSerializedTx(
2085-
Buffer.from(transaction.serializedTxHex, 'hex')
2086-
);
2087+
unsignedTx = FeeMarketEIP1559Transaction.fromSerializedTx(Buffer.from(transaction.serializedTxHex, 'hex'));
20872088
} else {
2088-
unsignedTx = await LegacyTransaction.fromSerializedTx(Buffer.from(transaction.serializedTxHex, 'hex'));
2089+
unsignedTx = LegacyTransaction.fromSerializedTx(Buffer.from(transaction.serializedTxHex, 'hex'));
20892090
}
20902091
const signedTx = this.getSignedTxFromSignature(ethCommmon, unsignedTx, finalSignature);
20912092
broadcastableTransactions.push({
20922093
serializedTx: addHexPrefix(signedTx.serialize().toString('hex')),
20932094
});
20942095

2095-
if (i === req.length - 1 && transaction.coinSpecific!.lastScanIndex) {
2096-
lastScanIndex = transaction.coinSpecific!.lastScanIndex as number;
2096+
if (i === req.length - 1 && transaction.coinSpecific?.lastScanIndex) {
2097+
lastScanIndex = transaction.coinSpecific?.lastScanIndex as number;
20972098
}
20982099
}
20992100

@@ -2112,11 +2113,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
21122113
if (!_.isUndefined(params.derivationSeed) && typeof params.derivationSeed !== 'string') {
21132114
throw new Error('invalid derivationSeed');
21142115
}
2115-
if (
2116-
_.isUndefined(params.bitgoDestinationAddress) ||
2117-
typeof params.bitgoDestinationAddress !== 'string' ||
2118-
!this.isValidAddress(params.bitgoDestinationAddress)
2119-
) {
2116+
if (_.isUndefined(params.recoveryDestination) || !this.isValidAddress(params.recoveryDestination)) {
21202117
throw new Error('missing or invalid destinationAddress');
21212118
}
21222119
}
@@ -2125,17 +2122,19 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
21252122
* Helper function for recover()
21262123
* This transforms the unsigned transaction information into a format the BitGo offline vault expects
21272124
* @param {UnformattedTxInfo} txInfo - tx info
2128-
* @param {EthLikeTxLib.Transaction | EthLikeTxLib.FeeMarketEIP1559Transaction} ethTx - the ethereumjs tx object
2125+
* @param {LegacyTransaction | FeeMarketEIP1559Transaction} ethTx - the ethereumjs tx object
21292126
* @param {string} derivationPath - the derivationPath
21302127
* @param {number} nonce - the nonce of the backup key address
21312128
* @param {Buffer} gasPrice - gas price for the tx
21322129
* @param {number} gasLimit - gas limit for the tx
21332130
* @param {EIP1559} eip1559 - eip1559 params
2131+
* @param replayProtectionOptions
2132+
* @param commonKeyChain
21342133
* @returns {Promise<OfflineVaultTxInfo>}
21352134
*/
21362135
private buildTxRequestForOfflineVaultMPCv2(
21372136
txInfo: UnformattedTxInfo,
2138-
ethTx: EthLikeTxLib.Transaction | EthLikeTxLib.FeeMarketEIP1559Transaction,
2137+
ethTx: LegacyTransaction | FeeMarketEIP1559Transaction,
21392138
derivationPath: string,
21402139
nonce: number,
21412140
gasPrice: Buffer,
@@ -2154,7 +2153,10 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
21542153

21552154
const unsignedTx: UnsignedTransactionTss = {
21562155
serializedTxHex: ethTx.serialize().toString('hex'),
2157-
signableHex: ethTx.getMessageToSign(false).toString('hex'),
2156+
signableHex:
2157+
ethTx instanceof FeeMarketEIP1559Transaction
2158+
? ethTx.getMessageToSign(false).toString('hex')
2159+
: Buffer.from(RLP.encode(bufArrToArr(ethTx.getMessageToSign(false)))).toString('hex'),
21582160
derivationPath: derivationPath,
21592161
feeInfo: {
21602162
fee: fee,
@@ -2194,8 +2196,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
21942196
}
21952197

21962198
private async buildTssRecoveryTxn(baseAddress: string, gasPrice: any, gasLimit: any, params: RecoverOptions) {
2197-
const nonce = await this.getAddressNonce(baseAddress, params.apiKey);
21982199
const txAmount = await this.validateBalanceAndGetTxAmount(baseAddress, gasPrice, gasLimit, params.apiKey);
2200+
const nonce = await this.getAddressNonce(baseAddress, params.apiKey);
21992201
const recipients = [
22002202
{
22012203
address: params.recoveryDestination,

modules/bitgo/test/v2/unit/recovery.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,14 +1383,19 @@ describe('Recovery:', function () {
13831383
walletPassphrase: ethLikeDKLSKeycard.walletPassphrase,
13841384
eip1559: {
13851385
maxPriorityFeePerGas: 3,
1386-
maxFeePerGas: 20,
1386+
maxFeePerGas: 20000000000,
13871387
},
13881388
isTss: true,
13891389
replayProtectionOptions: {
13901390
chain: chain,
13911391
},
13921392
};
13931393

1394+
if (coin === 'tbsc') {
1395+
recoveryParams.eip1559 = undefined;
1396+
recoveryParams['gasPrice'] = 20000000000;
1397+
}
1398+
13941399
const recovery = await basecoin.recover(recoveryParams);
13951400

13961401
should.exist(recovery);
@@ -1404,7 +1409,7 @@ describe('Recovery:', function () {
14041409
finalTx.common.chainIdBN().toNumber().should.equal(chain);
14051410
baseAddress.should.equal(senderAddress);
14061411
recoveryParams.recoveryDestination.should.equal(finalTx.to?.toString());
1407-
Number(finalTx.value).should.equal(999999999990000000);
1412+
Number(finalTx.value).should.equal(990000000000000000);
14081413
}
14091414
});
14101415

modules/sdk-coin-ethw/test/unit/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ describe('Ethereum pow', function () {
5151
recoveryDestination: '0xac05da78464520aa7c9d4c19bd7a440b111b3054',
5252
replayProtectionOptions: {
5353
chain: 10001,
54-
hardfork: 'london',
5554
},
5655
};
5756
});

modules/sdk-coin-xdc/test/unit/xdc.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { UnsignedSweepTxMPCv2 } from '@bitgo/abstract-eth';
88
import { mockDataUnsignedSweep, mockDataNonBitGoRecovery } from '../resources';
99
import nock from 'nock';
1010
import { common } from '@bitgo/sdk-core';
11-
import { FeeMarketEIP1559Transaction } from '@ethereumjs/tx';
11+
import { Transaction } from '@ethereumjs/tx';
1212
import { stripHexPrefix } from '@ethereumjs/util';
1313

1414
const bitgo: TestBitGoAPI = TestBitGo.decorate(BitGoAPI, { env: 'test' });
@@ -49,9 +49,9 @@ describe('xdc', function () {
4949

5050
describe('Build Unsigned Sweep for Self-Custody Cold Wallets - (MPCv2)', function () {
5151
const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
52+
bitgo.register('txdc', Txdc.createInstance);
5253
const explorerUrl = common.Environments[bitgo.getEnv()].xdcExplorerBaseUrl as string;
53-
const maxFeePerGasvalue = 20000000000;
54-
const maxPriorityFeePerGasValue = 10000000000;
54+
const gasPrice = 20000000000;
5555
const gasLimitValue = 500000;
5656
const chain_id = 51;
5757

@@ -73,11 +73,11 @@ describe('Build Unsigned Sweep for Self-Custody Cold Wallets - (MPCv2)', functio
7373
walletContractAddress: mockDataUnsignedSweep.walletBaseAddress,
7474
recoveryDestination: mockDataUnsignedSweep.recoveryDestination,
7575
isTss: true,
76-
eip1559: { maxFeePerGas: maxFeePerGasvalue, maxPriorityFeePerGas: maxPriorityFeePerGasValue },
76+
gasPrice: gasPrice,
7777
gasLimit: gasLimitValue,
7878
replayProtectionOptions: {
7979
chain: chain_id,
80-
hardfork: 'london',
80+
hardfork: 'petersburg',
8181
},
8282
})) as UnsignedSweepTxMPCv2;
8383
should.exist(transaction);
@@ -105,9 +105,9 @@ describe('Build Unsigned Sweep for Self-Custody Cold Wallets - (MPCv2)', functio
105105

106106
describe('Non Bitgo Recovery for Hot Wallets', function () {
107107
const bitgo = TestBitGo.decorate(BitGoAPI, { env: 'test' });
108+
bitgo.register('txdc', Txdc.createInstance);
108109
const explorerUrl = common.Environments[bitgo.getEnv()].xdcExplorerBaseUrl as string;
109-
const maxFeePerGasvalue = 20000000000;
110-
const maxPriorityFeePerGasValue = 10000000000;
110+
const gasPrice = 20000000000;
111111
const chain_id = 51;
112112
const gasLimitvalue = 500000;
113113

@@ -130,17 +130,17 @@ describe('Non Bitgo Recovery for Hot Wallets', function () {
130130
walletPassphrase: mockDataNonBitGoRecovery.walletPassphrase,
131131
recoveryDestination: mockDataNonBitGoRecovery.recoveryDestination,
132132
isTss: true,
133-
eip1559: { maxFeePerGas: maxFeePerGasvalue, maxPriorityFeePerGas: maxPriorityFeePerGasValue },
133+
gasPrice: gasPrice,
134134
gasLimit: gasLimitvalue,
135135
replayProtectionOptions: {
136136
chain: chain_id,
137-
hardfork: 'london',
137+
hardfork: 'petersburg',
138138
},
139139
});
140140
should.exist(transaction);
141141
transaction.should.have.property('id');
142142
transaction.should.have.property('tx');
143-
const tx = FeeMarketEIP1559Transaction.fromSerializedTx(Buffer.from(stripHexPrefix(transaction.tx), 'hex'));
143+
const tx = Transaction.fromSerializedTx(Buffer.from(stripHexPrefix(transaction.tx), 'hex'));
144144
tx.getSenderAddress().toString().should.equal(mockDataNonBitGoRecovery.walletRootAddress);
145145
const jsonTx = tx.toJSON();
146146
jsonTx.to?.should.equal(mockDataNonBitGoRecovery.recoveryDestination);

yarn.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1764,7 +1764,7 @@
17641764
resolved "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.0.tgz#dd81b32b2237bc32fb1b54534f8ff246a6c89d9b"
17651765
integrity sha512-WuS1l7GJmB0n0HsXLozCoEFc9IwYgf3l0gCkKVYgR67puVF1O4OpEaN0hWmm1c+iHUHFCKt1hJrvy5toLg+6ag==
17661766

1767-
"@ethereumjs/rlp@^4.0.0-beta.2":
1767+
"@ethereumjs/rlp@^4.0.0", "@ethereumjs/rlp@^4.0.0-beta.2":
17681768
version "4.0.1"
17691769
resolved "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41"
17701770
integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==

0 commit comments

Comments
 (0)