From 3292e9de444fa30b635c34b00d3c71e0b9f5ae21 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Fri, 21 Nov 2025 14:08:20 -0500 Subject: [PATCH 1/6] feat: add wallet address verification for eth TICKET: WP-6461 --- .../src/abstractEthLikeNewCoins.ts | 78 ++- modules/sdk-coin-eth/test/unit/eth.ts | 491 ++++++++++++------ .../sdk-core/src/bitgo/baseCoin/iBaseCoin.ts | 15 +- .../bitgo/utils/tss/addressVerification.ts | 33 +- modules/sdk-core/src/index.ts | 1 + 5 files changed, 435 insertions(+), 183 deletions(-) diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index be9a301d33..6a31911f9b 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -40,6 +40,9 @@ import { VerifyAddressOptions as BaseVerifyAddressOptions, VerifyTransactionOptions, Wallet, + verifyMPCWalletAddress, + TssVerifyAddressOptions, + isTssVerifyAddressOptions, } from '@bitgo/sdk-core'; import { getDerivationPath } from '@bitgo/sdk-lib-mpc'; import { bip32 } from '@bitgo/secp256k1'; @@ -369,6 +372,7 @@ interface PresignTransactionOptions extends TransactionPrebuild, BasePresignTran interface EthAddressCoinSpecifics extends AddressCoinSpecific { forwarderVersion: number; salt?: string; + feeAddress?: string; } export const DEFAULT_SCAN_FACTOR = 20; @@ -401,9 +405,12 @@ export interface EthConsolidationRecoveryOptions { export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions { baseAddress: string; coinSpecific: EthAddressCoinSpecifics; - forwarderVersion: number; + forwarderVersion?: number; + walletVersion?: number; } +export type TssVerifyEthAddressOptions = TssVerifyAddressOptions & VerifyEthAddressOptions; + const debug = debugLib('bitgo:v2:ethlike'); export const optionalDeps = { @@ -2725,6 +2732,23 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { return {}; } + getFactoryAndImplContractAddresses(walletVersion: number | undefined): { + forwarderFactoryAddress: string; + forwarderImplementationAddress: string; + } { + const ethNetwork = this.getNetwork(); + if (walletVersion && (walletVersion === 5 || walletVersion === 4)) { + return { + forwarderFactoryAddress: ethNetwork?.walletV4ForwarderFactoryAddress as string, + forwarderImplementationAddress: ethNetwork?.walletV4ForwarderImplementationAddress as string, + }; + } + return { + forwarderFactoryAddress: ethNetwork?.forwarderFactoryAddress as string, + forwarderImplementationAddress: ethNetwork?.forwarderImplementationAddress as string, + }; + } + /** * Make sure an address is a wallet address and throw an error if it's not. * @param {Object} params @@ -2736,45 +2760,57 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @throws {UnexpectedAddressError} * @returns {boolean} True iff address is a wallet address */ - async isWalletAddress(params: VerifyEthAddressOptions): Promise { + async isWalletAddress(params: VerifyEthAddressOptions | TssVerifyEthAddressOptions): Promise { const ethUtil = optionalDeps.ethUtil; let expectedAddress; let actualAddress; - const { address, coinSpecific, baseAddress, impliedForwarderVersion = coinSpecific?.forwarderVersion } = params; + const { address, impliedForwarderVersion, coinSpecific } = params; + const forwarderVersion = impliedForwarderVersion ?? coinSpecific?.forwarderVersion; if (address && !this.isValidAddress(address)) { throw new InvalidAddressError(`invalid address: ${address}`); } - - // base address is required to calculate the salt which is used in calculateForwarderV1Address method - if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) { - throw new InvalidAddressError('invalid base address'); + // Forwarder version 0 addresses cannot be verified because we do not store the nonce value required for address derivation. + if (forwarderVersion === 0) { + return true; } + // Verify MPC wallet address for wallet version 3 and 6 + if (isTssVerifyAddressOptions(params) && params.walletVersion !== 5) { + return verifyMPCWalletAddress({ ...params, keyCurve: 'secp256k1' }, this.isValidAddress, (pubKey) => { + return new KeyPairLib({ pub: pubKey }).getAddress(); + }); + } else { + // Verify forwarder receive address + const { coinSpecific, baseAddress } = params; - if (!_.isObject(coinSpecific)) { - throw new InvalidAddressVerificationObjectPropertyError( - 'address validation failure: coinSpecific field must be an object' - ); - } + if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) { + throw new InvalidAddressError('invalid base address'); + } - if (impliedForwarderVersion === 0 || impliedForwarderVersion === 3 || impliedForwarderVersion === 5) { - return true; - } else { - const ethNetwork = this.getNetwork(); - const forwarderFactoryAddress = ethNetwork?.forwarderFactoryAddress as string; - const forwarderImplementationAddress = ethNetwork?.forwarderImplementationAddress as string; + if (!_.isObject(coinSpecific)) { + throw new InvalidAddressVerificationObjectPropertyError( + 'address validation failure: coinSpecific field must be an object' + ); + } + const { forwarderFactoryAddress, forwarderImplementationAddress } = this.getFactoryAndImplContractAddresses( + params.walletVersion + ); const initcode = getProxyInitcode(forwarderImplementationAddress); const saltBuffer = ethUtil.setLengthLeft( Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(coinSpecific.salt || '')), 'hex'), 32 ); - // Hash the wallet base address with the given salt, so the address directly relies on the base address + const { createForwarderParams, createForwarderTypes } = + forwarderVersion === 4 + ? getCreateForwarderParamsAndTypes(baseAddress, saltBuffer, coinSpecific.feeAddress) + : getCreateForwarderParamsAndTypes(baseAddress, saltBuffer); + const calculationSalt = optionalDeps.ethUtil.bufferToHex( - optionalDeps.ethAbi.soliditySHA3(['address', 'bytes32'], [baseAddress, saltBuffer]) + optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams) ); expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode); @@ -3056,7 +3092,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { } const typedDataRaw = JSON.parse(typedData.typedDataRaw); const sanitizedData = TypedDataUtils.sanitizeData(typedDataRaw as unknown as TypedMessage); - const parts = [Buffer.from('1901', 'hex')]; + const parts: Buffer[] = [Buffer.from('1901', 'hex')]; const eip712Domain = 'EIP712Domain'; parts.push(TypedDataUtils.hashStruct(eip712Domain, sanitizedData.domain, sanitizedData.types, version)); diff --git a/modules/sdk-coin-eth/test/unit/eth.ts b/modules/sdk-coin-eth/test/unit/eth.ts index d0f2f82774..cac0c46241 100644 --- a/modules/sdk-coin-eth/test/unit/eth.ts +++ b/modules/sdk-coin-eth/test/unit/eth.ts @@ -23,8 +23,10 @@ import { Teth, TransactionBuilder, TransferBuilder, + TssVerifyEthAddressOptions, UnsignedBuilConsolidation, UnsignedSweepTxMPCv2, + VerifyEthAddressOptions, } from '../../src'; import { EthereumNetwork } from '@bitgo/statics'; import assert from 'assert'; @@ -662,182 +664,359 @@ describe('ETH:', function () { }); describe('Address Verification', function () { - it('should verify an address generated using forwarder version 0', async function () { - const coin = bitgo.coin('teth') as Teth; + describe('isWalletAddress', function () { + it('should verify an address generated using forwarder version 1', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const keychains = [ + { + id: '691f638f1d3c9fce8f5aa691569a99eb', + source: 'user', + type: 'independent', + pub: 'xpub661MyMwAqRbcGVb3PfCzwiEX94AB1nJQtzVmsa5SriNrfKZZAcAvRgxh1Augm6s8yoD8gSkq2FdZ8YCdVXUgLjf9QxvdYAJK5UthAmpQshU', + }, + { + id: '691f638f0b74e73b1f440ea4aceda87e', + source: 'backup', + type: 'independent', + pub: 'xpub661MyMwAqRbcF46pRHda3sZbuPzza9A9MiqAU9JRod8huYtyV4NY2oeJXsis7r26L1vmLntf9BcZJe1m4CQNSvYWfwpe1hSpo6J4x6YF1eN', + }, + { + id: '68b9ec587d0ba1c7440de551068c36a7', + source: 'bitgo', + type: 'independent', + pub: 'xpub661MyMwAqRbcGzTn5eyNGDkb18R43nH79HokYLc5PXZM19V8UrbuLdVRaCQMs4EeCAjnqmoYXqfyusTU46WoZMDyLpmTzoUX66ZBwGFjt1a', + }, + ]; + + const params = { + address: '0x6069a4baf2360bf67a6d02a7fc43d8f3910016ae', + baseAddress: '0xe1253bcce7d87db522fbceec6e55c9f78c376d9f', + coinSpecific: { + salt: '0x7', + forwarderVersion: 1, + }, + keychains, + index: 7, + walletVersion: 1, + } as unknown as TssVerifyEthAddressOptions; - const params = { - id: '6127bff4ecd84c0006cd9a0e5ccdc36f', - chain: 0, - index: 3174, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-26T16:23:16.563Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - pendingDeployment: false, - forwarderVersion: 0, - }, - }; + const isWalletAddr = await coin.isWalletAddress(params as any); + isWalletAddr.should.equal(true); + }); - const isAddressVerified = await coin.verifyAddress(params as any); - isAddressVerified.should.equal(true); - }); + it('should verify an address generated using forwarder version 2', async function () { + const coin = bitgo.coin('hteth') as Hteth; - it('should verify an address generated using forwarder version 1', async function () { - const coin = bitgo.coin('teth') as Teth; + const keychains = [ + { + id: '691e8c7b3c8aaa791118d9ce616d3b21', + source: 'user', + type: 'independent', + pub: 'xpub661MyMwAqRbcGrCxCX39zb3TvYjTqfUGwEUZHjnraRFm1WeMw9gfCD1wwc2wUDmBBZ2TkccJMwf5eBTja8r3z6HMxoTZGW6nvyoJMQFsecv', + }, + { + id: '691e8c7b1967fd6d9867a22a1a4131a0', + source: 'backup', + type: 'independent', + pub: 'xpub661MyMwAqRbcGKhdeC4nr1ta8d27xThtfFFHgbxWMrVb595meMS8i3fBMrTz8EdQMWBKHHKzxapGgheoMymVvRcQmaGDykRTBbtXqbiu9ps', + }, + { + id: '68b9ec587d0ba1c7440de551068c36a7', + source: 'bitgo', + type: 'independent', + pub: 'xpub661MyMwAqRbcGzTn5eyNGDkb18R43nH79HokYLc5PXZM19V8UrbuLdVRaCQMs4EeCAjnqmoYXqfyusTU46WoZMDyLpmTzoUX66ZBwGFjt1a', + }, + ]; + + const params = { + address: '0xf636ceddffe41d106586875c0e56dc8feb6268f7', + baseAddress: '0xdc485da076ed4a2b19584e9a1fdbb974f89b60f4', + coinSpecific: { + salt: '0x17', + forwarderVersion: 2, + }, + keychains, + index: 23, + walletVersion: 2, + } as unknown as TssVerifyEthAddressOptions; - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-24T14:28:39.841Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - salt: '0xc5a', - pendingDeployment: true, - forwarderVersion: 1, - }, - }; + const isWalletAddr = await coin.isWalletAddress(params as any); + isWalletAddr.should.equal(true); + }); - const isAddressVerified = await coin.verifyAddress(params); - isAddressVerified.should.equal(true); - }); + it('should verify a wallet version 5 forwarder version 4', async function () { + const coin = bitgo.coin('hteth') as Hteth; + const keychains = [ + { + id: '691e242d93f8d7ad0705887449763c96', + source: 'user', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + { + id: '691e242df8b6323d4b08df366864af66', + source: 'backup', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + { + id: '691e242c0595b4cfee6f957c1d6458f7', + source: 'bitgo', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + ]; + + const params = { + address: '0xd63b5e2b8d1b4fba3625460508900bf2a0499a4d', + baseAddress: '0xf1e3d30798acdf3a12fa5beb5fad8efb23d5be11', + coinSpecific: { + salt: '0x75', + forwarderVersion: 4, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 117, + walletVersion: 5, + } as unknown as TssVerifyEthAddressOptions; - it('should reject address verification if coinSpecific field is not an object', async function () { - const coin = bitgo.coin('teth') as Teth; + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', - }; + it('should verify a wallet version 6 forwarder version 5', async function () { + const coin = bitgo.coin('hteth') as Hteth; + const keychains = [ + { + id: '691f630e0c56098288a9b7fa107db144', + source: 'user', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630f1d3c9fce8f5a730bff826cf9', + source: 'backup', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630d56735e5eb61b06e353fe7639', + source: 'bitgo', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + ]; + + const params = { + address: '0xa33f0975f53cdcfcc0cb564d25fb5be03b0651cf', + baseAddress: '0xc012041dac143a59fa491db3a2b67b69bd78b685', + coinSpecific: { + forwarderVersion: 5, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 7, + walletVersion: 6, + } as unknown as TssVerifyEthAddressOptions; - assert.rejects(async () => coin.verifyAddress(params), InvalidAddressVerificationObjectPropertyError); - }); + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); - it('should reject address verification when an actual address is different from expected address', async function () { - const coin = bitgo.coin('teth') as Teth; + it('should reject when actual address differs from expected address', async function () { + const coin = bitgo.coin('teth') as Teth; - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0x28904591f735994f050804fda3b61b813b16e04c', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-24T14:28:39.841Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - salt: '0xc5a', - pendingDeployment: true, - forwarderVersion: 1, - }, - }; + const params = { + address: '0x28904591f735994f050804fda3b61b813b16e04c', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + salt: '0xc5a', + forwarderVersion: 1, + }, + } as unknown as VerifyEthAddressOptions; - assert.rejects(async () => coin.verifyAddress(params), UnexpectedAddressError); - }); + await assert.rejects(async () => coin.isWalletAddress(params), UnexpectedAddressError); + }); - it('should reject address verification if the derived address is in invalid format', async function () { - const coin = bitgo.coin('teth') as Teth; + it('should reject if coinSpecific field is not an object', async function () { + const coin = bitgo.coin('teth') as Teth; - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0xe0b56eeae1b283918caca02a14ada2df17a98bvf', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-24T14:28:39.841Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - salt: '0xc5a', - pendingDeployment: true, - forwarderVersion: 1, - }, - }; + const params = { + address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + }; - assert.rejects(async () => coin.verifyAddress(params), InvalidAddressError); - }); + await assert.rejects( + async () => coin.isWalletAddress(params as any), + InvalidAddressVerificationObjectPropertyError + ); + }); - it('should reject address verification if base address is undefined', async function () { - const coin = bitgo.coin('teth') as Teth; + it('should reject if the derived address is in invalid format', async function () { + const coin = bitgo.coin('teth') as Teth; - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-24T14:28:39.841Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - salt: '0xc5a', - pendingDeployment: true, - forwarderVersion: 1, - }, - }; + const params = { + address: '0xe0b56eeae1b283918caca02a14ada2df17a98bvf', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + salt: '0xc5a', + forwarderVersion: 1, + }, + } as unknown as VerifyEthAddressOptions; - assert.rejects(async () => coin.verifyAddress(params), InvalidAddressError); - }); + await assert.rejects(async () => coin.isWalletAddress(params), InvalidAddressError); + }); - it('should reject address verification if base address is in invalid format', async function () { - const coin = bitgo.coin('teth') as Teth; + it('should reject if base address is undefined', async function () { + const coin = bitgo.coin('teth') as Teth; - const params = { - id: '61250217c8c02b000654b15e7af6f618', - address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', - chain: 0, - index: 3162, - coin: 'teth', - lastNonce: 0, - wallet: '598f606cd8fc24710d2ebadb1d9459bb', - baseAddress: '0xe0b56eeae1b283918caca02a14ada2df17a98bvf', - coinSpecific: { - nonce: -1, - updateTime: '2021-08-24T14:28:39.841Z', - txCount: 0, - pendingChainInitialization: true, - creationFailure: [], - salt: '0xc5a', - pendingDeployment: true, - forwarderVersion: 1, - }, - }; + const params = { + address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', + coinSpecific: { + salt: '0xc5a', + forwarderVersion: 1, + }, + }; - assert.rejects(async () => coin.verifyAddress(params), InvalidAddressError); + await assert.rejects(async () => coin.isWalletAddress(params as any), InvalidAddressError); + }); + + it('should reject if base address is in invalid format', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0xb0b56eeae1b283918caca02a14ada2df17a98e6d', + baseAddress: '0xe0b56eeae1b283918caca02a14ada2df17a98bvf', + coinSpecific: { + salt: '0xc5a', + forwarderVersion: 1, + }, + } as unknown as VerifyEthAddressOptions; + + await assert.rejects(async () => coin.isWalletAddress(params), InvalidAddressError); + }); + + describe('MPC wallet addresses', function () { + const commonKeychain = + '03f9c2fb2e5a8b78a44f5d1e4f906f8e3d7a0e6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e0f9e8d7c6b5a4' + + '93827160594857463728190a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'; + const keychains = [ + { pub: 'user_pub', commonKeychain }, + { pub: 'backup_pub', commonKeychain }, + { pub: 'bitgo_pub', commonKeychain }, + ]; + + it('should verify an MPC wallet address with forwarder version 3', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0x01153f3adfe454a72589ca9ef74f013c19e54961', + coinSpecific: { + forwarderVersion: 3, + }, + keychains, + index: 0, + walletVersion: 3, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + + it('should verify an MPC wallet address with forwarder version 5', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0x01153f3adfe454a72589ca9ef74f013c19e54961', + coinSpecific: { + forwarderVersion: 5, + }, + keychains, + index: 0, + walletVersion: 6, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + + it('should reject MPC wallet address with wrong address', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0x0000000000000000000000000000000000000001', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + forwarderVersion: 3, + }, + keychains, + index: 0, + walletVersion: 3, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(false); + }); + + it('should reject MPC wallet address with invalid address format', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0xinvalid', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + forwarderVersion: 3, + }, + keychains, + index: 0, + walletVersion: 3, + } as unknown as TssVerifyEthAddressOptions; + + await assert.rejects(async () => coin.isWalletAddress(params), InvalidAddressError); + }); + + it('should reject if keychains are missing for MPC wallet', async function () { + const coin = bitgo.coin('teth') as Teth; + + const params = { + address: '0x9e7ce8c24d9f76a814e23633e61be7cb8e6e2d5e', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + forwarderVersion: 3, + }, + index: 0, + walletVersion: 3, + }; + + await assert.rejects(async () => coin.isWalletAddress(params as any), Error); + }); + + it('should reject if commonKeychain is missing for MPC wallet', async function () { + const coin = bitgo.coin('teth') as Teth; + + const invalidKeychains = [{ pub: 'user_pub' }, { pub: 'backup_pub' }, { pub: 'bitgo_pub' }]; + + const params = { + address: '0x9e7ce8c24d9f76a814e23633e61be7cb8e6e2d5e', + baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + coinSpecific: { + forwarderVersion: 3, + }, + keychains: invalidKeychains, + index: 0, + walletVersion: 3, + } as unknown as TssVerifyEthAddressOptions; + + await assert.rejects(async () => coin.isWalletAddress(params), Error); + }); + }); }); }); diff --git a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts index ac0925ee1b..136630361b 100644 --- a/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts +++ b/modules/sdk-core/src/bitgo/baseCoin/iBaseCoin.ts @@ -169,12 +169,23 @@ export interface TssVerifyAddressOptions { * For MPC wallets, the commonKeychain (combined public key from MPC key generation) * should be identical across all keychains (user, backup, bitgo). */ - keychains: Keychain[]; + keychains: Pick[]; /** * Derivation index for the address. * Used to derive child addresses from the root keychain via HD derivation path: m/{index} */ - index: string; + index: number | string; +} + +export function isTssVerifyAddressOptions( + params: T +): params is T & TssVerifyAddressOptions { + return !!( + 'keychains' in params && + 'index' in params && + 'address' in params && + params.keychains?.some((kc) => 'commonKeychain' in kc && !!kc.commonKeychain) + ); } export interface TransactionParams { diff --git a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts index cc42b95f14..4e5fd92080 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/addressVerification.ts @@ -1,3 +1,4 @@ +import { Ecdsa } from '../../../account-lib/mpc'; import { TssVerifyAddressOptions } from '../../baseCoin/iBaseCoin'; import { InvalidAddressError } from '../../errors'; import { EDDSAMethods } from '../../tss'; @@ -42,6 +43,27 @@ export async function verifyEddsaTssWalletAddress( params: TssVerifyAddressOptions, isValidAddress: (address: string) => boolean, getAddressFromPublicKey: (publicKey: string) => string +): Promise { + return verifyMPCWalletAddress({ ...params, keyCurve: 'ed25519' }, isValidAddress, getAddressFromPublicKey); +} + +/** + * Verifies if an address belongs to a wallet using ECDSA TSS MPC derivation. + * This is a common implementation for ECDSA-based MPC coins (ETH, BTC, etc.) + * + * @param params - Verification options including keychains, address, and derivation index + * @param isValidAddress - Coin-specific function to validate address format + * @param getAddressFromPublicKey - Coin-specific function to convert public key to address + * @returns true if the address matches the derived address, false otherwise + * @throws {InvalidAddressError} if the address is invalid + * @throws {Error} if required parameters are missing or invalid + */ +export async function verifyMPCWalletAddress( + params: TssVerifyAddressOptions & { + keyCurve: 'secp256k1' | 'ed25519'; + }, + isValidAddress: (address: string) => boolean, + getAddressFromPublicKey: (publicKey: string) => string ): Promise { const { keychains, address, index } = params; @@ -49,12 +71,15 @@ export async function verifyEddsaTssWalletAddress( throw new InvalidAddressError(`invalid address: ${address}`); } + const MPC = params.keyCurve === 'secp256k1' ? new Ecdsa() : await EDDSAMethods.getInitializedMpcInstance(); const commonKeychain = extractCommonKeychain(keychains); + const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, 'm/' + index); + + // secp256k1 expects 33 bytes; ed25519 expects 32 bytes + const publicKeySize = params.keyCurve === 'secp256k1' ? 33 : 32; + const publicKeyOnly = Buffer.from(derivedPublicKey, 'hex').subarray(0, publicKeySize).toString('hex'); - const MPC = await EDDSAMethods.getInitializedMpcInstance(); - const derivationPath = 'm/' + index; - const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64); - const expectedAddress = getAddressFromPublicKey(derivedPublicKey); + const expectedAddress = getAddressFromPublicKey(publicKeyOnly); return address === expectedAddress; } diff --git a/modules/sdk-core/src/index.ts b/modules/sdk-core/src/index.ts index e10fc32e2a..1b07844be5 100644 --- a/modules/sdk-core/src/index.ts +++ b/modules/sdk-core/src/index.ts @@ -10,6 +10,7 @@ import { EcdsaUtils } from './bitgo/utils/tss/ecdsa/ecdsa'; export { EcdsaUtils }; import { EcdsaMPCv2Utils } from './bitgo/utils/tss/ecdsa/ecdsaMPCv2'; export { EcdsaMPCv2Utils }; +export { verifyEddsaTssWalletAddress, verifyMPCWalletAddress } from './bitgo/utils/tss/addressVerification'; export { GShare, SignShare, YShare } from './account-lib/mpc/tss/eddsa/types'; export { TssEcdsaStep1ReturnMessage, TssEcdsaStep2ReturnMessage } from './bitgo/tss/types'; export { SShare } from './bitgo/tss/ecdsa/types'; From 6e929935bac534a6f70b1179dd5150facb789bb6 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Fri, 21 Nov 2025 15:33:59 -0500 Subject: [PATCH 2/6] refactor: check if v4 factory is defined TICKET: WP-6461 --- .../src/abstractEthLikeNewCoins.ts | 17 +++---- modules/sdk-coin-polygon/test/unit/polygon.ts | 45 +++++++++++++++++++ modules/statics/src/networks.ts | 10 +++++ 3 files changed, 64 insertions(+), 8 deletions(-) diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 6a31911f9b..1e76007eb3 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -2732,16 +2732,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { return {}; } - getFactoryAndImplContractAddresses(walletVersion: number | undefined): { + getForwarderFactoryAndImplContractAddresses(walletVersion: number | undefined): { forwarderFactoryAddress: string; forwarderImplementationAddress: string; } { const ethNetwork = this.getNetwork(); if (walletVersion && (walletVersion === 5 || walletVersion === 4)) { - return { - forwarderFactoryAddress: ethNetwork?.walletV4ForwarderFactoryAddress as string, - forwarderImplementationAddress: ethNetwork?.walletV4ForwarderImplementationAddress as string, - }; + if (ethNetwork?.walletV4ForwarderFactoryAddress && ethNetwork?.walletV4ForwarderImplementationAddress) { + return { + forwarderFactoryAddress: ethNetwork?.walletV4ForwarderFactoryAddress as string, + forwarderImplementationAddress: ethNetwork?.walletV4ForwarderImplementationAddress as string, + }; + } } return { forwarderFactoryAddress: ethNetwork?.forwarderFactoryAddress as string, @@ -2795,9 +2797,8 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { ); } - const { forwarderFactoryAddress, forwarderImplementationAddress } = this.getFactoryAndImplContractAddresses( - params.walletVersion - ); + const { forwarderFactoryAddress, forwarderImplementationAddress } = + this.getForwarderFactoryAndImplContractAddresses(params.walletVersion); const initcode = getProxyInitcode(forwarderImplementationAddress); const saltBuffer = ethUtil.setLengthLeft( Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(coinSpecific.salt || '')), 'hex'), diff --git a/modules/sdk-coin-polygon/test/unit/polygon.ts b/modules/sdk-coin-polygon/test/unit/polygon.ts index 16063a63c4..646bcf5a16 100644 --- a/modules/sdk-coin-polygon/test/unit/polygon.ts +++ b/modules/sdk-coin-polygon/test/unit/polygon.ts @@ -1315,4 +1315,49 @@ describe('Polygon', function () { ); }); }); + + describe('Test isWalletAddress', function () { + it('verify address for tpolygon', async function () { + const keychains = [ + { + id: '6920c05e0b195abd8ed0bb0a5df32cdc', + source: 'user', + type: 'tss', + commonKeychain: + '02f929f92c25eaccb0e3ebaa0f5ed900c5ad30ba94f8444dc89c94e12f01aa5371522d810b8918ecc9e41eb901352df1c7977420fbaf9b8617f61b780b32b2ccad', + }, + { + id: '6920c05e93c3b1c9e5006bf00a3cf016', + source: 'backup', + type: 'tss', + commonKeychain: + '02f929f92c25eaccb0e3ebaa0f5ed900c5ad30ba94f8444dc89c94e12f01aa5371522d810b8918ecc9e41eb901352df1c7977420fbaf9b8617f61b780b32b2ccad', + }, + { + id: '6920c05d9ebee0100ec4a8aa7e300c02', + source: 'bitgo', + type: 'tss', + commonKeychain: + '02f929f92c25eaccb0e3ebaa0f5ed900c5ad30ba94f8444dc89c94e12f01aa5371522d810b8918ecc9e41eb901352df1c7977420fbaf9b8617f61b780b32b2ccad', + }, + ]; + + const params = { + address: '0x4e9fc44697f4135455157396485f6fe8f909752f', + baseAddress: '0x8cf5ebd51585d159c4a1ca36178f9ad0fd7a594c', + coinSpecific: { + salt: '0xd', + forwarderVersion: 4, + feeAddress: '0x44dcb3504e323a3d70142036a99e2d4bba3f2270', + }, + keychains, + index: 13, + walletVersion: 5, + }; + + const coin = bitgo.coin('tpolygon'); + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + }); }); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index cc75c6cef9..36a26e9b97 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -1186,6 +1186,8 @@ class Polygon extends Mainnet implements EthereumNetwork { batcherContractAddress = '0x3e1e5d78e44f15593b3b61ed278f12c27f0ff33e'; nativeCoinOperationHashPrefix = 'POLYGON'; tokenOperationHashPrefix = 'POLYGON-ERC20'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; } class PolygonTestnet extends Testnet implements EthereumNetwork { @@ -1201,6 +1203,8 @@ class PolygonTestnet extends Testnet implements EthereumNetwork { batcherContractAddress = '0x3e1e5d78e44f15593b3b61ed278f12c27f0ff33e'; nativeCoinOperationHashPrefix = 'POLYGON'; tokenOperationHashPrefix = 'POLYGON-ERC20'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; } class Optimism extends Mainnet implements EthereumNetwork { @@ -1215,6 +1219,8 @@ class Optimism extends Mainnet implements EthereumNetwork { forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; walletImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; batcherContractAddress = '0xebe27913fcc7510eadf10643a8f86bf5492a9541'; } @@ -1230,6 +1236,8 @@ class OptimismTestnet extends Testnet implements EthereumNetwork { forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; walletImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; batcherContractAddress = '0xebe27913fcc7510eadf10643a8f86bf5492a9541'; } @@ -1253,6 +1261,8 @@ class ZkSyncTestnet extends Testnet implements EthereumNetwork { tokenOperationHashPrefix = '300-ERC20'; forwarderFactoryAddress = '0xdd498702f44c4da08eb9e08d3f015eefe5cb71fc'; forwarderImplementationAddress = '0xbe69cae311191fb45e648ed20847f06fad2dbab4'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x4550e1e7616d3364877fc6c9324938dab678621a'; walletImplementationAddress = '0x92db2759d1dca129a0d9d46877f361be819184c4'; } From 0fee53fdde13bf320e573c59bd72c75643f4a0d2 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 25 Nov 2025 00:08:20 -0500 Subject: [PATCH 3/6] feat: add coverage for base address TICKET: WP-6461 --- .../src/abstractEthLikeNewCoins.ts | 296 +++++++++++++++--- modules/sdk-coin-eth/test/unit/eth.ts | 281 ++++++++++++++++- modules/statics/src/networks.ts | 36 ++- 3 files changed, 558 insertions(+), 55 deletions(-) diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 1e76007eb3..0ad5b8fe56 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -411,6 +411,40 @@ export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions { export type TssVerifyEthAddressOptions = TssVerifyAddressOptions & VerifyEthAddressOptions; +/** + * Keychain with ethAddress for BIP32 wallet verification (V1, V2, V4) + * Used for wallets that derive addresses using Ethereum addresses from keychains + */ +export interface KeychainWithEthAddress { + ethAddress: string; + pub: string; +} + +/** + * BIP32 wallet base address verification options + * Supports V1, V2, and V4 wallets that use ethAddress-based derivation + */ +export interface VerifyBip32BaseAddressOptions extends VerifyEthAddressOptions { + walletVersion: number; + keychains: KeychainWithEthAddress[]; +} + +/** + * Type guard to check if params are for BIP32 base address verification (V1, V2, V4) + * These wallet versions use ethAddress for address derivation + */ +export function isVerifyBip32BaseAddressOptions( + params: VerifyEthAddressOptions | TssVerifyEthAddressOptions +): params is VerifyBip32BaseAddressOptions { + return ( + (params.walletVersion === 1 || params.walletVersion === 2 || params.walletVersion === 4) && + 'keychains' in params && + Array.isArray(params.keychains) && + params.keychains.length === 3 && + params.keychains.every((kc: any) => 'ethAddress' in kc && typeof kc.ethAddress === 'string') + ); +} + const debug = debugLib('bitgo:v2:ethlike'); export const optionalDeps = { @@ -2732,23 +2766,176 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { return {}; } - getForwarderFactoryAndImplContractAddresses(walletVersion: number | undefined): { + /** + * Get forwarder factory and implementation addresses for deposit address verification. + * Forwarders are smart contracts that forward funds to the base wallet address. + * + * @param {number | undefined} walletVersion - The wallet version + * @returns {object} Factory and implementation addresses for forwarders + */ + getForwarderFactoryAddressesAndForwarderImplementationAddress(walletVersion: number | undefined): { forwarderFactoryAddress: string; forwarderImplementationAddress: string; } { const ethNetwork = this.getNetwork(); - if (walletVersion && (walletVersion === 5 || walletVersion === 4)) { - if (ethNetwork?.walletV4ForwarderFactoryAddress && ethNetwork?.walletV4ForwarderImplementationAddress) { + + switch (walletVersion) { + case 2: + if (!ethNetwork?.walletV2ForwarderFactoryAddress || !ethNetwork?.walletV2ForwarderImplementationAddress) { + throw new Error('Wallet v2 factory addresses not configured for this network'); + } return { - forwarderFactoryAddress: ethNetwork?.walletV4ForwarderFactoryAddress as string, - forwarderImplementationAddress: ethNetwork?.walletV4ForwarderImplementationAddress as string, + forwarderFactoryAddress: ethNetwork.walletV2ForwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.walletV2ForwarderImplementationAddress, + }; + case 4: + case 5: + if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) { + throw new Error(`Forwarder v${walletVersion} factory addresses not configured for this network`); + } + return { + forwarderFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress, + }; + default: + if (!ethNetwork?.forwarderFactoryAddress || !ethNetwork?.forwarderImplementationAddress) { + throw new Error('Forwarder factory addresses not configured for this network'); + } + return { + forwarderFactoryAddress: ethNetwork.forwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.forwarderImplementationAddress, }; - } } - return { - forwarderFactoryAddress: ethNetwork?.forwarderFactoryAddress as string, - forwarderImplementationAddress: ethNetwork?.forwarderImplementationAddress as string, - }; + } + + /** + * Get wallet base address factory and implementation addresses. + * This is used for base address verification for V1, V2, V4, and V5 wallets. + * The base address is the main wallet contract deployed via CREATE2. + * + * @param {number} walletVersion - The wallet version (1, 2, 4, or 5) + * @returns {object} Factory and implementation addresses for the wallet base address + * @throws {Error} if wallet version addresses are not configured + */ + getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion: number): { + walletFactoryAddress: string; + walletImplementationAddress: string; + } { + const ethNetwork = this.getNetwork(); + + switch (walletVersion) { + case 2: + if (!ethNetwork?.walletV2FactoryAddress || !ethNetwork?.walletV2ImplementationAddress) { + throw new Error('Wallet v2 factory addresses not configured for this network'); + } + return { + walletFactoryAddress: ethNetwork.walletV2FactoryAddress, + walletImplementationAddress: ethNetwork.walletV2ImplementationAddress, + }; + case 4: + case 5: + if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) { + throw new Error(`Wallet v${walletVersion} factory addresses not configured for this network`); + } + return { + walletFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress, + walletImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress, + }; + default: + if (!ethNetwork?.walletFactoryAddress || !ethNetwork?.walletImplementationAddress) { + throw new Error('Wallet v1 factory addresses not configured for this network'); + } + return { + walletFactoryAddress: ethNetwork.walletFactoryAddress, + walletImplementationAddress: ethNetwork.walletImplementationAddress, + }; + } + } + + /** + * Helper method to create a salt buffer from hex string. + * Converts a hex salt string to a 32-byte buffer. + * + * @param {string} salt - The hex salt string + * @returns {Buffer} 32-byte salt buffer + */ + private createSaltBuffer(salt: string): Buffer { + const ethUtil = optionalDeps.ethUtil; + return ethUtil.setLengthLeft(Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(salt || '')), 'hex'), 32); + } + + /** + * Verify BIP32 wallet base address (V1, V2, V4). + * These wallets use a wallet factory to deploy base addresses with CREATE2. + * The address is derived from the keychains' ethAddresses and a salt. + * + * @param {VerifyBip32BaseAddressOptions} params - Verification parameters + * @returns {object} Expected and actual addresses for comparison + */ + private verifyBip32BaseAddress(params: VerifyBip32BaseAddressOptions): { + expectedAddress: string; + actualAddress: string; + } { + const { address, coinSpecific, keychains, walletVersion } = params; + + if (!coinSpecific.salt) { + throw new Error(`missing salt for v${walletVersion} base address verification`); + } + + // Get wallet factory and implementation addresses for the wallet version + const { walletFactoryAddress, walletImplementationAddress } = + this.getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion); + const initcode = getProxyInitcode(walletImplementationAddress); + + // Convert the wallet salt to a buffer, pad to 32 bytes + const saltBuffer = this.createSaltBuffer(coinSpecific.salt); + + // Reconstruct calculationSalt using keychains' ethAddresses and wallet salt + const ethAddresses = keychains.map((kc) => { + if (!kc.ethAddress) { + throw new Error(`keychain missing ethAddress for v${walletVersion} base address verification`); + } + return kc.ethAddress; + }); + + const calculationSalt = optionalDeps.ethUtil.bufferToHex( + optionalDeps.ethAbi.soliditySHA3(['address[]', 'bytes32'], [ethAddresses, saltBuffer]) + ); + + const expectedAddress = calculateForwarderV1Address(walletFactoryAddress, calculationSalt, initcode); + return { expectedAddress, actualAddress: address }; + } + + /** + * Verify forwarder receive address (deposit address). + * Forwarder addresses are derived using CREATE2 from the base address and salt. + * + * @param {VerifyEthAddressOptions} params - Verification parameters + * @param {number} forwarderVersion - The forwarder version + * @returns {object} Expected and actual addresses for comparison + */ + private verifyForwarderAddress( + params: VerifyEthAddressOptions, + forwarderVersion: number + ): { expectedAddress: string; actualAddress: string } { + const { address, coinSpecific, baseAddress } = params; + + const { forwarderFactoryAddress, forwarderImplementationAddress } = + this.getForwarderFactoryAddressesAndForwarderImplementationAddress(params.walletVersion); + const initcode = getProxyInitcode(forwarderImplementationAddress); + const saltBuffer = this.createSaltBuffer(coinSpecific.salt || ''); + + const { createForwarderParams, createForwarderTypes } = + forwarderVersion === 4 + ? getCreateForwarderParamsAndTypes(baseAddress, saltBuffer, coinSpecific.feeAddress) + : getCreateForwarderParamsAndTypes(baseAddress, saltBuffer); + + const calculationSalt = optionalDeps.ethUtil.bufferToHex( + optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams) + ); + + const expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode); + return { expectedAddress, actualAddress: address }; } /** @@ -2763,66 +2950,79 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @returns {boolean} True iff address is a wallet address */ async isWalletAddress(params: VerifyEthAddressOptions | TssVerifyEthAddressOptions): Promise { - const ethUtil = optionalDeps.ethUtil; - - let expectedAddress; - let actualAddress; - - const { address, impliedForwarderVersion, coinSpecific } = params; + const { address, impliedForwarderVersion, coinSpecific, baseAddress } = params; const forwarderVersion = impliedForwarderVersion ?? coinSpecific?.forwarderVersion; + // Validate address format if (address && !this.isValidAddress(address)) { throw new InvalidAddressError(`invalid address: ${address}`); } + // Forwarder version 0 addresses cannot be verified because we do not store the nonce value required for address derivation. if (forwarderVersion === 0) { return true; } - // Verify MPC wallet address for wallet version 3 and 6 - if (isTssVerifyAddressOptions(params) && params.walletVersion !== 5) { + + // Determine if we are verifying a base address + const isVerifyingBaseAddress = baseAddress && address === baseAddress; + + // TSS/MPC wallet address verification (V3, V5, V6) + // V5 base addresses use TSS, but V5 forwarders use the regular forwarder verification + const isTssWalletVersion = params.walletVersion === 3 || params.walletVersion === 5 || params.walletVersion === 6; + const shouldUseTssVerification = + isTssVerifyAddressOptions(params) && isTssWalletVersion && (params.walletVersion !== 5 || isVerifyingBaseAddress); + + if (shouldUseTssVerification) { + if (isVerifyingBaseAddress) { + const index = typeof params.index === 'string' ? parseInt(params.index, 10) : params.index; + if (index !== 0) { + throw new Error( + `Base address verification requires index 0, but got index ${params.index}. ` + + `The base address is always derived at index 0.` + ); + } + } + return verifyMPCWalletAddress({ ...params, keyCurve: 'secp256k1' }, this.isValidAddress, (pubKey) => { return new KeyPairLib({ pub: pubKey }).getAddress(); }); - } else { - // Verify forwarder receive address - const { coinSpecific, baseAddress } = params; - - if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) { - throw new InvalidAddressError('invalid base address'); - } + } - if (!_.isObject(coinSpecific)) { - throw new InvalidAddressVerificationObjectPropertyError( - 'address validation failure: coinSpecific field must be an object' - ); - } + // From here on, we need baseAddress and coinSpecific for non-TSS verifications + if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) { + throw new InvalidAddressError('invalid base address'); + } - const { forwarderFactoryAddress, forwarderImplementationAddress } = - this.getForwarderFactoryAndImplContractAddresses(params.walletVersion); - const initcode = getProxyInitcode(forwarderImplementationAddress); - const saltBuffer = ethUtil.setLengthLeft( - Buffer.from(ethUtil.padToEven(ethUtil.stripHexPrefix(coinSpecific.salt || '')), 'hex'), - 32 + if (!_.isObject(coinSpecific)) { + throw new InvalidAddressVerificationObjectPropertyError( + 'address validation failure: coinSpecific field must be an object' ); + } - const { createForwarderParams, createForwarderTypes } = - forwarderVersion === 4 - ? getCreateForwarderParamsAndTypes(baseAddress, saltBuffer, coinSpecific.feeAddress) - : getCreateForwarderParamsAndTypes(baseAddress, saltBuffer); + // BIP32 wallet base address verification (V1, V2, V4) + if (isVerifyingBaseAddress && isVerifyBip32BaseAddressOptions(params)) { + const { expectedAddress, actualAddress } = this.verifyBip32BaseAddress(params); - const calculationSalt = optionalDeps.ethUtil.bufferToHex( - optionalDeps.ethAbi.soliditySHA3(createForwarderTypes, createForwarderParams) - ); + if (expectedAddress !== actualAddress) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } - expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode); - actualAddress = address; + return true; } - if (expectedAddress !== actualAddress) { - throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + // Forwarder receive address verification (deposit addresses) + if (!isVerifyingBaseAddress) { + const { expectedAddress, actualAddress } = this.verifyForwarderAddress(params, forwarderVersion); + + if (expectedAddress !== actualAddress) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } + + return true; } - return true; + // If we reach here, it's a base address verification for an unsupported wallet version + throw new Error(`Base address verification not supported for wallet version ${params.walletVersion}`); } /** diff --git a/modules/sdk-coin-eth/test/unit/eth.ts b/modules/sdk-coin-eth/test/unit/eth.ts index cac0c46241..b9ee890f23 100644 --- a/modules/sdk-coin-eth/test/unit/eth.ts +++ b/modules/sdk-coin-eth/test/unit/eth.ts @@ -831,11 +831,12 @@ describe('ETH:', function () { }); it('should reject when actual address differs from expected address', async function () { - const coin = bitgo.coin('teth') as Teth; + const coin = bitgo.coin('hteth') as Hteth; const params = { address: '0x28904591f735994f050804fda3b61b813b16e04c', baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e', + walletVersion: 1, coinSpecific: { salt: '0xc5a', forwarderVersion: 1, @@ -1017,6 +1018,284 @@ describe('ETH:', function () { await assert.rejects(async () => coin.isWalletAddress(params), Error); }); }); + + describe('Base Address Verification', function () { + it('should verify base address for wallet version 6 (TSS)', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const keychains = [ + { + id: '691f630e0c56098288a9b7fa107db144', + source: 'user', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630f1d3c9fce8f5a730bff826cf9', + source: 'backup', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630d56735e5eb61b06e353fe7639', + source: 'bitgo', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + ]; + + const baseAddress = '0xc012041dac143a59fa491db3a2b67b69bd78b685'; + + const params = { + address: baseAddress, + baseAddress: baseAddress, + coinSpecific: { + salt: '0x0', + forwarderVersion: 5, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 0, + walletVersion: 6, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + + it('should verify base address for wallet version 5 (TSS)', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const keychains = [ + { + id: '691e242d93f8d7ad0705887449763c96', + source: 'user', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + { + id: '691e242df8b6323d4b08df366864af66', + source: 'backup', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + { + id: '691e242c0595b4cfee6f957c1d6458f7', + source: 'bitgo', + type: 'tss', + commonKeychain: + '02c8a496b16abfe2567520a279e2154642fc3c0e08e629775cb4d845c0c5fbf55ab7ba153e886de65748ed18f4ff8f5cee2242e687399ea3297a1f5524fdefd56c', + }, + ]; + + const baseAddress = '0xf1e3d30798acdf3a12fa5beb5fad8efb23d5be11'; + + const params = { + address: baseAddress, + baseAddress: baseAddress, + coinSpecific: { + salt: '0x0', + forwarderVersion: 4, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 0, + walletVersion: 5, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + + it('should reject base address verification with non-zero index', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const keychains = [ + { + id: '691f630e0c56098288a9b7fa107db144', + source: 'user', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630f1d3c9fce8f5a730bff826cf9', + source: 'backup', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630d56735e5eb61b06e353fe7639', + source: 'bitgo', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + ]; + + const baseAddress = '0xc012041dac143a59fa491db3a2b67b69bd78b685'; + + const params = { + address: baseAddress, + baseAddress: baseAddress, + coinSpecific: { + salt: '0x0', + forwarderVersion: 5, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 5, // Wrong index - should be 0 for base address + walletVersion: 6, + } as unknown as TssVerifyEthAddressOptions; + + await assert.rejects( + async () => coin.isWalletAddress(params), + /Base address verification requires index 0, but got index 5/ + ); + }); + + it('should reject base address verification with wrong address', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const keychains = [ + { + id: '691f630e0c56098288a9b7fa107db144', + source: 'user', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630f1d3c9fce8f5a730bff826cf9', + source: 'backup', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + { + id: '691f630d56735e5eb61b06e353fe7639', + source: 'bitgo', + type: 'tss', + commonKeychain: + '033b02aac4f038fef5118350b77d302ec6202931ca2e7122aad88994ffefcbc70a6069e662436236abb1619195232c41580204cb202c22357ed8f53e69eac5c69e', + }, + ]; + + const wrongAddress = '0x0000000000000000000000000000000000000001'; + const actualBaseAddress = '0xc012041dac143a59fa491db3a2b67b69bd78b685'; + + const params = { + address: wrongAddress, + baseAddress: actualBaseAddress, + coinSpecific: { + salt: '0x0', + forwarderVersion: 5, + feeAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + keychains, + index: 0, + walletVersion: 6, + } as unknown as TssVerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(false); + }); + + it('should verify base address for wallet version 2 (BIP32) using wallet factory', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const baseAddress = '0xdc485da076ed4a2b19584e9a1fdbb974f89b60f4'; + const walletSalt = '0x2'; + + const keychains = [ + { + id: '691e8c7b3c8aaa791118d9ce616d3b21', + source: 'user', + type: 'independent', + ethAddress: '0x9d16bb867b792c5e3bf636a0275f2db8601bd7d4', + }, + { + id: '691e8c7b1967fd6d9867a22a1a4131a0', + source: 'backup', + type: 'independent', + ethAddress: '0x2dfce5cfeb5c03fbe680cd39ac0d2b25399b7d22', + }, + { + id: '68b9ec587d0ba1c7440de551068c36a7', + source: 'bitgo', + type: 'independent', + ethAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + }, + ]; + + const params = { + address: baseAddress, + baseAddress: baseAddress, + coinSpecific: { + salt: walletSalt, + forwarderVersion: 2, + }, + keychains: keychains, + index: 0, + walletVersion: 2, + } as unknown as VerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + + it('should verify base address for wallet version 1 (BIP32) using wallet factory', async function () { + const coin = bitgo.coin('hteth') as Hteth; + + const baseAddress = '0xe1253bcce7d87db522fbceec6e55c9f78c376d9f'; + const walletSalt = '0x5'; + + const keychains = [ + { + id: '691f638f1d3c9fce8f5aa691569a99eb', + source: 'user', + type: 'independent', + ethAddress: '0xf45dadce751a317957f2a247ff37cb764b97620d', + pub: 'xpub661MyMwAqRbcGVb3PfCzwiEX94AB1nJQtzVm...', + }, + { + id: '691f638f0b74e73b1f440ea4aceda87e', + source: 'backup', + type: 'independent', + ethAddress: '0x5bdf3ae1d2c2fadeeb70a45872bf4f4252312b55', + pub: 'xpub661MyMwAqRbcF46pRHda3sZbuPzza9A9MiqA...', + }, + { + id: '68b9ec587d0ba1c7440de551068c36a7', + source: 'bitgo', + type: 'independent', + ethAddress: '0xb1e725186990b86ca8efed08a3ccda9c9f400f09', + pub: 'xpub661MyMwAqRbcGzTn5eyNGDkb18R43nH79Hok...', + }, + ]; + + const params = { + address: baseAddress, + baseAddress: baseAddress, + coinSpecific: { + salt: walletSalt, + forwarderVersion: 1, + }, + keychains: keychains, + index: 0, + walletVersion: 1, + } as unknown as VerifyEthAddressOptions; + + const isWalletAddr = await coin.isWalletAddress(params); + isWalletAddr.should.equal(true); + }); + }); }); }); diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index 36a26e9b97..142d5b55ff 100644 --- a/modules/statics/src/networks.ts +++ b/modules/statics/src/networks.ts @@ -152,10 +152,18 @@ export interface EthereumNetwork extends AccountNetwork { // forwarder configuration addresses used for calculating forwarder version 1 addresses readonly forwarderFactoryAddress?: string; readonly forwarderImplementationAddress?: string; - readonly nativeCoinOperationHashPrefix?: string; - readonly tokenOperationHashPrefix?: string; + readonly walletV2ForwarderFactoryAddress?: string; + readonly walletV2ForwarderImplementationAddress?: string; readonly walletV4ForwarderFactoryAddress?: string; readonly walletV4ForwarderImplementationAddress?: string; + readonly walletFactoryAddress?: string; + readonly walletImplementationAddress?: string; + readonly walletV2FactoryAddress?: string; + readonly walletV2ImplementationAddress?: string; + readonly walletV4FactoryAddress?: string; + readonly walletV4ImplementationAddress?: string; + readonly nativeCoinOperationHashPrefix?: string; + readonly tokenOperationHashPrefix?: string; } export interface TronNetwork extends AccountNetwork { @@ -578,10 +586,18 @@ class Ethereum extends Mainnet implements EthereumNetwork { batcherContractAddress = '0xebe27913fcc7510eadf10643a8f86bf5492a9541'; forwarderFactoryAddress = '0xffa397285ce46fb78c588a9e993286aac68c37cd'; forwarderImplementationAddress = '0x059ffafdc6ef594230de44f824e2bd0a51ca5ded'; - nativeCoinOperationHashPrefix = 'ETHER'; - tokenOperationHashPrefix = 'ERC20'; + walletV2ForwarderFactoryAddress = '0x29Ef46035e9fA3D570c598d3266424Ca11413b0C'; + walletV2ForwarderImplementationAddress = '0x5397d0869aBA0D55e96D5716d383F6e1d8695ed7'; walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; + nativeCoinOperationHashPrefix = 'ETHER'; + tokenOperationHashPrefix = 'ERC20'; + walletFactoryAddress = '0x9a0d63911620f7fc15c3c020edbe4d7267ea3e4d'; + walletImplementationAddress = '0xe8e847cf573fc8ed75621660a36affd18c543d7e'; + walletV2FactoryAddress = '0xa7198f48C58E91f01317E70Cd24C5Cce475c1555'; + walletV2ImplementationAddress = '0xe5DcdC13B628c2df813DB1080367E929c1507Ca0'; + walletV4FactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; + walletV4ImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; } class Ethereum2 extends Mainnet implements AccountNetwork { @@ -668,10 +684,18 @@ class Hoodi extends Testnet implements EthereumNetwork { batcherContractAddress = '0x3e1e5d78e44f15593b3b61ed278f12c27f0ff33e'; forwarderFactoryAddress = '0x0e2874d6824fae4f61e446012317a9b86384bd8e'; forwarderImplementationAddress = '0x7441f20a59be97011842404b9aefd8d85fd81aa6'; - nativeCoinOperationHashPrefix = 'ETHER'; - tokenOperationHashPrefix = 'ERC20'; + walletV2ForwarderFactoryAddress = '0x0e2874d6824fae4f61e446012317a9b86384bd8e'; + walletV2ForwarderImplementationAddress = '0x7441f20a59be97011842404b9aefd8d85fd81aa6'; walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; + walletFactoryAddress = '0xf514cd80a41bde2e20033b251f1f74633caf3e59'; + walletImplementationAddress = '0x6bb86b3b27b092bf8a285080fe7d58acdf841041'; + walletV2FactoryAddress = '0xf514cd80a41bde2e20033b251f1f74633caf3e59'; + walletV2ImplementationAddress = '0x6bb86b3b27b092bf8a285080fe7d58acdf841041'; + walletV4FactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; + walletV4ImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + nativeCoinOperationHashPrefix = 'ETHER'; + tokenOperationHashPrefix = 'ERC20'; } class EthereumClassic extends Mainnet implements EthereumNetwork { From 74440e7703d4c47eac5218cca5e06bc5ed739c9c Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 25 Nov 2025 00:44:05 -0500 Subject: [PATCH 4/6] refactor: fix test TICKET: WP-6461 --- .../statics/test/unit/resources/amsTokenConfig.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/statics/test/unit/resources/amsTokenConfig.ts b/modules/statics/test/unit/resources/amsTokenConfig.ts index ec31b76c3a..87bd9f96ed 100644 --- a/modules/statics/test/unit/resources/amsTokenConfig.ts +++ b/modules/statics/test/unit/resources/amsTokenConfig.ts @@ -661,10 +661,18 @@ export const amsTokenConfigWithCustomToken = { batcherContractAddress: '0x3e1e5d78e44f15593b3b61ed278f12c27f0ff33e', forwarderFactoryAddress: '0x0e2874d6824fae4f61e446012317a9b86384bd8e', forwarderImplementationAddress: '0x7441f20a59be97011842404b9aefd8d85fd81aa6', - nativeCoinOperationHashPrefix: 'ETHER', - tokenOperationHashPrefix: 'ERC20', + walletV2ForwarderFactoryAddress: '0x0e2874d6824fae4f61e446012317a9b86384bd8e', + walletV2ForwarderImplementationAddress: '0x7441f20a59be97011842404b9aefd8d85fd81aa6', walletV4ForwarderFactoryAddress: '0x37996e762fa8b671869740c79eb33f625b3bf92a', walletV4ForwarderImplementationAddress: '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b', + walletFactoryAddress: '0xf514cd80a41bde2e20033b251f1f74633caf3e59', + walletImplementationAddress: '0x6bb86b3b27b092bf8a285080fe7d58acdf841041', + walletV2FactoryAddress: '0xf514cd80a41bde2e20033b251f1f74633caf3e59', + walletV2ImplementationAddress: '0x6bb86b3b27b092bf8a285080fe7d58acdf841041', + walletV4FactoryAddress: '0x809ee567e413543af1caebcdb247f6a67eafc8dd', + walletV4ImplementationAddress: '0x944fef03af368414f29dc31a72061b8d64f568d2', + nativeCoinOperationHashPrefix: 'ETHER', + tokenOperationHashPrefix: 'ERC20', }, primaryKeyCurve: 'secp256k1', contractAddress: '0x89a959b9184b4f8c8633646d5dfd049d2ebc983a', From db92c3870ae2c9c6abca3c07ec32f5940dbaa69e Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 25 Nov 2025 12:20:44 -0500 Subject: [PATCH 5/6] refactor: renames types and made function naming more clear TICKET: WP-6461 --- .../src/abstractEthLikeNewCoins.ts | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index 0ad5b8fe56..ab096bbea8 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -424,7 +424,7 @@ export interface KeychainWithEthAddress { * BIP32 wallet base address verification options * Supports V1, V2, and V4 wallets that use ethAddress-based derivation */ -export interface VerifyBip32BaseAddressOptions extends VerifyEthAddressOptions { +export interface VerifyContractBaseAddressOptions extends VerifyEthAddressOptions { walletVersion: number; keychains: KeychainWithEthAddress[]; } @@ -433,9 +433,9 @@ export interface VerifyBip32BaseAddressOptions extends VerifyEthAddressOptions { * Type guard to check if params are for BIP32 base address verification (V1, V2, V4) * These wallet versions use ethAddress for address derivation */ -export function isVerifyBip32BaseAddressOptions( +export function isVerifyContractBaseAddressOptions( params: VerifyEthAddressOptions | TssVerifyEthAddressOptions -): params is VerifyBip32BaseAddressOptions { +): params is VerifyContractBaseAddressOptions { return ( (params.walletVersion === 1 || params.walletVersion === 2 || params.walletVersion === 4) && 'keychains' in params && @@ -2770,16 +2770,16 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * Get forwarder factory and implementation addresses for deposit address verification. * Forwarders are smart contracts that forward funds to the base wallet address. * - * @param {number | undefined} walletVersion - The wallet version + * @param {number | undefined} forwarderVersion - The wallet version * @returns {object} Factory and implementation addresses for forwarders */ - getForwarderFactoryAddressesAndForwarderImplementationAddress(walletVersion: number | undefined): { + getForwarderFactoryAddressesAndForwarderImplementationAddress(forwarderVersion: number | undefined): { forwarderFactoryAddress: string; forwarderImplementationAddress: string; } { const ethNetwork = this.getNetwork(); - switch (walletVersion) { + switch (forwarderVersion) { case 2: if (!ethNetwork?.walletV2ForwarderFactoryAddress || !ethNetwork?.walletV2ForwarderImplementationAddress) { throw new Error('Wallet v2 factory addresses not configured for this network'); @@ -2791,7 +2791,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { case 4: case 5: if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) { - throw new Error(`Forwarder v${walletVersion} factory addresses not configured for this network`); + throw new Error(`Forwarder v${forwarderVersion} factory addresses not configured for this network`); } return { forwarderFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress, @@ -2817,7 +2817,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @returns {object} Factory and implementation addresses for the wallet base address * @throws {Error} if wallet version addresses are not configured */ - getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion: number): { + getWalletAddressFactoryAddressesAndImplementationAddress(walletVersion: number): { walletFactoryAddress: string; walletImplementationAddress: string; } { @@ -2872,10 +2872,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @param {VerifyBip32BaseAddressOptions} params - Verification parameters * @returns {object} Expected and actual addresses for comparison */ - private verifyBip32BaseAddress(params: VerifyBip32BaseAddressOptions): { - expectedAddress: string; - actualAddress: string; - } { + private verifyCreate2BaseAddress(params: VerifyContractBaseAddressOptions): boolean { const { address, coinSpecific, keychains, walletVersion } = params; if (!coinSpecific.salt) { @@ -2884,7 +2881,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { // Get wallet factory and implementation addresses for the wallet version const { walletFactoryAddress, walletImplementationAddress } = - this.getWalletBaseAddressFactoryAddressesAndImplementationAddress(walletVersion); + this.getWalletAddressFactoryAddressesAndImplementationAddress(walletVersion); const initcode = getProxyInitcode(walletImplementationAddress); // Convert the wallet salt to a buffer, pad to 32 bytes @@ -2903,7 +2900,12 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { ); const expectedAddress = calculateForwarderV1Address(walletFactoryAddress, calculationSalt, initcode); - return { expectedAddress, actualAddress: address }; + + if (expectedAddress !== address) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } + + return true; } /** @@ -2914,14 +2916,11 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @param {number} forwarderVersion - The forwarder version * @returns {object} Expected and actual addresses for comparison */ - private verifyForwarderAddress( - params: VerifyEthAddressOptions, - forwarderVersion: number - ): { expectedAddress: string; actualAddress: string } { + private verifyForwarderAddress(params: VerifyEthAddressOptions, forwarderVersion: number): boolean { const { address, coinSpecific, baseAddress } = params; const { forwarderFactoryAddress, forwarderImplementationAddress } = - this.getForwarderFactoryAddressesAndForwarderImplementationAddress(params.walletVersion); + this.getForwarderFactoryAddressesAndForwarderImplementationAddress(forwarderVersion); const initcode = getProxyInitcode(forwarderImplementationAddress); const saltBuffer = this.createSaltBuffer(coinSpecific.salt || ''); @@ -2935,7 +2934,12 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { ); const expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode); - return { expectedAddress, actualAddress: address }; + + if (expectedAddress !== address) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } + + return true; } /** @@ -3000,25 +3004,13 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { } // BIP32 wallet base address verification (V1, V2, V4) - if (isVerifyingBaseAddress && isVerifyBip32BaseAddressOptions(params)) { - const { expectedAddress, actualAddress } = this.verifyBip32BaseAddress(params); - - if (expectedAddress !== actualAddress) { - throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); - } - - return true; + if (isVerifyingBaseAddress && isVerifyContractBaseAddressOptions(params)) { + return this.verifyCreate2BaseAddress(params); } // Forwarder receive address verification (deposit addresses) if (!isVerifyingBaseAddress) { - const { expectedAddress, actualAddress } = this.verifyForwarderAddress(params, forwarderVersion); - - if (expectedAddress !== actualAddress) { - throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); - } - - return true; + return this.verifyForwarderAddress(params, forwarderVersion); } // If we reach here, it's a base address verification for an unsupported wallet version From cf67e8b8559b5008751c132997fe510c74754f5f Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 25 Nov 2025 14:09:41 -0500 Subject: [PATCH 6/6] refactor: fail if factory address is not setup TICKET: WP-6461 --- .../src/abstractEthLikeNewCoins.ts | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index ab096bbea8..8d2bcbfcc4 100644 --- a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts +++ b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts @@ -2780,6 +2780,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const ethNetwork = this.getNetwork(); switch (forwarderVersion) { + case 1: + if (!ethNetwork?.forwarderFactoryAddress || !ethNetwork?.forwarderImplementationAddress) { + throw new Error('Forwarder factory addresses not configured for this network'); + } + return { + forwarderFactoryAddress: ethNetwork.forwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.forwarderImplementationAddress, + }; case 2: if (!ethNetwork?.walletV2ForwarderFactoryAddress || !ethNetwork?.walletV2ForwarderImplementationAddress) { throw new Error('Wallet v2 factory addresses not configured for this network'); @@ -2798,13 +2806,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { forwarderImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress, }; default: - if (!ethNetwork?.forwarderFactoryAddress || !ethNetwork?.forwarderImplementationAddress) { - throw new Error('Forwarder factory addresses not configured for this network'); - } - return { - forwarderFactoryAddress: ethNetwork.forwarderFactoryAddress, - forwarderImplementationAddress: ethNetwork.forwarderImplementationAddress, - }; + throw new Error(`Forwarder version ${forwarderVersion} not supported`); } } @@ -2824,6 +2826,14 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { const ethNetwork = this.getNetwork(); switch (walletVersion) { + case 1: + if (!ethNetwork?.walletFactoryAddress || !ethNetwork?.walletImplementationAddress) { + throw new Error('Wallet v1 factory addresses not configured for this network'); + } + return { + walletFactoryAddress: ethNetwork.walletFactoryAddress, + walletImplementationAddress: ethNetwork.walletImplementationAddress, + }; case 2: if (!ethNetwork?.walletV2FactoryAddress || !ethNetwork?.walletV2ImplementationAddress) { throw new Error('Wallet v2 factory addresses not configured for this network'); @@ -2842,13 +2852,7 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { walletImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress, }; default: - if (!ethNetwork?.walletFactoryAddress || !ethNetwork?.walletImplementationAddress) { - throw new Error('Wallet v1 factory addresses not configured for this network'); - } - return { - walletFactoryAddress: ethNetwork.walletFactoryAddress, - walletImplementationAddress: ethNetwork.walletImplementationAddress, - }; + throw new Error(`Wallet version ${walletVersion} not supported`); } }