@@ -411,6 +411,40 @@ export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
411411
412412export 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+
414448const debug = debugLib ( 'bitgo:v2:ethlike' ) ;
415449
416450export 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