Skip to content

feat(abstract-eth): add recover consolidation for eth #6550

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 190 additions & 1 deletion modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
IWallet,
KeyPair,
MPCSweepRecoveryOptions,
MPCSweepTxs,
MPCTx,
MPCTxs,
ParsedTransaction,
Expand Down Expand Up @@ -57,7 +58,7 @@ import { BigNumber } from 'bignumber.js';
import BN from 'bn.js';
import { randomBytes } from 'crypto';
import debugLib from 'debug';
import { addHexPrefix, bufArrToArr, stripHexPrefix } from 'ethereumjs-util';
import { addHexPrefix, bufArrToArr, stripHexPrefix, bufferToHex, setLengthLeft, toBuffer } from 'ethereumjs-util';
import Keccak from 'keccak';
import _ from 'lodash';
import secp256k1 from 'secp256k1';
Expand All @@ -70,6 +71,7 @@ import {
ERC721TransferBuilder,
getBufferedByteCode,
getCommon,
getCreateForwarderParamsAndTypes,
getProxyInitcode,
getRawDecoded,
getToken,
Expand Down Expand Up @@ -224,6 +226,11 @@ export type UnsignedSweepTxMPCv2 = {
}[];
};

export type UnsignedBuilConsolidation = {
transactions: MPCSweepTxs[] | UnsignedSweepTxMPCv2[] | RecoveryInfo[] | OfflineVaultTxInfo[];
lastScanIndex: number;
};

export type RecoverOptionsWithBytes = {
isTss: true;
/**
Expand Down Expand Up @@ -361,6 +368,33 @@ interface EthAddressCoinSpecifics extends AddressCoinSpecific {
salt?: string;
}

export const DEFAULT_SCAN_FACTOR = 20;
export interface EthConsolidationRecoveryOptions {
coinName?: string;
walletContractAddress?: string;
apiKey?: string;
isTss?: boolean;
userKey?: string;
backupKey?: string;
walletPassphrase?: string;
recoveryDestination?: string;
krsProvider?: string;
gasPrice?: number;
gasLimit?: number;
eip1559?: EIP1559;
replayProtectionOptions?: ReplayProtectionOptions;
bitgoFeeAddress?: string;
bitgoDestinationAddress?: string;
tokenContractAddress?: string;
intendedChain?: string;
common?: EthLikeCommon.default;
derivationSeed?: string;
bitgoKey?: string;
startingScanIndex?: number;
endingScanIndex?: number;
ignoreAddressTypes?: unknown;
}

export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
baseAddress: string;
coinSpecific: EthAddressCoinSpecifics;
Expand Down Expand Up @@ -1192,6 +1226,161 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
return this.recoverEthLike(params);
}

generateForwarderAddress(
baseAddress: string,
feeAddress: string,
forwarderFactoryAddress: string,
forwarderImplementationAddress: string,
index: number
): string {
const salt = addHexPrefix(index.toString(16));
const saltBuffer = setLengthLeft(toBuffer(salt), 32);

const { createForwarderParams, createForwarderTypes } = getCreateForwarderParamsAndTypes(
Copy link
Contributor

Choose a reason for hiding this comment

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

this is specific for v4 wallet, can you not make it generic ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since we are not sure which version is being used, we have kept this in a try statement in order to handle the failure

baseAddress,
saltBuffer,
feeAddress
);

const calculationSalt = bufferToHex(optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams));

const initCode = getProxyInitcode(forwarderImplementationAddress);
return calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initCode);
}

deriveAddressFromPublicKey(commonKeychain: string, index: number): string {
const derivationPath = `m/${index}`;
const pubkeySize = 33;

const ecdsaMpc = new Ecdsa();
const derivedPublicKey = Buffer.from(ecdsaMpc.deriveUnhardened(commonKeychain, derivationPath), 'hex')
.subarray(0, pubkeySize)
.toString('hex');

const publicKey = Buffer.from(derivedPublicKey, 'hex').slice(0, 66).toString('hex');

const keyPair = new KeyPairLib({ pub: publicKey });
const address = keyPair.getAddress();
return address;
}

getConsolidationAddress(params: EthConsolidationRecoveryOptions, index: number): string[] {
const possibleConsolidationAddresses: string[] = [];
if (params.walletContractAddress && params.bitgoFeeAddress) {
const ethNetwork = this.getNetwork();
const forwarderFactoryAddress = ethNetwork?.walletV4ForwarderFactoryAddress as string;
const forwarderImplementationAddress = ethNetwork?.walletV4ForwarderImplementationAddress as string;
try {
const forwarderAddress = this.generateForwarderAddress(
params.walletContractAddress,
params.bitgoFeeAddress,
forwarderFactoryAddress,
forwarderImplementationAddress,
index
);
possibleConsolidationAddresses.push(forwarderAddress);
} catch (e) {
console.log(`Failed to generate forwarder address: ${e.message}`);
Copy link
Preview

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

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

Using console.log for error logging is not appropriate for production code. Consider using a proper logging framework or throwing/handling the error appropriately.

Suggested change
console.log(`Failed to generate forwarder address: ${e.message}`);
debug(`Failed to generate forwarder address: ${e instanceof Error ? e.message : e}`);

Copilot uses AI. Check for mistakes.

}
}

if (params.userKey) {
try {
const derivedAddress = this.deriveAddressFromPublicKey(params.userKey, index);
Copy link
Contributor

Choose a reason for hiding this comment

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

derivedAddress is returned for every getConsolidationAddress call, why is that ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't have any means to differentiate between v6 and v5 wallet thus we try to derive address every time.

Copy link
Contributor

Choose a reason for hiding this comment

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

in that case this would execute for v6 wallet as well right ?
const forwarderAddress = this.generateForwarderAddress( params.walletContractAddress, params.bitgoFeeAddress, forwarderFactoryAddress, forwarderImplementationAddress, index ); possibleConsolidationAddresses.push(forwarderAddress);

possibleConsolidationAddresses.push(derivedAddress);
} catch (e) {
console.log(`Failed to generate derived address: ${e}`);
Copy link
Preview

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

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

Using console.log for error logging is not appropriate for production code. Consider using a proper logging framework or throwing/handling the error appropriately.

Suggested change
console.log(`Failed to generate derived address: ${e}`);
debug(`Failed to generate derived address: ${e}`);

Copilot uses AI. Check for mistakes.

}
}

if (possibleConsolidationAddresses.length === 0) {
throw new Error(
'Unable to generate consolidation address. Check that wallet contract address, fee address, or user key is valid.'
);
}
return possibleConsolidationAddresses;
}

async recoverConsolidations(params: EthConsolidationRecoveryOptions): Promise<UnsignedBuilConsolidation> {
const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;
const startIdx = params.startingScanIndex || 1;
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;

if (!params.walletContractAddress || params.walletContractAddress === '') {
throw new Error(`Invalid wallet contract address ${params.walletContractAddress}`);
}

if (!params.bitgoFeeAddress || params.bitgoFeeAddress === '') {
throw new Error(`Invalid fee address ${params.bitgoFeeAddress}`);
}

if (startIdx < 1 || endIdx <= startIdx || endIdx - startIdx > 10 * DEFAULT_SCAN_FACTOR) {
throw new Error(
`Invalid starting or ending index to scan for addresses. startingScanIndex: ${startIdx}, endingScanIndex: ${endIdx}.`
);
}

const consolidatedTransactions: any[] = [];
let lastScanIndex = startIdx;

for (let i = startIdx; i < endIdx; i++) {
const consolidationAddress = this.getConsolidationAddress(params, i);
for (const address of consolidationAddress) {
const recoverParams = {
apiKey: params.apiKey,
backupKey: params.backupKey || '',
gasLimit: params.gasLimit,
recoveryDestination: params.recoveryDestination || '',
userKey: params.userKey || '',
walletContractAddress: address,
derivationSeed: '',
isTss: params.isTss,
eip1559: {
maxFeePerGas: params.eip1559?.maxFeePerGas || 20,
maxPriorityFeePerGas: params.eip1559?.maxPriorityFeePerGas || 200000,
},
replayProtectionOptions: {
chain: params.replayProtectionOptions?.chain || 0,
hardfork: params.replayProtectionOptions?.hardfork || 'london',
},
bitgoKey: '',
Copy link
Preview

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

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

Setting bitgoKey to an empty string may be problematic. Consider using a proper default value or making this parameter optional if it's not required.

Suggested change
bitgoKey: '',
...(params.bitgoKey !== undefined ? { bitgoKey: params.bitgoKey } : {}),

Copilot uses AI. Check for mistakes.

ignoreAddressTypes: [],
};
let recoveryTransaction;
try {
recoveryTransaction = await this.recover(recoverParams);
} catch (e) {
if (
e.message === 'Did not find address with funds to recover' ||
e.message === 'Did not find token account to recover tokens, please check token account' ||
e.message === 'Not enough token funds to recover'
) {
lastScanIndex = i;
continue;
}
throw e;
}
if (isUnsignedSweep) {
consolidatedTransactions.push((recoveryTransaction as MPCSweepTxs).txRequests[0]);
} else {
consolidatedTransactions.push(recoveryTransaction);
}
}
// To avoid rate limit for etherscan
await new Promise((resolve) => setTimeout(resolve, 1000));
// lastScanIndex = i;
Copy link
Preview

Copilot AI Aug 4, 2025

Choose a reason for hiding this comment

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

This commented out code should be removed or properly implemented if needed.

Suggested change
// lastScanIndex = i;

Copilot uses AI. Check for mistakes.

}

if (consolidatedTransactions.length === 0) {
throw new Error(
`Did not find an address with sufficient funds to recover. Please start the next scan at address index ${
lastScanIndex + 1
}.`
);
}
return { transactions: consolidatedTransactions, lastScanIndex };
}

/**
* Builds a funds recovery transaction without BitGo for non-TSS transaction
* @param params
Expand Down
Loading
Loading