Skip to content

Commit 0fee53f

Browse files
committed
feat: add coverage for base address
TICKET: WP-6461
1 parent 6e92993 commit 0fee53f

File tree

3 files changed

+558
-55
lines changed

3 files changed

+558
-55
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 248 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,40 @@ export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
411411

412412
export type TssVerifyEthAddressOptions = TssVerifyAddressOptions & VerifyEthAddressOptions;
413413

414+
/**
415+
* Keychain with ethAddress for BIP32 wallet verification (V1, V2, V4)
416+
* Used for wallets that derive addresses using Ethereum addresses from keychains
417+
*/
418+
export interface KeychainWithEthAddress {
419+
ethAddress: string;
420+
pub: string;
421+
}
422+
423+
/**
424+
* BIP32 wallet base address verification options
425+
* Supports V1, V2, and V4 wallets that use ethAddress-based derivation
426+
*/
427+
export interface VerifyBip32BaseAddressOptions extends VerifyEthAddressOptions {
428+
walletVersion: number;
429+
keychains: KeychainWithEthAddress[];
430+
}
431+
432+
/**
433+
* Type guard to check if params are for BIP32 base address verification (V1, V2, V4)
434+
* These wallet versions use ethAddress for address derivation
435+
*/
436+
export function isVerifyBip32BaseAddressOptions(
437+
params: VerifyEthAddressOptions | TssVerifyEthAddressOptions
438+
): params is VerifyBip32BaseAddressOptions {
439+
return (
440+
(params.walletVersion === 1 || params.walletVersion === 2 || params.walletVersion === 4) &&
441+
'keychains' in params &&
442+
Array.isArray(params.keychains) &&
443+
params.keychains.length === 3 &&
444+
params.keychains.every((kc: any) => 'ethAddress' in kc && typeof kc.ethAddress === 'string')
445+
);
446+
}
447+
414448
const debug = debugLib('bitgo:v2:ethlike');
415449

416450
export const optionalDeps = {
@@ -2732,23 +2766,176 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27322766
return {};
27332767
}
27342768

2735-
getForwarderFactoryAndImplContractAddresses(walletVersion: number | undefined): {
2769+
/**
2770+
* Get forwarder factory and implementation addresses for deposit address verification.
2771+
* Forwarders are smart contracts that forward funds to the base wallet address.
2772+
*
2773+
* @param {number | undefined} walletVersion - The wallet version
2774+
* @returns {object} Factory and implementation addresses for forwarders
2775+
*/
2776+
getForwarderFactoryAddressesAndForwarderImplementationAddress(walletVersion: number | undefined): {
27362777
forwarderFactoryAddress: string;
27372778
forwarderImplementationAddress: string;
27382779
} {
27392780
const ethNetwork = this.getNetwork();
2740-
if (walletVersion && (walletVersion === 5 || walletVersion === 4)) {
2741-
if (ethNetwork?.walletV4ForwarderFactoryAddress && ethNetwork?.walletV4ForwarderImplementationAddress) {
2781+
2782+
switch (walletVersion) {
2783+
case 2:
2784+
if (!ethNetwork?.walletV2ForwarderFactoryAddress || !ethNetwork?.walletV2ForwarderImplementationAddress) {
2785+
throw new Error('Wallet v2 factory addresses not configured for this network');
2786+
}
27422787
return {
2743-
forwarderFactoryAddress: ethNetwork?.walletV4ForwarderFactoryAddress as string,
2744-
forwarderImplementationAddress: ethNetwork?.walletV4ForwarderImplementationAddress as string,
2788+
forwarderFactoryAddress: ethNetwork.walletV2ForwarderFactoryAddress,
2789+
forwarderImplementationAddress: ethNetwork.walletV2ForwarderImplementationAddress,
2790+
};
2791+
case 4:
2792+
case 5:
2793+
if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) {
2794+
throw new Error(`Forwarder v${walletVersion} factory addresses not configured for this network`);
2795+
}
2796+
return {
2797+
forwarderFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress,
2798+
forwarderImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress,
2799+
};
2800+
default:
2801+
if (!ethNetwork?.forwarderFactoryAddress || !ethNetwork?.forwarderImplementationAddress) {
2802+
throw new Error('Forwarder factory addresses not configured for this network');
2803+
}
2804+
return {
2805+
forwarderFactoryAddress: ethNetwork.forwarderFactoryAddress,
2806+
forwarderImplementationAddress: ethNetwork.forwarderImplementationAddress,
27452807
};
2746-
}
27472808
}
2748-
return {
2749-
forwarderFactoryAddress: ethNetwork?.forwarderFactoryAddress as string,
2750-
forwarderImplementationAddress: ethNetwork?.forwarderImplementationAddress as string,
2751-
};
2809+
}
2810+
2811+
/**
2812+
* Get wallet base address factory and implementation addresses.
2813+
* This is used for base address verification for V1, V2, V4, and V5 wallets.
2814+
* The base address is the main wallet contract deployed via CREATE2.
2815+
*
2816+
* @param {number} walletVersion - The wallet version (1, 2, 4, or 5)
2817+
* @returns {object} Factory and implementation addresses for the wallet base address
2818+
* @throws {Error} if wallet version addresses are not configured
2819+
*/
2820+
getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion: number): {
2821+
walletFactoryAddress: string;
2822+
walletImplementationAddress: string;
2823+
} {
2824+
const ethNetwork = this.getNetwork();
2825+
2826+
switch (walletVersion) {
2827+
case 2:
2828+
if (!ethNetwork?.walletV2FactoryAddress || !ethNetwork?.walletV2ImplementationAddress) {
2829+
throw new Error('Wallet v2 factory addresses not configured for this network');
2830+
}
2831+
return {
2832+
walletFactoryAddress: ethNetwork.walletV2FactoryAddress,
2833+
walletImplementationAddress: ethNetwork.walletV2ImplementationAddress,
2834+
};
2835+
case 4:
2836+
case 5:
2837+
if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) {
2838+
throw new Error(`Wallet v${walletVersion} factory addresses not configured for this network`);
2839+
}
2840+
return {
2841+
walletFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress,
2842+
walletImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress,
2843+
};
2844+
default:
2845+
if (!ethNetwork?.walletFactoryAddress || !ethNetwork?.walletImplementationAddress) {
2846+
throw new Error('Wallet v1 factory addresses not configured for this network');
2847+
}
2848+
return {
2849+
walletFactoryAddress: ethNetwork.walletFactoryAddress,
2850+
walletImplementationAddress: ethNetwork.walletImplementationAddress,
2851+
};
2852+
}
2853+
}
2854+
2855+
/**
2856+
* Helper method to create a salt buffer from hex string.
2857+
* Converts a hex salt string to a 32-byte buffer.
2858+
*
2859+
* @param {string} salt - The hex salt string
2860+
* @returns {Buffer} 32-byte salt buffer
2861+
*/
2862+
private createSaltBuffer(salt: string): Buffer {
2863+
const ethUtil = optionalDeps.ethUtil;
2864+
return ethUtil.setLengthLeft(Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(salt || '')), 'hex'), 32);
2865+
}
2866+
2867+
/**
2868+
* Verify BIP32 wallet base address (V1, V2, V4).
2869+
* These wallets use a wallet factory to deploy base addresses with CREATE2.
2870+
* The address is derived from the keychains' ethAddresses and a salt.
2871+
*
2872+
* @param {VerifyBip32BaseAddressOptions} params - Verification parameters
2873+
* @returns {object} Expected and actual addresses for comparison
2874+
*/
2875+
private verifyBip32BaseAddress(params: VerifyBip32BaseAddressOptions): {
2876+
expectedAddress: string;
2877+
actualAddress: string;
2878+
} {
2879+
const { address, coinSpecific, keychains, walletVersion } = params;
2880+
2881+
if (!coinSpecific.salt) {
2882+
throw new Error(`missing salt for v${walletVersion} base address verification`);
2883+
}
2884+
2885+
// Get wallet factory and implementation addresses for the wallet version
2886+
const { walletFactoryAddress, walletImplementationAddress } =
2887+
this.getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion);
2888+
const initcode = getProxyInitcode(walletImplementationAddress);
2889+
2890+
// Convert the wallet salt to a buffer, pad to 32 bytes
2891+
const saltBuffer = this.createSaltBuffer(coinSpecific.salt);
2892+
2893+
// Reconstruct calculationSalt using keychains' ethAddresses and wallet salt
2894+
const ethAddresses = keychains.map((kc) => {
2895+
if (!kc.ethAddress) {
2896+
throw new Error(`keychain missing ethAddress for v${walletVersion} base address verification`);
2897+
}
2898+
return kc.ethAddress;
2899+
});
2900+
2901+
const calculationSalt = optionalDeps.ethUtil.bufferToHex(
2902+
optionalDeps.ethAbi.soliditySHA3(['address[]', 'bytes32'], [ethAddresses, saltBuffer])
2903+
);
2904+
2905+
const expectedAddress = calculateForwarderV1Address(walletFactoryAddress, calculationSalt, initcode);
2906+
return { expectedAddress, actualAddress: address };
2907+
}
2908+
2909+
/**
2910+
* Verify forwarder receive address (deposit address).
2911+
* Forwarder addresses are derived using CREATE2 from the base address and salt.
2912+
*
2913+
* @param {VerifyEthAddressOptions} params - Verification parameters
2914+
* @param {number} forwarderVersion - The forwarder version
2915+
* @returns {object} Expected and actual addresses for comparison
2916+
*/
2917+
private verifyForwarderAddress(
2918+
params: VerifyEthAddressOptions,
2919+
forwarderVersion: number
2920+
): { expectedAddress: string; actualAddress: string } {
2921+
const { address, coinSpecific, baseAddress } = params;
2922+
2923+
const { forwarderFactoryAddress, forwarderImplementationAddress } =
2924+
this.getForwarderFactoryAddressesAndForwarderImplementationAddress(params.walletVersion);
2925+
const initcode = getProxyInitcode(forwarderImplementationAddress);
2926+
const saltBuffer = this.createSaltBuffer(coinSpecific.salt || '');
2927+
2928+
const { createForwarderParams, createForwarderTypes } =
2929+
forwarderVersion === 4
2930+
? getCreateForwarderParamsAndTypes(baseAddress, saltBuffer, coinSpecific.feeAddress)
2931+
: getCreateForwarderParamsAndTypes(baseAddress, saltBuffer);
2932+
2933+
const calculationSalt = optionalDeps.ethUtil.bufferToHex(
2934+
optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams)
2935+
);
2936+
2937+
const expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode);
2938+
return { expectedAddress, actualAddress: address };
27522939
}
27532940

27542941
/**
@@ -2763,66 +2950,79 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27632950
* @returns {boolean} True iff address is a wallet address
27642951
*/
27652952
async isWalletAddress(params: VerifyEthAddressOptions | TssVerifyEthAddressOptions): Promise<boolean> {
2766-
const ethUtil = optionalDeps.ethUtil;
2767-
2768-
let expectedAddress;
2769-
let actualAddress;
2770-
2771-
const { address, impliedForwarderVersion, coinSpecific } = params;
2953+
const { address, impliedForwarderVersion, coinSpecific, baseAddress } = params;
27722954
const forwarderVersion = impliedForwarderVersion ?? coinSpecific?.forwarderVersion;
27732955

2956+
// Validate address format
27742957
if (address && !this.isValidAddress(address)) {
27752958
throw new InvalidAddressError(`invalid address: ${address}`);
27762959
}
2960+
27772961
// Forwarder version 0 addresses cannot be verified because we do not store the nonce value required for address derivation.
27782962
if (forwarderVersion === 0) {
27792963
return true;
27802964
}
2781-
// Verify MPC wallet address for wallet version 3 and 6
2782-
if (isTssVerifyAddressOptions(params) && params.walletVersion !== 5) {
2965+
2966+
// Determine if we are verifying a base address
2967+
const isVerifyingBaseAddress = baseAddress && address === baseAddress;
2968+
2969+
// TSS/MPC wallet address verification (V3, V5, V6)
2970+
// V5 base addresses use TSS, but V5 forwarders use the regular forwarder verification
2971+
const isTssWalletVersion = params.walletVersion === 3 || params.walletVersion === 5 || params.walletVersion === 6;
2972+
const shouldUseTssVerification =
2973+
isTssVerifyAddressOptions(params) && isTssWalletVersion && (params.walletVersion !== 5 || isVerifyingBaseAddress);
2974+
2975+
if (shouldUseTssVerification) {
2976+
if (isVerifyingBaseAddress) {
2977+
const index = typeof params.index === 'string' ? parseInt(params.index, 10) : params.index;
2978+
if (index !== 0) {
2979+
throw new Error(
2980+
`Base address verification requires index 0, but got index ${params.index}. ` +
2981+
`The base address is always derived at index 0.`
2982+
);
2983+
}
2984+
}
2985+
27832986
return verifyMPCWalletAddress({ ...params, keyCurve: 'secp256k1' }, this.isValidAddress, (pubKey) => {
27842987
return new KeyPairLib({ pub: pubKey }).getAddress();
27852988
});
2786-
} else {
2787-
// Verify forwarder receive address
2788-
const { coinSpecific, baseAddress } = params;
2789-
2790-
if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) {
2791-
throw new InvalidAddressError('invalid base address');
2792-
}
2989+
}
27932990

2794-
if (!_.isObject(coinSpecific)) {
2795-
throw new InvalidAddressVerificationObjectPropertyError(
2796-
'address validation failure: coinSpecific field must be an object'
2797-
);
2798-
}
2991+
// From here on, we need baseAddress and coinSpecific for non-TSS verifications
2992+
if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) {
2993+
throw new InvalidAddressError('invalid base address');
2994+
}
27992995

2800-
const { forwarderFactoryAddress, forwarderImplementationAddress } =
2801-
this.getForwarderFactoryAndImplContractAddresses(params.walletVersion);
2802-
const initcode = getProxyInitcode(forwarderImplementationAddress);
2803-
const saltBuffer = ethUtil.setLengthLeft(
2804-
Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(coinSpecific.salt || '')), 'hex'),
2805-
32
2996+
if (!_.isObject(coinSpecific)) {
2997+
throw new InvalidAddressVerificationObjectPropertyError(
2998+
'address validation failure: coinSpecific field must be an object'
28062999
);
3000+
}
28073001

2808-
const { createForwarderParams, createForwarderTypes } =
2809-
forwarderVersion === 4
2810-
? getCreateForwarderParamsAndTypes(baseAddress, saltBuffer, coinSpecific.feeAddress)
2811-
: getCreateForwarderParamsAndTypes(baseAddress, saltBuffer);
3002+
// BIP32 wallet base address verification (V1, V2, V4)
3003+
if (isVerifyingBaseAddress && isVerifyBip32BaseAddressOptions(params)) {
3004+
const { expectedAddress, actualAddress } = this.verifyBip32BaseAddress(params);
28123005

2813-
const calculationSalt = optionalDeps.ethUtil.bufferToHex(
2814-
optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams)
2815-
);
3006+
if (expectedAddress !== actualAddress) {
3007+
throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`);
3008+
}
28163009

2817-
expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode);
2818-
actualAddress = address;
3010+
return true;
28193011
}
28203012

2821-
if (expectedAddress !== actualAddress) {
2822-
throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`);
3013+
// Forwarder receive address verification (deposit addresses)
3014+
if (!isVerifyingBaseAddress) {
3015+
const { expectedAddress, actualAddress } = this.verifyForwarderAddress(params, forwarderVersion);
3016+
3017+
if (expectedAddress !== actualAddress) {
3018+
throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`);
3019+
}
3020+
3021+
return true;
28233022
}
28243023

2825-
return true;
3024+
// If we reach here, it's a base address verification for an unsupported wallet version
3025+
throw new Error(`Base address verification not supported for wallet version ${params.walletVersion}`);
28263026
}
28273027

28283028
/**

0 commit comments

Comments
 (0)