From 35df3004693543d8526da7c371e8a55f4fa510e0 Mon Sep 17 00:00:00 2001 From: Antonio Date: Fri, 16 Feb 2024 11:24:40 +0100 Subject: [PATCH] feat: parametrized Safe version --- src/generateEphemeralPrivateKey.ts | 21 +++++++++++++++++++++ src/predictStealthSafeAddress.ts | 12 ++++++++++-- src/predictStealthSafeAddressTypes.ts | 1 + test/predictStealthSafeAddress.test.ts | 11 +++++++++++ 4 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/predictStealthSafeAddressTypes.ts diff --git a/src/generateEphemeralPrivateKey.ts b/src/generateEphemeralPrivateKey.ts index 1963983..55a4096 100644 --- a/src/generateEphemeralPrivateKey.ts +++ b/src/generateEphemeralPrivateKey.ts @@ -38,6 +38,27 @@ export function generateEphemeralPrivateKey({ const coinTypePart1 = parseInt(coinTypeString.slice(0, 1), 16); const coinTypePart2 = parseInt(coinTypeString.slice(1), 16); + // key derivation structure is m/5564'/N'/c0'/c1'/0'/p/n + + // 5564 is the purpose as defined in BIP-43 and aligns with the stealth address EIP number, EIP-5564: + // https://github.com/bitcoin/bips/blob/master/bip-0043.mediawiki + // https://eips.ethereum.org/EIPS/eip-5564 + + // N is the number of the node shared with the server + + // c is the coinType as derived in ENSIP-11 for EVM chains: https://docs.ens.domains/ens-improvement-proposals/ensip-11-evmchain-address-resolution + // as it is a number above 0x80000000, which is not allowed in secure-bip32 (see HARDENED_OFFSET), it is split into two parts: + // c0, the first byte of c + // c1, the remaining bytes of c + + // only the node m/5564'/0' of the private viewing key is shared with the server (see extractPrivateKeyNode) + // the server then generates pseudo-random addresses by incrementing n + // since each value cannot be larger than 2^31 - 1 (see HARDENED_OFFSET - 0x7FFFFFF) + // we therefore introduce a parent nonce p to allow for more addresses to be generated. + // If the nonce is bigger than n, we put the overflow part into a parentNonce. To simplify + // the creation of parentNonce, we set MAX_N to be 0xFFFFFFF. With this schema the combination of nonce + // and parent nonce has a max value of 0x7FFFFFFFFFFFFFF = 576460752303423487₁₀ + // Split the nonce into two parts to ensure no number above 0x80000000 is used in the derivation path const MAX_NONCE = 0xfffffff; let parentNonce = 0; diff --git a/src/predictStealthSafeAddress.ts b/src/predictStealthSafeAddress.ts index c3a8870..8349d25 100644 --- a/src/predictStealthSafeAddress.ts +++ b/src/predictStealthSafeAddress.ts @@ -15,6 +15,7 @@ import { toBytes, } from 'viem'; import * as chains from 'viem/chains'; +import { SafeVersion } from './predictStealthSafeAddressTypes'; /** * Using Viem transaction simulation, predict a new Safe address using the parameters passed in input. @@ -32,12 +33,14 @@ export async function predictStealthSafeAddressWithClient({ stealthAddresses, transport, useDefaultAddress, + safeVersion, }: { threshold: number; stealthAddresses: string[]; chainId?: number; transport?: any; useDefaultAddress?: boolean; + safeVersion: SafeVersion; }): Promise<{ stealthSafeAddress: `0x${string}` }> { // if useDefaultAddress is false, chainId is required @@ -53,6 +56,7 @@ export async function predictStealthSafeAddressWithClient({ threshold, stealthAddresses, useDefaultAddress, + safeVersion, }); const safeSingletonAddress = useDefaultAddress ? safeSingleton.defaultAddress : safeSingleton.networkAddresses[chainId.toString()]; @@ -109,12 +113,14 @@ export function predictStealthSafeAddressWithBytecode({ threshold, stealthAddresses, useDefaultAddress, + safeVersion, }: { safeProxyBytecode: `0x${string}`; threshold: number; stealthAddresses: string[]; chainId?: number; useDefaultAddress?: boolean; + safeVersion: SafeVersion; }): { stealthSafeAddress: `0x${string}` } { // if useDefaultAddress is false, chainId is required if (!useDefaultAddress) { @@ -129,6 +135,7 @@ export function predictStealthSafeAddressWithBytecode({ threshold, stealthAddresses, useDefaultAddress, + safeVersion, }); const safeSingletonAddress = useDefaultAddress ? safeSingleton.defaultAddress : safeSingleton.networkAddresses[chainId.toString()]; @@ -170,11 +177,13 @@ function getSafeInitializerData ({ threshold, stealthAddresses, useDefaultAddress, + safeVersion, }: { threshold: number; stealthAddresses: string[]; chainId?: number; useDefaultAddress?: boolean; + safeVersion: SafeVersion; }): { initializer: `0x${string}`; proxyFactory: SingletonDeployment; @@ -182,7 +191,6 @@ function getSafeInitializerData ({ } { // Constants const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; - const SAFE_VERSION = '1.3.0'; // if useDefaultAddress is false, chainId is required if (!useDefaultAddress) { @@ -195,7 +203,7 @@ function getSafeInitializerData ({ // Configuration to fetch the Safe contracts const configuration = { network: chainId.toString(), - version: SAFE_VERSION, + version: safeVersion, released: true, }; diff --git a/src/predictStealthSafeAddressTypes.ts b/src/predictStealthSafeAddressTypes.ts new file mode 100644 index 0000000..78c6e23 --- /dev/null +++ b/src/predictStealthSafeAddressTypes.ts @@ -0,0 +1 @@ +export type SafeVersion = '1.4.1' | '1.3.0' | '1.2.0' | '1.1.1' | '1.0.0'; diff --git a/test/predictStealthSafeAddress.test.ts b/test/predictStealthSafeAddress.test.ts index a2dadbf..e63424c 100644 --- a/test/predictStealthSafeAddress.test.ts +++ b/test/predictStealthSafeAddress.test.ts @@ -17,6 +17,7 @@ describe('predictStealthSafeAddressWithClient', () => { chainId, threshold, stealthAddresses, + safeVersion: '1.3.0', }); expect(result).toEqual({ stealthSafeAddress: expectedStealthSafeAddress }); @@ -35,12 +36,14 @@ describe('predictStealthSafeAddressWithClient', () => { useDefaultAddress: true, threshold, stealthAddresses, + safeVersion: '1.3.0', }); const resultWithChainMainnet = await predictStealthSafeAddressWithClient({ chainId: 1, threshold, stealthAddresses, + safeVersion: '1.3.0', }); expect(resultWithUseDefaultAddress).toEqual(resultWithChainMainnet); @@ -61,6 +64,7 @@ describe('predictStealthSafeAddressWithClient', () => { chainId, threshold, stealthAddresses, + safeVersion: '1.3.0', }), ).rejects.toThrow('No safe contracts found for this configuration.'); }); @@ -76,6 +80,7 @@ describe('predictStealthSafeAddressWithClient', () => { chainId, threshold, stealthAddresses, + safeVersion: '1.3.0', }); expect(result).toHaveProperty('stealthSafeAddress'); expect(result.stealthSafeAddress).toMatch(/^0x[a-fA-F0-9]{40}$/); @@ -107,6 +112,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }); expect(result).toEqual({ stealthSafeAddress: expectedStealthSafeAddress }); @@ -126,6 +132,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }); const resultWithChainMainnet = predictStealthSafeAddressWithBytecode({ @@ -133,6 +140,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }); expect(resultWithUseDefaultAddress).toEqual(resultWithChainMainnet); @@ -154,6 +162,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }), ).toThrow('No safe contracts found for this configuration.'); }); @@ -172,6 +181,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }), ).toThrow('chainId is required when useDefaultAddress is false'); }); @@ -188,6 +198,7 @@ describe('predictStealthSafeAddressWithBytecode', () => { threshold, stealthAddresses, safeProxyBytecode, + safeVersion: '1.3.0', }); expect(result).toHaveProperty('stealthSafeAddress'); expect(result.stealthSafeAddress).toMatch(/^0x[a-fA-F0-9]{40}$/);