fix(sdk): sourceCode entity too large for contract verification (#4166)

### Description

- used BFS to traverse and prune the dependency graph of any named
contract in the buildArtifact input to address 'entity size too large'
error thrown from bsc mainnet etherscan explorer

### Drive-by changes

- none

### Related issues

- fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/4164

### Backward compatibility

- yes

### Testing

- manual (bsc now verifying)
mo/verify-proxy-contracts
Noah Bayindirli 🥂 4 months ago committed by GitHub
parent fef6296737
commit 7fdd3958d6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/soft-tigers-explode.md
  2. 171
      typescript/sdk/src/deploy/verify/ContractVerifier.ts
  3. 8
      typescript/sdk/src/deploy/verify/types.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/sdk': patch
---
Adds logic to prune and minify build artifacts to address 'entity size too large' error thrown from explorers. Note that the only identified instance of this issue is on BSC mainnet.

@ -16,6 +16,7 @@ import {
ExplorerApiActions,
ExplorerApiErrors,
FormOptions,
SolidityStandardJsonInput,
} from './types.js';
export class ContractVerifier {
@ -23,7 +24,7 @@ export class ContractVerifier {
protected contractSourceMap: { [contractName: string]: string } = {};
protected readonly standardInputJson: string;
protected readonly standardInputJson: SolidityStandardJsonInput;
protected readonly compilerOptions: CompilerOptions;
constructor(
@ -32,8 +33,8 @@ export class ContractVerifier {
buildArtifact: BuildArtifact,
licenseType: CompilerOptions['licenseType'],
) {
// Extract the standard input json and compiler version from the build artifact
this.standardInputJson = JSON.stringify(buildArtifact.input);
this.standardInputJson = buildArtifact.input;
const compilerversion = `v${buildArtifact.solcLongVersion}`;
// double check compiler version matches expected format
@ -67,6 +68,51 @@ export class ContractVerifier {
);
}
public async verifyContract(
chain: ChainName,
input: ContractVerificationInput,
logger = this.logger,
): Promise<void> {
const verificationLogger = logger.child({
chain,
name: input.name,
address: input.address,
});
const metadata = this.multiProvider.tryGetChainMetadata(chain);
const rpcUrl = metadata?.rpcUrls[0].http ?? '';
if (rpcUrl.includes('localhost') || rpcUrl.includes('127.0.0.1')) {
verificationLogger.debug('Skipping verification for local endpoints');
return;
}
const explorerApi = this.multiProvider.tryGetExplorerApi(chain);
if (!explorerApi) {
verificationLogger.debug('No explorer API set, skipping');
return;
}
if (!explorerApi.family) {
verificationLogger.debug(`No explorer family set, skipping`);
return;
}
if (explorerApi.family === ExplorerFamily.Other) {
verificationLogger.debug(`Unsupported explorer family, skipping`);
return;
}
if (input.address === ethers.constants.AddressZero) return;
if (Array.isArray(input.constructorArguments)) {
verificationLogger.debug(
'Constructor arguments in legacy format, skipping',
);
return;
}
await this.verify(chain, input, verificationLogger);
}
private async submitForm(
chain: ChainName,
action: ExplorerApiActions,
@ -307,8 +353,15 @@ export class ContractVerifier {
throw new Error(`[${chain}] ${errorMessage}`);
}
const filteredStandardInputJson =
this.filterStandardInputJsonByContractName(
input.name,
this.standardInputJson,
verificationLogger,
);
return {
sourceCode: this.standardInputJson,
sourceCode: JSON.stringify(filteredStandardInputJson),
contractname: `${sourceName}:${input.name}`,
contractaddress: input.address,
/* TYPO IS ENFORCED BY API */
@ -317,48 +370,88 @@ export class ContractVerifier {
};
}
public async verifyContract(
chain: ChainName,
input: ContractVerificationInput,
logger = this.logger,
): Promise<void> {
const verificationLogger = logger.child({
chain,
name: input.name,
address: input.address,
});
/**
* Filters the solidity standard input for a specific contract name.
*
* This is a BFS impl to traverse the source input dependency graph.
* 1. Named contract file is set as root node.
* 2. The next level is formed by the direct imports of the contract file.
* 3. Each subsequent level's dependencies form the next level, etc.
* 4. The queue tracks the next files to process, and ensures the dependency graph explorered level by level.
*/
private filterStandardInputJsonByContractName(
contractName: string,
input: SolidityStandardJsonInput,
verificationLogger: Logger,
): SolidityStandardJsonInput {
verificationLogger.trace(
{ contractName },
'Filtering unused contracts from solidity standard input JSON....',
);
const filteredSources: SolidityStandardJsonInput['sources'] = {};
const sourceFiles: string[] = Object.keys(input.sources);
const contractFile: string = this.getContractFile(
contractName,
sourceFiles,
);
const queue: string[] = [contractFile];
const processed = new Set<string>();
const metadata = this.multiProvider.tryGetChainMetadata(chain);
const rpcUrl = metadata?.rpcUrls[0].http ?? '';
if (rpcUrl.includes('localhost') || rpcUrl.includes('127.0.0.1')) {
verificationLogger.debug('Skipping verification for local endpoints');
return;
}
while (queue.length > 0) {
const file = queue.shift()!;
if (processed.has(file)) continue;
processed.add(file);
const explorerApi = this.multiProvider.tryGetExplorerApi(chain);
if (!explorerApi) {
verificationLogger.debug('No explorer API set, skipping');
return;
}
filteredSources[file] = input.sources[file];
if (!explorerApi.family) {
verificationLogger.debug(`No explorer family set, skipping`);
return;
}
const content = input.sources[file].content;
const importStatements = this.getAllImportStatements(content);
if (explorerApi.family === ExplorerFamily.Other) {
verificationLogger.debug(`Unsupported explorer family, skipping`);
return;
importStatements.forEach((importStatement) => {
const importPath = importStatement.match(/["']([^"']+)["']/)?.[1];
if (importPath) {
const resolvedPath = this.resolveImportPath(file, importPath);
if (sourceFiles.includes(resolvedPath)) queue.push(resolvedPath);
}
});
}
if (input.address === ethers.constants.AddressZero) return;
if (Array.isArray(input.constructorArguments)) {
verificationLogger.debug(
'Constructor arguments in legacy format, skipping',
);
return;
return {
...input,
sources: filteredSources,
};
}
private getContractFile(contractName: string, sourceFiles: string[]): string {
const contractFile = sourceFiles.find((file) =>
file.endsWith(`/${contractName}.sol`),
);
if (!contractFile) {
throw new Error(`Contract ${contractName} not found in sources.`);
}
return contractFile;
}
await this.verify(chain, input, verificationLogger);
private getAllImportStatements(content: string) {
const importRegex =
/import\s+(?:(?:(?:"[^"]+"|'[^']+')\s*;)|(?:{[^}]+}\s+from\s+(?:"[^"]+"|'[^']+')\s*;)|(?:\s*(?:"[^"]+"|'[^']+')\s*;))/g;
return content.match(importRegex) || [];
}
private resolveImportPath(currentFile: string, importPath: string): string {
/* Use as-is for external dependencies and absolute imports */
if (importPath.startsWith('@') || importPath.startsWith('http')) {
return importPath;
}
const currentDir = currentFile.split('/').slice(0, -1).join('/');
const resolvedPath = importPath.split('/').reduce((acc, part) => {
if (part === '..') {
acc.pop();
} else if (part !== '.') {
acc.push(part);
}
return acc;
}, currentDir.split('/'));
return resolvedPath.join('/');
}
}

@ -14,6 +14,14 @@ export type SolidityStandardJsonInput = {
content: string;
};
};
language: string;
settings: {
optimizer: {
enabled: boolean;
runs: number;
};
outputSelection: any;
};
};
export type BuildArtifact = {

Loading…
Cancel
Save