@@ -40,6 +40,9 @@ import {
4040 VerifyAddressOptions as BaseVerifyAddressOptions ,
4141 VerifyTransactionOptions ,
4242 Wallet ,
43+ verifyMPCWalletAddress ,
44+ TssVerifyAddressOptions ,
45+ isTssVerifyAddressOptions ,
4346} from '@bitgo/sdk-core' ;
4447import { getDerivationPath } from '@bitgo/sdk-lib-mpc' ;
4548import { bip32 } from '@bitgo/secp256k1' ;
@@ -369,6 +372,7 @@ interface PresignTransactionOptions extends TransactionPrebuild, BasePresignTran
369372interface EthAddressCoinSpecifics extends AddressCoinSpecific {
370373 forwarderVersion : number ;
371374 salt ?: string ;
375+ feeAddress ?: string ;
372376}
373377
374378export const DEFAULT_SCAN_FACTOR = 20 ;
@@ -401,7 +405,44 @@ export interface EthConsolidationRecoveryOptions {
401405export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions {
402406 baseAddress : string ;
403407 coinSpecific : EthAddressCoinSpecifics ;
404- forwarderVersion : number ;
408+ forwarderVersion ?: number ;
409+ walletVersion ?: number ;
410+ }
411+
412+ export type TssVerifyEthAddressOptions = TssVerifyAddressOptions & VerifyEthAddressOptions ;
413+
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 VerifyContractBaseAddressOptions 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 isVerifyContractBaseAddressOptions (
437+ params : VerifyEthAddressOptions | TssVerifyEthAddressOptions
438+ ) : params is VerifyContractBaseAddressOptions {
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+ ) ;
405446}
406447
407448const debug = debugLib ( 'bitgo:v2:ethlike' ) ;
@@ -2725,6 +2766,186 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27252766 return { } ;
27262767 }
27272768
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 } forwarderVersion - The wallet version
2774+ * @returns {object } Factory and implementation addresses for forwarders
2775+ */
2776+ getForwarderFactoryAddressesAndForwarderImplementationAddress ( forwarderVersion : number | undefined ) : {
2777+ forwarderFactoryAddress : string ;
2778+ forwarderImplementationAddress : string ;
2779+ } {
2780+ const ethNetwork = this . getNetwork ( ) ;
2781+
2782+ switch ( forwarderVersion ) {
2783+ case 1 :
2784+ if ( ! ethNetwork ?. forwarderFactoryAddress || ! ethNetwork ?. forwarderImplementationAddress ) {
2785+ throw new Error ( 'Forwarder factory addresses not configured for this network' ) ;
2786+ }
2787+ return {
2788+ forwarderFactoryAddress : ethNetwork . forwarderFactoryAddress ,
2789+ forwarderImplementationAddress : ethNetwork . forwarderImplementationAddress ,
2790+ } ;
2791+ case 2 :
2792+ if ( ! ethNetwork ?. walletV2ForwarderFactoryAddress || ! ethNetwork ?. walletV2ForwarderImplementationAddress ) {
2793+ throw new Error ( 'Wallet v2 factory addresses not configured for this network' ) ;
2794+ }
2795+ return {
2796+ forwarderFactoryAddress : ethNetwork . walletV2ForwarderFactoryAddress ,
2797+ forwarderImplementationAddress : ethNetwork . walletV2ForwarderImplementationAddress ,
2798+ } ;
2799+ case 4 :
2800+ case 5 :
2801+ if ( ! ethNetwork ?. walletV4ForwarderFactoryAddress || ! ethNetwork ?. walletV4ForwarderImplementationAddress ) {
2802+ throw new Error ( `Forwarder v${ forwarderVersion } factory addresses not configured for this network` ) ;
2803+ }
2804+ return {
2805+ forwarderFactoryAddress : ethNetwork . walletV4ForwarderFactoryAddress ,
2806+ forwarderImplementationAddress : ethNetwork . walletV4ForwarderImplementationAddress ,
2807+ } ;
2808+ default :
2809+ throw new Error ( `Forwarder version ${ forwarderVersion } not supported` ) ;
2810+ }
2811+ }
2812+
2813+ /**
2814+ * Get wallet base address factory and implementation addresses.
2815+ * This is used for base address verification for V1, V2, V4, and V5 wallets.
2816+ * The base address is the main wallet contract deployed via CREATE2.
2817+ *
2818+ * @param {number } walletVersion - The wallet version (1, 2, 4, or 5)
2819+ * @returns {object } Factory and implementation addresses for the wallet base address
2820+ * @throws {Error } if wallet version addresses are not configured
2821+ */
2822+ getWalletAddressFactoryAddressesAndImplementationAddress ( walletVersion : number ) : {
2823+ walletFactoryAddress : string ;
2824+ walletImplementationAddress : string ;
2825+ } {
2826+ const ethNetwork = this . getNetwork ( ) ;
2827+
2828+ switch ( walletVersion ) {
2829+ case 1 :
2830+ if ( ! ethNetwork ?. walletFactoryAddress || ! ethNetwork ?. walletImplementationAddress ) {
2831+ throw new Error ( 'Wallet v1 factory addresses not configured for this network' ) ;
2832+ }
2833+ return {
2834+ walletFactoryAddress : ethNetwork . walletFactoryAddress ,
2835+ walletImplementationAddress : ethNetwork . walletImplementationAddress ,
2836+ } ;
2837+ case 2 :
2838+ if ( ! ethNetwork ?. walletV2FactoryAddress || ! ethNetwork ?. walletV2ImplementationAddress ) {
2839+ throw new Error ( 'Wallet v2 factory addresses not configured for this network' ) ;
2840+ }
2841+ return {
2842+ walletFactoryAddress : ethNetwork . walletV2FactoryAddress ,
2843+ walletImplementationAddress : ethNetwork . walletV2ImplementationAddress ,
2844+ } ;
2845+ case 4 :
2846+ case 5 :
2847+ if ( ! ethNetwork ?. walletV4ForwarderFactoryAddress || ! ethNetwork ?. walletV4ForwarderImplementationAddress ) {
2848+ throw new Error ( `Wallet v${ walletVersion } factory addresses not configured for this network` ) ;
2849+ }
2850+ return {
2851+ walletFactoryAddress : ethNetwork . walletV4ForwarderFactoryAddress ,
2852+ walletImplementationAddress : ethNetwork . walletV4ForwarderImplementationAddress ,
2853+ } ;
2854+ default :
2855+ throw new Error ( `Wallet version ${ walletVersion } not supported` ) ;
2856+ }
2857+ }
2858+
2859+ /**
2860+ * Helper method to create a salt buffer from hex string.
2861+ * Converts a hex salt string to a 32-byte buffer.
2862+ *
2863+ * @param {string } salt - The hex salt string
2864+ * @returns {Buffer } 32-byte salt buffer
2865+ */
2866+ private createSaltBuffer ( salt : string ) : Buffer {
2867+ const ethUtil = optionalDeps . ethUtil ;
2868+ return ethUtil . setLengthLeft ( Buffer . from ( ethUtil . padToEven ( ethUtil . stripHexPrefix ( salt || '' ) ) , 'hex' ) , 32 ) ;
2869+ }
2870+
2871+ /**
2872+ * Verify BIP32 wallet base address (V1, V2, V4).
2873+ * These wallets use a wallet factory to deploy base addresses with CREATE2.
2874+ * The address is derived from the keychains' ethAddresses and a salt.
2875+ *
2876+ * @param {VerifyBip32BaseAddressOptions } params - Verification parameters
2877+ * @returns {object } Expected and actual addresses for comparison
2878+ */
2879+ private verifyCreate2BaseAddress ( params : VerifyContractBaseAddressOptions ) : boolean {
2880+ const { address, coinSpecific, keychains, walletVersion } = params ;
2881+
2882+ if ( ! coinSpecific . salt ) {
2883+ throw new Error ( `missing salt for v${ walletVersion } base address verification` ) ;
2884+ }
2885+
2886+ // Get wallet factory and implementation addresses for the wallet version
2887+ const { walletFactoryAddress, walletImplementationAddress } =
2888+ this . getWalletAddressFactoryAddressesAndImplementationAddress ( walletVersion ) ;
2889+ const initcode = getProxyInitcode ( walletImplementationAddress ) ;
2890+
2891+ // Convert the wallet salt to a buffer, pad to 32 bytes
2892+ const saltBuffer = this . createSaltBuffer ( coinSpecific . salt ) ;
2893+
2894+ // Reconstruct calculationSalt using keychains' ethAddresses and wallet salt
2895+ const ethAddresses = keychains . map ( ( kc ) => {
2896+ if ( ! kc . ethAddress ) {
2897+ throw new Error ( `keychain missing ethAddress for v${ walletVersion } base address verification` ) ;
2898+ }
2899+ return kc . ethAddress ;
2900+ } ) ;
2901+
2902+ const calculationSalt = optionalDeps . ethUtil . bufferToHex (
2903+ optionalDeps . ethAbi . soliditySHA3 ( [ 'address[]' , 'bytes32' ] , [ ethAddresses , saltBuffer ] )
2904+ ) ;
2905+
2906+ const expectedAddress = calculateForwarderV1Address ( walletFactoryAddress , calculationSalt , initcode ) ;
2907+
2908+ if ( expectedAddress !== address ) {
2909+ throw new UnexpectedAddressError ( `address validation failure: expected ${ expectedAddress } but got ${ address } ` ) ;
2910+ }
2911+
2912+ return true ;
2913+ }
2914+
2915+ /**
2916+ * Verify forwarder receive address (deposit address).
2917+ * Forwarder addresses are derived using CREATE2 from the base address and salt.
2918+ *
2919+ * @param {VerifyEthAddressOptions } params - Verification parameters
2920+ * @param {number } forwarderVersion - The forwarder version
2921+ * @returns {object } Expected and actual addresses for comparison
2922+ */
2923+ private verifyForwarderAddress ( params : VerifyEthAddressOptions , forwarderVersion : number ) : boolean {
2924+ const { address, coinSpecific, baseAddress } = params ;
2925+
2926+ const { forwarderFactoryAddress, forwarderImplementationAddress } =
2927+ this . getForwarderFactoryAddressesAndForwarderImplementationAddress ( forwarderVersion ) ;
2928+ const initcode = getProxyInitcode ( forwarderImplementationAddress ) ;
2929+ const saltBuffer = this . createSaltBuffer ( coinSpecific . salt || '' ) ;
2930+
2931+ const { createForwarderParams, createForwarderTypes } =
2932+ forwarderVersion === 4
2933+ ? getCreateForwarderParamsAndTypes ( baseAddress , saltBuffer , coinSpecific . feeAddress )
2934+ : getCreateForwarderParamsAndTypes ( baseAddress , saltBuffer ) ;
2935+
2936+ const calculationSalt = optionalDeps . ethUtil . bufferToHex (
2937+ optionalDeps . ethAbi . soliditySHA3 ( createForwarderTypes , createForwarderParams )
2938+ ) ;
2939+
2940+ const expectedAddress = calculateForwarderV1Address ( forwarderFactoryAddress , calculationSalt , initcode ) ;
2941+
2942+ if ( expectedAddress !== address ) {
2943+ throw new UnexpectedAddressError ( `address validation failure: expected ${ expectedAddress } but got ${ address } ` ) ;
2944+ }
2945+
2946+ return true ;
2947+ }
2948+
27282949 /**
27292950 * Make sure an address is a wallet address and throw an error if it's not.
27302951 * @param {Object } params
@@ -2736,19 +2957,46 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27362957 * @throws {UnexpectedAddressError }
27372958 * @returns {boolean } True iff address is a wallet address
27382959 */
2739- async isWalletAddress ( params : VerifyEthAddressOptions ) : Promise < boolean > {
2740- const ethUtil = optionalDeps . ethUtil ;
2741-
2742- let expectedAddress ;
2743- let actualAddress ;
2744-
2745- const { address, coinSpecific, baseAddress, impliedForwarderVersion = coinSpecific ?. forwarderVersion } = params ;
2960+ async isWalletAddress ( params : VerifyEthAddressOptions | TssVerifyEthAddressOptions ) : Promise < boolean > {
2961+ const { address, impliedForwarderVersion, coinSpecific, baseAddress } = params ;
2962+ const forwarderVersion = impliedForwarderVersion ?? coinSpecific ?. forwarderVersion ;
27462963
2964+ // Validate address format
27472965 if ( address && ! this . isValidAddress ( address ) ) {
27482966 throw new InvalidAddressError ( `invalid address: ${ address } ` ) ;
27492967 }
27502968
2751- // base address is required to calculate the salt which is used in calculateForwarderV1Address method
2969+ // Forwarder version 0 addresses cannot be verified because we do not store the nonce value required for address derivation.
2970+ if ( forwarderVersion === 0 ) {
2971+ return true ;
2972+ }
2973+
2974+ // Determine if we are verifying a base address
2975+ const isVerifyingBaseAddress = baseAddress && address === baseAddress ;
2976+
2977+ // TSS/MPC wallet address verification (V3, V5, V6)
2978+ // V5 base addresses use TSS, but V5 forwarders use the regular forwarder verification
2979+ const isTssWalletVersion = params . walletVersion === 3 || params . walletVersion === 5 || params . walletVersion === 6 ;
2980+ const shouldUseTssVerification =
2981+ isTssVerifyAddressOptions ( params ) && isTssWalletVersion && ( params . walletVersion !== 5 || isVerifyingBaseAddress ) ;
2982+
2983+ if ( shouldUseTssVerification ) {
2984+ if ( isVerifyingBaseAddress ) {
2985+ const index = typeof params . index === 'string' ? parseInt ( params . index , 10 ) : params . index ;
2986+ if ( index !== 0 ) {
2987+ throw new Error (
2988+ `Base address verification requires index 0, but got index ${ params . index } . ` +
2989+ `The base address is always derived at index 0.`
2990+ ) ;
2991+ }
2992+ }
2993+
2994+ return verifyMPCWalletAddress ( { ...params , keyCurve : 'secp256k1' } , this . isValidAddress , ( pubKey ) => {
2995+ return new KeyPairLib ( { pub : pubKey } ) . getAddress ( ) ;
2996+ } ) ;
2997+ }
2998+
2999+ // From here on, we need baseAddress and coinSpecific for non-TSS verifications
27523000 if ( _ . isUndefined ( baseAddress ) || ! this . isValidAddress ( baseAddress ) ) {
27533001 throw new InvalidAddressError ( 'invalid base address' ) ;
27543002 }
@@ -2759,33 +3007,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
27593007 ) ;
27603008 }
27613009
2762- if ( impliedForwarderVersion === 0 || impliedForwarderVersion === 3 || impliedForwarderVersion === 5 ) {
2763- return true ;
2764- } else {
2765- const ethNetwork = this . getNetwork ( ) ;
2766- const forwarderFactoryAddress = ethNetwork ?. forwarderFactoryAddress as string ;
2767- const forwarderImplementationAddress = ethNetwork ?. forwarderImplementationAddress as string ;
2768-
2769- const initcode = getProxyInitcode ( forwarderImplementationAddress ) ;
2770- const saltBuffer = ethUtil . setLengthLeft (
2771- Buffer . from ( ethUtil . padToEven ( ethUtil . stripHexPrefix ( coinSpecific . salt || '' ) ) , 'hex' ) ,
2772- 32
2773- ) ;
2774-
2775- // Hash the wallet base address with the given salt, so the address directly relies on the base address
2776- const calculationSalt = optionalDeps . ethUtil . bufferToHex (
2777- optionalDeps . ethAbi . soliditySHA3 ( [ 'address' , 'bytes32' ] , [ baseAddress , saltBuffer ] )
2778- ) ;
2779-
2780- expectedAddress = calculateForwarderV1Address ( forwarderFactoryAddress , calculationSalt , initcode ) ;
2781- actualAddress = address ;
3010+ // BIP32 wallet base address verification (V1, V2, V4)
3011+ if ( isVerifyingBaseAddress && isVerifyContractBaseAddressOptions ( params ) ) {
3012+ return this . verifyCreate2BaseAddress ( params ) ;
27823013 }
27833014
2784- if ( expectedAddress !== actualAddress ) {
2785- throw new UnexpectedAddressError ( `address validation failure: expected ${ expectedAddress } but got ${ address } ` ) ;
3015+ // Forwarder receive address verification (deposit addresses)
3016+ if ( ! isVerifyingBaseAddress ) {
3017+ return this . verifyForwarderAddress ( params , forwarderVersion ) ;
27863018 }
27873019
2788- return true ;
3020+ // If we reach here, it's a base address verification for an unsupported wallet version
3021+ throw new Error ( `Base address verification not supported for wallet version ${ params . walletVersion } ` ) ;
27893022 }
27903023
27913024 /**
@@ -3056,7 +3289,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
30563289 }
30573290 const typedDataRaw = JSON . parse ( typedData . typedDataRaw ) ;
30583291 const sanitizedData = TypedDataUtils . sanitizeData ( typedDataRaw as unknown as TypedMessage < any > ) ;
3059- const parts = [ Buffer . from ( '1901' , 'hex' ) ] ;
3292+ const parts : Buffer [ ] = [ Buffer . from ( '1901' , 'hex' ) ] ;
30603293 const eip712Domain = 'EIP712Domain' ;
30613294 parts . push ( TypedDataUtils . hashStruct ( eip712Domain , sanitizedData . domain , sanitizedData . types , version ) ) ;
30623295
0 commit comments