Skip to content

Commit 4851e67

Browse files
committed
feat(abstract-eth): add recover consolidation for eth
ticket: WIN-5700
1 parent 685933b commit 4851e67

File tree

4 files changed

+373
-1
lines changed

4 files changed

+373
-1
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
IWallet,
2020
KeyPair,
2121
MPCSweepRecoveryOptions,
22+
MPCSweepTxs,
2223
MPCTx,
2324
MPCTxs,
2425
ParsedTransaction,
@@ -57,7 +58,7 @@ import { BigNumber } from 'bignumber.js';
5758
import BN from 'bn.js';
5859
import { randomBytes } from 'crypto';
5960
import debugLib from 'debug';
60-
import { addHexPrefix, bufArrToArr, stripHexPrefix } from 'ethereumjs-util';
61+
import { addHexPrefix, bufArrToArr, stripHexPrefix, bufferToHex, setLengthLeft, toBuffer } from 'ethereumjs-util';
6162
import Keccak from 'keccak';
6263
import _ from 'lodash';
6364
import secp256k1 from 'secp256k1';
@@ -70,6 +71,7 @@ import {
7071
ERC721TransferBuilder,
7172
getBufferedByteCode,
7273
getCommon,
74+
getCreateForwarderParamsAndTypes,
7375
getProxyInitcode,
7476
getRawDecoded,
7577
getToken,
@@ -224,6 +226,11 @@ export type UnsignedSweepTxMPCv2 = {
224226
}[];
225227
};
226228

229+
export type UnsignedBuilConsolidation = {
230+
transactions: MPCSweepTxs[] | UnsignedSweepTxMPCv2[] | RecoveryInfo[] | OfflineVaultTxInfo[];
231+
lastScanIndex: number;
232+
};
233+
227234
export type RecoverOptionsWithBytes = {
228235
isTss: true;
229236
/**
@@ -361,6 +368,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
361368
salt?: string;
362369
}
363370

371+
export const DEFAULT_SCAN_FACTOR = 20;
372+
export interface EthConsolidationRecoveryOptions {
373+
coinName?: string;
374+
walletContractAddress?: string;
375+
apiKey?: string;
376+
isTss?: boolean;
377+
userKey?: string;
378+
backupKey?: string;
379+
walletPassphrase?: string;
380+
recoveryDestination?: string;
381+
krsProvider?: string;
382+
gasPrice?: number;
383+
gasLimit?: number;
384+
eip1559?: EIP1559;
385+
replayProtectionOptions?: ReplayProtectionOptions;
386+
bitgoFeeAddress?: string;
387+
bitgoDestinationAddress?: string;
388+
tokenContractAddress?: string;
389+
intendedChain?: string;
390+
common?: EthLikeCommon.default;
391+
derivationSeed?: string;
392+
bitgoKey?: string;
393+
startingScanIndex?: number;
394+
endingScanIndex?: number;
395+
ignoreAddressTypes?: unknown;
396+
}
397+
364398
export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
365399
baseAddress: string;
366400
coinSpecific: EthAddressCoinSpecifics;
@@ -1192,6 +1226,161 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11921226
return this.recoverEthLike(params);
11931227
}
11941228

1229+
generateForwarderAddress(
1230+
baseAddress: string,
1231+
feeAddress: string,
1232+
forwarderFactoryAddress: string,
1233+
forwarderImplementationAddress: string,
1234+
index: number
1235+
): string {
1236+
const salt = addHexPrefix(index.toString(16));
1237+
const saltBuffer = setLengthLeft(toBuffer(salt), 32);
1238+
1239+
const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes(
1240+
baseAddress,
1241+
saltBuffer,
1242+
feeAddress
1243+
);
1244+
1245+
const calculationSalt = bufferToHex(optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams));
1246+
1247+
const initCode = getProxyInitcode(forwarderImplementationAddress);
1248+
return calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initCode);
1249+
}
1250+
1251+
deriveAddressFromPublicKey(commonKeychain: string, index: number): string {
1252+
const derivationPath = `m/${index}`;
1253+
const pubkeySize = 33;
1254+
1255+
const ecdsaMpc = new Ecdsa();
1256+
const derivedPublicKey = Buffer.from(ecdsaMpc.deriveUnhardened(commonKeychain, derivationPath), 'hex')
1257+
.subarray(0, pubkeySize)
1258+
.toString('hex');
1259+
1260+
const publicKey = Buffer.from(derivedPublicKey, 'hex').slice(0, 66).toString('hex');
1261+
1262+
const keyPair = new KeyPairLib({ pub: publicKey });
1263+
const address = keyPair.getAddress();
1264+
return address;
1265+
}
1266+
1267+
getConsolidationAddress(params: EthConsolidationRecoveryOptions, index: number): string[] {
1268+
const possibleConsolidationAddresses: string[] = [];
1269+
if (params.walletContractAddress && params.bitgoFeeAddress) {
1270+
const ethNetwork = this.getNetwork();
1271+
const forwarderFactoryAddress = ethNetwork?.walletV4ForwarderFactoryAddress as string;
1272+
const forwarderImplementationAddress = ethNetwork?.walletV4ForwarderImplementationAddress as string;
1273+
try {
1274+
const forwarderAddress = this.generateForwarderAddress(
1275+
params.walletContractAddress,
1276+
params.bitgoFeeAddress,
1277+
forwarderFactoryAddress,
1278+
forwarderImplementationAddress,
1279+
index
1280+
);
1281+
possibleConsolidationAddresses.push(forwarderAddress);
1282+
} catch (e) {
1283+
console.log(`Failed to generate forwarder address: ${e.message}`);
1284+
}
1285+
}
1286+
1287+
if (params.userKey) {
1288+
try {
1289+
const derivedAddress = this.deriveAddressFromPublicKey(params.userKey, index);
1290+
possibleConsolidationAddresses.push(derivedAddress);
1291+
} catch (e) {
1292+
console.log(`Failed to generate derived address: ${e}`);
1293+
}
1294+
}
1295+
1296+
if (possibleConsolidationAddresses.length === 0) {
1297+
throw new Error(
1298+
'Unable to generate consolidation address. Check that wallet contract address, fee address, or user key is valid.'
1299+
);
1300+
}
1301+
return possibleConsolidationAddresses;
1302+
}
1303+
1304+
async recoverConsolidations(params: EthConsolidationRecoveryOptions): Promise<UnsignedBuilConsolidation> {
1305+
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
1306+
const startIdx = params.startingScanIndex || 1;
1307+
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;
1308+
1309+
if (!params.walletContractAddress || params.walletContractAddress === '') {
1310+
throw new Error(`Invalid wallet contract address ${params.walletContractAddress}`);
1311+
}
1312+
1313+
if (!params.bitgoFeeAddress || params.bitgoFeeAddress === '') {
1314+
throw new Error(`Invalid fee address ${params.bitgoFeeAddress}`);
1315+
}
1316+
1317+
if (startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR) {
1318+
throw new Error(
1319+
`Invalid starting or ending index to scan for addresses. startingScanIndex: ${startIdx}, endingScanIndex: ${endIdx}.`
1320+
);
1321+
}
1322+
1323+
const consolidatedTransactions: any[] = [];
1324+
let lastScanIndex = startIdx;
1325+
1326+
for (let i = startIdx; i < endIdx; i++) {
1327+
const consolidationAddress = this.getConsolidationAddress(params, i);
1328+
for (const address of consolidationAddress) {
1329+
const recoverParams = {
1330+
apiKey: params.apiKey,
1331+
backupKey: params.backupKey || '',
1332+
gasLimit: params.gasLimit,
1333+
recoveryDestination: params.recoveryDestination || '',
1334+
userKey: params.userKey || '',
1335+
walletContractAddress: address,
1336+
derivationSeed: '',
1337+
isTss: params.isTss,
1338+
eip1559: {
1339+
maxFeePerGas: params.eip1559?.maxFeePerGas || 20,
1340+
maxPriorityFeePerGas: params.eip1559?.maxPriorityFeePerGas || 200000,
1341+
},
1342+
replayProtectionOptions: {
1343+
chain: params.replayProtectionOptions?.chain || 0,
1344+
hardfork: params.replayProtectionOptions?.hardfork || 'london',
1345+
},
1346+
bitgoKey: '',
1347+
ignoreAddressTypes: [],
1348+
};
1349+
let recoveryTransaction;
1350+
try {
1351+
recoveryTransaction = await this.recover(recoverParams);
1352+
} catch (e) {
1353+
if (
1354+
e.message === 'Did not find address with funds to recover' ||
1355+
e.message === 'Did not find token account to recover tokens, please check token account' ||
1356+
e.message === 'Not enough token funds to recover'
1357+
) {
1358+
lastScanIndex = i;
1359+
continue;
1360+
}
1361+
throw e;
1362+
}
1363+
if (isUnsignedSweep) {
1364+
consolidatedTransactions.push((recoveryTransaction as MPCSweepTxs).txRequests[0]);
1365+
} else {
1366+
consolidatedTransactions.push(recoveryTransaction);
1367+
}
1368+
}
1369+
// To avoid rate limit for etherscan
1370+
await new Promise((resolve) => setTimeout(resolve, 1000));
1371+
// lastScanIndex = i;
1372+
}
1373+
1374+
if (consolidatedTransactions.length === 0) {
1375+
throw new Error(
1376+
`Did not find an address with sufficient funds to recover. Please start the next scan at address index ${
1377+
lastScanIndex + 1
1378+
}.`
1379+
);
1380+
}
1381+
return { transactions: consolidatedTransactions, lastScanIndex };
1382+
}
1383+
11951384
/**
11961385
* Builds a funds recovery transaction without BitGo for non-TSS transaction
11971386
* @param params

0 commit comments

Comments
 (0)