diff --git a/modules/abstract-eth/src/abstractEthLikeNewCoins.ts b/modules/abstract-eth/src/abstractEthLikeNewCoins.ts index be9a301d33..8d2bcbfcc4 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,7 +405,44 @@ export interface EthConsolidationRecoveryOptions { export interface VerifyEthAddressOptions extends BaseVerifyAddressOptions { baseAddress: string; coinSpecific: EthAddressCoinSpecifics; - forwarderVersion: number; + forwarderVersion?: number; + walletVersion?: number; +} + +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 VerifyContractBaseAddressOptions 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 isVerifyContractBaseAddressOptions( + params: VerifyEthAddressOptions | TssVerifyEthAddressOptions +): params is VerifyContractBaseAddressOptions { + 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'); @@ -2725,6 +2766,186 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { return {}; } + /** + * 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} forwarderVersion - The wallet version + * @returns {object} Factory and implementation addresses for forwarders + */ + getForwarderFactoryAddressesAndForwarderImplementationAddress(forwarderVersion: number | undefined): { + forwarderFactoryAddress: string; + forwarderImplementationAddress: string; + } { + 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'); + } + return { + forwarderFactoryAddress: ethNetwork.walletV2ForwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.walletV2ForwarderImplementationAddress, + }; + case 4: + case 5: + if (!ethNetwork?.walletV4ForwarderFactoryAddress || !ethNetwork?.walletV4ForwarderImplementationAddress) { + throw new Error(`Forwarder v${forwarderVersion} factory addresses not configured for this network`); + } + return { + forwarderFactoryAddress: ethNetwork.walletV4ForwarderFactoryAddress, + forwarderImplementationAddress: ethNetwork.walletV4ForwarderImplementationAddress, + }; + default: + throw new Error(`Forwarder version ${forwarderVersion} not supported`); + } + } + + /** + * 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 + */ + getWalletAddressFactoryAddressesAndImplementationAddress(walletVersion: number): { + walletFactoryAddress: string; + walletImplementationAddress: string; + } { + 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'); + } + 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: + throw new Error(`Wallet version ${walletVersion} not supported`); + } + } + + /** + * 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 verifyCreate2BaseAddress(params: VerifyContractBaseAddressOptions): boolean { + 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.getWalletAddressFactoryAddressesAndImplementationAddress(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); + + if (expectedAddress !== address) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } + + return true; + } + + /** + * 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): boolean { + const { address, coinSpecific, baseAddress } = params; + + const { forwarderFactoryAddress, forwarderImplementationAddress } = + this.getForwarderFactoryAddressesAndForwarderImplementationAddress(forwarderVersion); + 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); + + if (expectedAddress !== address) { + throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + } + + return true; + } + /** * Make sure an address is a wallet address and throw an error if it's not. * @param {Object} params @@ -2736,19 +2957,46 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { * @throws {UnexpectedAddressError} * @returns {boolean} True iff address is a wallet address */ - async isWalletAddress(params: VerifyEthAddressOptions): Promise { - const ethUtil = optionalDeps.ethUtil; - - let expectedAddress; - let actualAddress; - - const { address, coinSpecific, baseAddress, impliedForwarderVersion = coinSpecific?.forwarderVersion } = params; + async isWalletAddress(params: VerifyEthAddressOptions | TssVerifyEthAddressOptions): Promise { + 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}`); } - // base address is required to calculate the salt which is used in calculateForwarderV1Address method + // Forwarder version 0 addresses cannot be verified because we do not store the nonce value required for address derivation. + if (forwarderVersion === 0) { + return true; + } + + // 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(); + }); + } + + // From here on, we need baseAddress and coinSpecific for non-TSS verifications if (_.isUndefined(baseAddress) || !this.isValidAddress(baseAddress)) { throw new InvalidAddressError('invalid base address'); } @@ -2759,33 +3007,18 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin { ); } - 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; - - 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 calculationSalt = optionalDeps.ethUtil.bufferToHex( - optionalDeps.ethAbi.soliditySHA3(['address', 'bytes32'], [baseAddress, saltBuffer]) - ); - - expectedAddress = calculateForwarderV1Address(forwarderFactoryAddress, calculationSalt, initcode); - actualAddress = address; + // BIP32 wallet base address verification (V1, V2, V4) + if (isVerifyingBaseAddress && isVerifyContractBaseAddressOptions(params)) { + return this.verifyCreate2BaseAddress(params); } - if (expectedAddress !== actualAddress) { - throw new UnexpectedAddressError(`address validation failure: expected ${expectedAddress} but got ${address}`); + // Forwarder receive address verification (deposit addresses) + if (!isVerifyingBaseAddress) { + return this.verifyForwarderAddress(params, forwarderVersion); } - 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}`); } /** @@ -3056,7 +3289,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..b9ee890f23 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,638 @@ 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('hteth') as Hteth; - 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', + walletVersion: 1, + 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, + }, + }; + + 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); + }); + }); + + 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; - assert.rejects(async () => coin.verifyAddress(params), InvalidAddressError); + 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/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/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'; diff --git a/modules/statics/src/networks.ts b/modules/statics/src/networks.ts index cc75c6cef9..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 { @@ -1186,6 +1210,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 +1227,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 +1243,8 @@ class Optimism extends Mainnet implements EthereumNetwork { forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; walletImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; batcherContractAddress = '0xebe27913fcc7510eadf10643a8f86bf5492a9541'; } @@ -1230,6 +1260,8 @@ class OptimismTestnet extends Testnet implements EthereumNetwork { forwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x809ee567e413543af1caebcdb247f6a67eafc8dd'; walletImplementationAddress = '0x944fef03af368414f29dc31a72061b8d64f568d2'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; batcherContractAddress = '0xebe27913fcc7510eadf10643a8f86bf5492a9541'; } @@ -1253,6 +1285,8 @@ class ZkSyncTestnet extends Testnet implements EthereumNetwork { tokenOperationHashPrefix = '300-ERC20'; forwarderFactoryAddress = '0xdd498702f44c4da08eb9e08d3f015eefe5cb71fc'; forwarderImplementationAddress = '0xbe69cae311191fb45e648ed20847f06fad2dbab4'; + walletV4ForwarderFactoryAddress = '0x37996e762fa8b671869740c79eb33f625b3bf92a'; + walletV4ForwarderImplementationAddress = '0xd5fe1c1f216b775dfd30638fa7164d41321ef79b'; walletFactoryAddress = '0x4550e1e7616d3364877fc6c9324938dab678621a'; walletImplementationAddress = '0x92db2759d1dca129a0d9d46877f361be819184c4'; } 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',