Skip to content

Commit 250f542

Browse files
committed
feat(abstract-eth): add recover consolidation for eth
ticket: WIN-5700
1 parent 5644b36 commit 250f542

File tree

2 files changed

+174
-1
lines changed

2 files changed

+174
-1
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 168 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,
@@ -55,7 +56,7 @@ import { BigNumber } from 'bignumber.js';
5556
import BN from 'bn.js';
5657
import { randomBytes } from 'crypto';
5758
import debugLib from 'debug';
58-
import { addHexPrefix, stripHexPrefix } from 'ethereumjs-util';
59+
import { addHexPrefix, stripHexPrefix, bufferToHex, setLengthLeft, toBuffer } from 'ethereumjs-util';
5960
import Keccak from 'keccak';
6061
import _ from 'lodash';
6162
import secp256k1 from 'secp256k1';
@@ -68,6 +69,7 @@ import {
6869
ERC721TransferBuilder,
6970
getBufferedByteCode,
7071
getCommon,
72+
getCreateForwarderParamsAndTypes,
7173
getProxyInitcode,
7274
getRawDecoded,
7375
getToken,
@@ -359,6 +361,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
359361
salt?: string;
360362
}
361363

364+
export const DEFAULT_SCAN_FACTOR = 20;
365+
export interface EthConsolidationRecoveryOptions {
366+
coinName: string;
367+
walletContractAddress?: string;
368+
apiKey: string;
369+
isTss: boolean;
370+
userKey: string;
371+
backupKey: string;
372+
walletPassphrase?: string;
373+
recoveryDestination: string;
374+
krsProvider?: string;
375+
gasPrice?: number;
376+
gasLimit?: number;
377+
eip1559?: EIP1559;
378+
replayProtectionOptions?: ReplayProtectionOptions;
379+
bitgoFeeAddress?: string;
380+
bitgoDestinationAddress?: string;
381+
tokenContractAddress?: string;
382+
intendedChain?: string;
383+
common?: EthLikeCommon.default;
384+
derivationSeed?: string;
385+
bitgoKey: string;
386+
startingScanIndex: number;
387+
endingScanIndex: number;
388+
ignoreAddressTypes?: unknown;
389+
}
390+
362391
export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
363392
baseAddress: string;
364393
coinSpecific: EthAddressCoinSpecifics;
@@ -1181,6 +1210,144 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
11811210
return this.recoverEthLike(params);
11821211
}
11831212

1213+
generateForwarderAddress(
1214+
baseAddress: string,
1215+
feeAddress: string,
1216+
forwarderFactoryAddress: string,
1217+
forwarderImplementationAddress: string,
1218+
index: number
1219+
): string {
1220+
const salt = addHexPrefix(index.toString(16));
1221+
const saltBuffer = setLengthLeft(toBuffer(salt), 32);
1222+
1223+
const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes(
1224+
baseAddress,
1225+
saltBuffer,
1226+
feeAddress
1227+
);
1228+
1229+
const calculationSalt = bufferToHex(optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams));
1230+
1231+
const initCode = getProxyInitcode(forwarderImplementationAddress);
1232+
return calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initCode);
1233+
}
1234+
1235+
deriveAddressFromPublicKey(publicKey: string): string {
1236+
const keyPair = new KeyPairLib({ pub: publicKey });
1237+
const address = keyPair.getAddress();
1238+
return address;
1239+
}
1240+
1241+
getConsolidationAddress(params: EthConsolidationRecoveryOptions, index: number): string {
1242+
if (params.walletContractAddress && params.bitgoFeeAddress) {
1243+
const ethNetwork = this.getNetwork();
1244+
const forwarderFactoryAddress = ethNetwork?.walletV4ForwarderFactoryAddress as string;
1245+
const forwarderImplementationAddress = ethNetwork?.walletV4ForwarderImplementationAddress as string;
1246+
try {
1247+
return this.generateForwarderAddress(
1248+
params.walletContractAddress,
1249+
params.bitgoFeeAddress,
1250+
forwarderFactoryAddress,
1251+
forwarderImplementationAddress,
1252+
index
1253+
);
1254+
} catch (e) {
1255+
console.log(`Failed to generate forwarder address: ${e.message}`);
1256+
}
1257+
}
1258+
1259+
if (params.userKey) {
1260+
try {
1261+
return this.deriveAddressFromPublicKey(params.userKey);
1262+
} catch (e) {
1263+
console.log(`Failed to generate derived address: ${e.message}`);
1264+
}
1265+
}
1266+
throw new Error(
1267+
'Unable to generate consolidation address. Check that wallet contract address, fee address, or user key is valid.'
1268+
);
1269+
}
1270+
1271+
async recoverConsolidations(params: EthConsolidationRecoveryOptions): Promise<Record<string, unknown> | undefined> {
1272+
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
1273+
const startIdx = params.startingScanIndex || 1;
1274+
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;
1275+
1276+
if (!params.walletContractAddress || params.walletContractAddress === '') {
1277+
throw new Error(`Invalid wallet contract address ${params.walletContractAddress}`);
1278+
}
1279+
1280+
if (!params.bitgoFeeAddress || params.bitgoFeeAddress === '') {
1281+
throw new Error(`Invalid fee address ${params.bitgoFeeAddress}`);
1282+
}
1283+
1284+
if (startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR) {
1285+
throw new Error(
1286+
`Invalid starting or ending index to scan for addresses. startingScanIndex: ${startIdx}, endingScanIndex: ${endIdx}.`
1287+
);
1288+
}
1289+
1290+
const consolidationTransactions: any[] = [];
1291+
let lastScanIndex = startIdx;
1292+
1293+
for (let i = startIdx; i < endIdx; i++) {
1294+
const consolidationAddress = this.getConsolidationAddress(params, i);
1295+
1296+
const recoverParams = {
1297+
apiKey: params.apiKey,
1298+
backupKey: params.backupKey,
1299+
gasLimit: params.gasLimit,
1300+
recoveryDestination: params.recoveryDestination,
1301+
userKey: params.userKey,
1302+
walletContractAddress: consolidationAddress,
1303+
derivationSeed: '',
1304+
isTss: params.isTss,
1305+
eip1559: {
1306+
maxFeePerGas: params.eip1559?.maxFeePerGas || 20,
1307+
maxPriorityFeePerGas: params.eip1559?.maxPriorityFeePerGas || 200000,
1308+
},
1309+
replayProtectionOptions: {
1310+
chain: params.replayProtectionOptions?.chain || 0,
1311+
hardfork: params.replayProtectionOptions?.hardfork || 'london',
1312+
},
1313+
bitgoKey: '',
1314+
ignoreAddressTypes: [],
1315+
};
1316+
1317+
let recoveryTransaction;
1318+
try {
1319+
recoveryTransaction = await this.recover(recoverParams);
1320+
} catch (e) {
1321+
if (
1322+
e.message === 'Did not find address with funds to recover' ||
1323+
e.message === 'Did not find token account to recover tokens, please check token account' ||
1324+
e.message === 'Not enough token funds to recover'
1325+
) {
1326+
lastScanIndex = i;
1327+
continue;
1328+
}
1329+
throw e;
1330+
}
1331+
1332+
if (isUnsignedSweep) {
1333+
consolidationTransactions.push((recoveryTransaction as MPCSweepTxs).txRequests[0]);
1334+
} else {
1335+
consolidationTransactions.push(recoveryTransaction);
1336+
}
1337+
1338+
lastScanIndex = i;
1339+
}
1340+
1341+
if (consolidationTransactions.length === 0) {
1342+
throw new Error(
1343+
`Did not find an address with sufficient funds to recover. Please start the next scan at address index ${
1344+
lastScanIndex + 1
1345+
}.`
1346+
);
1347+
}
1348+
return { transactions: consolidationTransactions, lastScanIndex };
1349+
}
1350+
11841351
/**
11851352
* Builds a funds recovery transaction without BitGo for non-TSS transaction
11861353
* @param params

modules/statics/src/networks.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export interface EthereumNetwork extends AccountNetwork {
120120
readonly forwarderImplementationAddress?: string;
121121
readonly nativeCoinOperationHashPrefix?: string;
122122
readonly tokenOperationHashPrefix?: string;
123+
readonly walletV4ForwarderFactoryAddress?: string;
124+
readonly walletV4ForwarderImplementationAddress?: string;
123125
}
124126

125127
export interface TronNetwork extends AccountNetwork {
@@ -544,6 +546,8 @@ class Ethereum extends Mainnet implements EthereumNetwork {
544546
forwarderImplementationAddress = '0x059ffafdc6ef594230de44f824e2bd0a51ca5ded';
545547
nativeCoinOperationHashPrefix = 'ETHER';
546548
tokenOperationHashPrefix = 'ERC20';
549+
walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a';
550+
walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b';
547551
}
548552

549553
class Ethereum2 extends Mainnet implements AccountNetwork {
@@ -615,6 +619,8 @@ class Holesky extends Testnet implements EthereumNetwork {
615619
forwarderImplementationAddress = '0x059ffafdc6ef594230de44f824e2bd0a51ca5ded';
616620
nativeCoinOperationHashPrefix = 'ETHER';
617621
tokenOperationHashPrefix = 'ERC20';
622+
walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a';
623+
walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b';
618624
}
619625

620626
class Hoodi extends Testnet implements EthereumNetwork {

0 commit comments

Comments
 (0)