From 301fd9fba3ea0916abc765b433e4b003c40954ff Mon Sep 17 00:00:00 2001 From: Mondal <137494766+krishanu717@users.noreply.github.com> Date: Sat, 8 Nov 2025 06:24:20 +0000 Subject: [PATCH] fix: handle custom RPC log formats in SafeFactory deployment (#650) This fixes an issue where SafeFactory.deploySafe() would incorrectly throw 'SafeProxy was not deployed correctly' on custom chains due to variations in how different RPC providers format event logs. - Added robust event log parsing with multiple fallback mechanisms - Added comprehensive test suite for different log formats - Handles both v1.0.0 and v1.3.0+ Safe versions - Supports logs with missing/null topics - Handles case sensitivity in addresses Fixes #650 --- packages/protocol-kit/src/contracts/utils.ts | 65 +++++++--- .../getSafeAddressFromDeploymentTx.test.ts | 118 ++++++++++++++++++ 2 files changed, 169 insertions(+), 14 deletions(-) create mode 100644 packages/protocol-kit/tests/e2e/getSafeAddressFromDeploymentTx.test.ts diff --git a/packages/protocol-kit/src/contracts/utils.ts b/packages/protocol-kit/src/contracts/utils.ts index 16acec82a..65f1ad32e 100644 --- a/packages/protocol-kit/src/contracts/utils.ts +++ b/packages/protocol-kit/src/contracts/utils.ts @@ -444,27 +444,64 @@ export function getSafeAddressFromDeploymentTx( txReceipt: FormattedTransactionReceipt, safeVersion: SafeVersion ): string { - const eventHash = toEventHash(getProxyCreationEvent(safeVersion)) - const proxyCreationEvent = txReceipt?.logs.find((event) => event.topics[0] === eventHash) - - if (!proxyCreationEvent) { + if (!txReceipt?.logs?.length) { throw new Error('SafeProxy was not deployed correctly') } - const { data, topics } = proxyCreationEvent + // Try to find the proxy creation event by topic first (fast path) + try { + const eventHash = toEventHash(getProxyCreationEvent(safeVersion)) + const proxyCreationEvent = txReceipt?.logs.find((event) => event.topics?.[0] === eventHash) - const { args } = decodeEventLog({ - abi: parseAbi([getProxyCreationEvent(safeVersion)]), - eventName: 'ProxyCreation', - data, - topics - }) + if (proxyCreationEvent) { + const { data, topics } = proxyCreationEvent - if (!args || !args.length) { - throw new Error('SafeProxy was not deployed correctly') + const { args } = decodeEventLog({ + abi: parseAbi([getProxyCreationEvent(safeVersion)]), + eventName: 'ProxyCreation', + data, + topics + }) + + if (args && args.length) return args[0] as string + } + } catch (e) { + console.log('Fast path failed:', e) + } + + // Fallback: some RPCs or chains may return logs in a slightly different format + // Try each log with manual address extraction first, then ABI decoding + for (const log of txReceipt.logs) { + // Skip obviously invalid logs + if (!log.data || log.data === '0x') continue + + // Try to find the address in the data field, regardless of version + if (log.data !== '0x') { + // Slice out bytes 12-32 (20 bytes) which should contain the address + // This matches both v1.3.0+ with padding and v1.0.0 without padding + const proxyCreationAddress = '0x' + log.data.slice(26, 66).toLowerCase() + if (isAddress(proxyCreationAddress)) { + // Return extracted address if it matches the expected address format + return proxyCreationAddress + } + } + + // Try ABI decoding as last resort + try { + const { args } = decodeEventLog({ + abi: parseAbi([getProxyCreationEvent(safeVersion)]), + eventName: 'ProxyCreation', + data: log.data, + topics: log.topics || [] + }) + + if (args && args.length) return args[0] as string + } catch (e) { + // ignore and continue trying other logs + } } - return args[0] as string + throw new Error('SafeProxy was not deployed correctly') } /** diff --git a/packages/protocol-kit/tests/e2e/getSafeAddressFromDeploymentTx.test.ts b/packages/protocol-kit/tests/e2e/getSafeAddressFromDeploymentTx.test.ts new file mode 100644 index 000000000..b25e0a148 --- /dev/null +++ b/packages/protocol-kit/tests/e2e/getSafeAddressFromDeploymentTx.test.ts @@ -0,0 +1,118 @@ +import { describe } from 'mocha'; +import chai from 'chai'; +import { getSafeAddressFromDeploymentTx } from '@safe-global/protocol-kit/contracts/utils'; +import type { FormattedTransactionReceipt, Log } from 'viem'; + +const { expect } = chai; + +describe('getSafeAddressFromDeploymentTx', () => { + const mockSafeAddress = '0x123456789A123456789A123456789A123456789A'; + + const createMockLog = ( + topics: `0x${string}`[] | null = [], + data: `0x${string}` = '0x' + ): Log => ({ + address: '0x1234567890123456789012345678901234567890', + topics: topics as [`0x${string}`], + data, + blockHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + blockNumber: 1n, + transactionHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + transactionIndex: 0, + logIndex: 0, + removed: false + }); + + const createMockReceipt = (logs: Log[]): FormattedTransactionReceipt => ({ + blockHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + blockNumber: 1n, + contractAddress: null, + cumulativeGasUsed: 100000n, + effectiveGasPrice: 1000000000n, + from: '0x0000000000000000000000000000000000000001', + gasUsed: 50000n, + logs, + logsBloom: '0x00', + status: 'success', + to: '0x0000000000000000000000000000000000000002', + transactionHash: '0x1234567890123456789012345678901234567890123456789012345678901234', + transactionIndex: 0, + type: 'eip1559' + }); + + describe('with Safe v1.3.0', () => { + const eventTopic = '0xa38789425dbeee0239e16ff2d2567e31720127fbc6430758c1a4efc6aef29f80' as const; + const proxyData = `0x000000000000000000000000${mockSafeAddress.slice(2)}0000000000000000000000000000000000000000000000000000000000000000` as `0x${string}`; + + it('should extract address from standard ProxyCreation event format', () => { + const mockLog = createMockLog([eventTopic], proxyData); + const receipt = createMockReceipt([mockLog]); + + const extractedAddress = getSafeAddressFromDeploymentTx(receipt, '1.3.0'); + expect(extractedAddress.toLowerCase()).to.equal(mockSafeAddress.toLowerCase()); + }); + + it('should handle logs without proper topics but valid data', () => { + const mockLog = createMockLog([], proxyData); + const receipt = createMockReceipt([mockLog]); + + const extractedAddress = getSafeAddressFromDeploymentTx(receipt, '1.3.0'); + expect(extractedAddress.toLowerCase()).to.equal(mockSafeAddress.toLowerCase()); + }); + + it('should handle custom RPC logs with null topics', () => { + const mockLog = createMockLog(null, proxyData); + const receipt = createMockReceipt([mockLog]); + + const extractedAddress = getSafeAddressFromDeploymentTx(receipt, '1.3.0'); + expect(extractedAddress.toLowerCase()).to.equal(mockSafeAddress.toLowerCase()); + }); + }); + + describe('with Safe v1.0.0', () => { + const eventTopic = '0x4f51faf6c4561ff95f067657e43439f0f856d97c04d9ec9070a6199ad418e235' as const; + const proxyData = `0x000000000000000000000000${mockSafeAddress.slice(2)}` as `0x${string}`; + + it('should extract address from legacy ProxyCreation event format', () => { + const mockLog = createMockLog([eventTopic], proxyData); + const receipt = createMockReceipt([mockLog]); + + const extractedAddress = getSafeAddressFromDeploymentTx(receipt, '1.0.0'); + expect(extractedAddress.toLowerCase()).to.equal(mockSafeAddress.toLowerCase()); + }); + + it('should handle legacy logs without proper topics', () => { + const mockLog = createMockLog([], proxyData); + const receipt = createMockReceipt([mockLog]); + + const extractedAddress = getSafeAddressFromDeploymentTx(receipt, '1.0.0'); + expect(extractedAddress.toLowerCase()).to.equal(mockSafeAddress.toLowerCase()); + }); + }); + + describe('error cases', () => { + it('should throw if no valid ProxyCreation event is found', () => { + const mockLog = createMockLog([], '0x'); + const receipt = createMockReceipt([mockLog]); + + expect(() => getSafeAddressFromDeploymentTx(receipt, '1.3.0')) + .to.throw('SafeProxy was not deployed correctly'); + }); + + it('should throw if receipt has no logs', () => { + const receipt = createMockReceipt([]); + expect(() => getSafeAddressFromDeploymentTx(receipt, '1.3.0')) + .to.throw('SafeProxy was not deployed correctly'); + }); + + it('should throw if logs array is null', () => { + const receipt = { + ...createMockReceipt([]), + logs: null + } as unknown as FormattedTransactionReceipt; + + expect(() => getSafeAddressFromDeploymentTx(receipt, '1.3.0')) + .to.throw('SafeProxy was not deployed correctly'); + }); + }); +});