From b36ae6457bd342f736c4b2faf4c81898c0c60919 Mon Sep 17 00:00:00 2001 From: Axiom Bot <0xAxiom@users.noreply.github.com> Date: Wed, 20 May 2026 23:25:41 -0700 Subject: [PATCH] fix(cli/foundry): support multiple addresses sharing same ABI via DisplayName:ArtifactName Closes #4396. When multiple tokens share an ABI (e.g. DAI and WETH both use ERC20), the `deployments` map previously only accepted one entry per contract name, causing later entries to silently overwrite earlier ones. Add `'DisplayName:ArtifactName'` key syntax to the foundry plugin's `deployments` option. Any key ending in `:ArtifactName` is treated as an alias: the ABI is read from `ArtifactName.json` and the generated export uses `DisplayName` as its name. Multiple aliases for the same artifact each produce their own named contract config. ```ts // wagmi.config.ts deployments: { 'DAI:ERC20': { 1: '0x6B175...' }, 'WETH:ERC20': { 1: '0xC02aA...' }, } // generates: daiConfig + wethConfig, both with erc20Abi ``` Direct-key deployments (`{ ERC20: { ... } }`) continue to work unchanged. Tests added for multi-alias, single-alias, and direct-key cases. --- packages/cli/src/plugins/foundry.test.ts | 84 ++++++++++++++++++++++++ packages/cli/src/plugins/foundry.ts | 53 +++++++++++++-- 2 files changed, 133 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/plugins/foundry.test.ts b/packages/cli/src/plugins/foundry.test.ts index 90b844e051..7143b19a93 100644 --- a/packages/cli/src/plugins/foundry.test.ts +++ b/packages/cli/src/plugins/foundry.test.ts @@ -407,3 +407,87 @@ test('watch callbacks use broadcast deployments', async () => { }, }) }) + +test('deployments: multiple addresses sharing same ABI via DisplayName:ArtifactName', async () => { + const dir = await createTempDir() + const spy = vi.spyOn(process, 'cwd') + spy.mockImplementation(() => dir) + + const artifactsDir = resolve(dir, 'out') + await fs.mkdir(artifactsDir, { recursive: true }) + + const erc20Abi = [ + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ] + await fs.writeFile( + resolve(artifactsDir, 'ERC20.json'), + JSON.stringify({ abi: erc20Abi }, null, 2), + ) + + const contracts = await foundry({ + deployments: { + 'DAI:ERC20': { 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F' }, + 'WETH:ERC20': { 1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, + }, + forge: { build: false }, + }).contracts?.() + + expect(contracts).toHaveLength(2) + + const dai = contracts?.find((c) => c.name === 'DAI') + const weth = contracts?.find((c) => c.name === 'WETH') + + expect(dai).toBeDefined() + expect(dai?.abi).toEqual(erc20Abi) + expect(dai?.address).toEqual({ + 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }) + + expect(weth).toBeDefined() + expect(weth?.abi).toEqual(erc20Abi) + expect(weth?.address).toEqual({ + 1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + }) +}) + +test('deployments: single DisplayName:ArtifactName alias', async () => { + const dir = await createTempDir() + const spy = vi.spyOn(process, 'cwd') + spy.mockImplementation(() => dir) + + const artifactsDir = resolve(dir, 'out') + await fs.mkdir(artifactsDir, { recursive: true }) + + const erc20Abi = [ + { + inputs: [{ internalType: 'address', name: 'account', type: 'address' }], + name: 'balanceOf', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + ] + await fs.writeFile( + resolve(artifactsDir, 'ERC20.json'), + JSON.stringify({ abi: erc20Abi }, null, 2), + ) + + const contracts = await foundry({ + deployments: { + 'DAI:ERC20': { 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F' }, + }, + forge: { build: false }, + }).contracts?.() + + expect(contracts).toHaveLength(1) + expect(contracts?.[0]?.name).toBe('DAI') + expect(contracts?.[0]?.address).toEqual({ + 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + }) +}) diff --git a/packages/cli/src/plugins/foundry.ts b/packages/cli/src/plugins/foundry.ts index 0dc2de12e3..b9f7b63dbf 100644 --- a/packages/cli/src/plugins/foundry.ts +++ b/packages/cli/src/plugins/foundry.ts @@ -60,7 +60,24 @@ export type FoundryConfig = { * @default false */ includeBroadcasts?: boolean | undefined - /** Mapping of addresses to attach to artifacts. */ + /** + * Mapping of addresses to attach to artifacts. + * + * Keys are artifact names (e.g. `'ERC20'`). To generate multiple named contracts + * from the same ABI, use `'DisplayName:ArtifactName'` — each entry produces a + * separate export that shares the ABI but gets its own address and name. + * + * @example + * // Single deployment + * { ERC20: { 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F' } } + * + * @example + * // Multiple tokens sharing the same ERC20 ABI + * { + * 'DAI:ERC20': { 1: '0x6B175474E89094C44Da98b954EedeAC495271d0F' }, + * 'WETH:ERC20': { 1: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, + * } + */ deployments?: { [key: string]: ContractConfig['address'] } | undefined /** Artifact files to exclude. */ exclude?: string[] | undefined @@ -149,6 +166,22 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { } } + function getDeploymentsForArtifact( + artifactName: string, + contractDeployments: { [key: string]: ContractConfig['address'] }, + ): Array<{ displayName: string; address: ContractConfig['address'] }> { + const direct = contractDeployments[artifactName] + if (direct !== undefined) + return [{ displayName: artifactName, address: direct }] + + return Object.entries(contractDeployments) + .filter(([key]) => key.endsWith(`:${artifactName}`)) + .map(([key, address]) => ({ + displayName: key.slice(0, -(artifactName.length + 1)), + address, + })) + } + function getArtifactPaths(artifactsDirectory: string) { const crawler = new fdir().withBasePath().globWithOptions( include.map((x) => `${artifactsDirectory}/**/${x}`), @@ -274,9 +307,21 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { const artifactPaths = await getArtifactPaths(artifactsDirectory) const contracts = [] for (const artifactPath of artifactPaths) { - const contract = await getContract(artifactPath, allDeployments) - if (!contract.abi?.length) continue - contracts.push(contract) + const artifact = JSON.parse(await readFile(artifactPath, 'utf8')) + if (!artifact.abi?.length) continue + const artifactName = getContractName(artifactPath, false) + const matches = getDeploymentsForArtifact(artifactName, allDeployments) + if (matches.length >= 1 && matches[0]!.displayName !== artifactName) { + for (const match of matches) { + contracts.push({ + abi: artifact.abi, + address: match.address, + name: `${namePrefix}${match.displayName}`, + }) + } + } else { + contracts.push(await getContract(artifactPath, allDeployments)) + } } return contracts },