Skip to content

Commit

Permalink
feat(contract): add verification and constructor params utils
Browse files Browse the repository at this point in the history
  • Loading branch information
jnsdls committed Feb 8, 2024
1 parent fa57c1f commit ece389c
Show file tree
Hide file tree
Showing 18 changed files with 1,167 additions and 1 deletion.
6 changes: 6 additions & 0 deletions packages/thirdweb/src/contract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ export {
export { formatCompilerMetadata } from "./actions/compiler-metadata.js";

export { getByteCode } from "./actions/get-bytecode.js";

// verification
export {
verifyContract,
checkVerificationStatus,
} from "./verification/index.js";
124 changes: 124 additions & 0 deletions packages/thirdweb/src/contract/verification/constructor-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import type { Abi } from "abitype";
import type { ThirdwebContract } from "../contract.js";
import { eth_getTransactionByHash, getRpcClient } from "../../rpc/index.js";
import { fetchDeployBytecodeFromPublishedContractMetadata } from "./publisher.js";
import { getCreate2FactoryAddress } from "../../utils/any-evm/create-2-factory.js";
import { decodeAbiParameters } from "viem";

export type FetchConstructorParamsOptions = {
contract: ThirdwebContract;
explorerApiUrl: string;
explorerApiKey: string;
abi: Abi;
};

// TODO: move to abi helpers (?)
function extractConstructorParamsFromAbi(abi: Abi) {
for (const input of abi) {
if (input.type === "constructor") {
return input.inputs || [];
}
}
return [];
}

const RequestStatus = {
OK: "1",
NOTOK: "0",
};

/**
*
* @param options
* @example
* @internal
*/
export async function fetchConstructorParams(
options: FetchConstructorParamsOptions,
): Promise<string> {
const constructorParamTypes = extractConstructorParamsFromAbi(options.abi);
if (constructorParamTypes.length === 0) {
return "";
}
const res = await fetch(
`${options.explorerApiUrl}?module=contract&action=getcontractcreation&contractaddresses=${options.contract.address}&apikey=${options.explorerApiKey}`,
);
const explorerData = await res.json();

if (
!explorerData ||
explorerData.status !== RequestStatus.OK ||
!explorerData.result[0]
) {
// Could not retrieve constructor parameters, using empty parameters as fallback
return "";
}
let constructorArgs = "";

const txHash = explorerData.result[0].txHash as `0x${string}`;
const rpcRequest = getRpcClient(options.contract);
const tx = await eth_getTransactionByHash(rpcRequest, {
hash: txHash,
});
const txDeployBytecode = tx.input;

// first: attempt to get it from Publish
try {
const bytecode = await fetchDeployBytecodeFromPublishedContractMetadata(
options.contract,
);
if (!bytecode) {
throw new Error("Contract not published through thirdweb");
}
const bytecodeHex = bytecode.startsWith("0x") ? bytecode : `0x${bytecode}`;
const create2FactoryAddress = await getCreate2FactoryAddress(
options.contract,
);
// if deterministic deploy through create2factory, remove salt length too
const create2SaltLength = tx.to === create2FactoryAddress ? 64 : 0;
constructorArgs = txDeployBytecode.substring(
bytecodeHex.length + create2SaltLength,
);
} catch {
// contracts not published through thirdweb
}
if (!constructorArgs) {
// couldn't find bytecode from Publish, using regex to locate consturctor args thruogh solc metadata
// https://docs.soliditylang.org/en/v0.8.17/metadata.html#encoding-of-the-metadata-hash-in-the-bytecode
// {6} = solc version
// {4} = 0033, but noticed some contracts have values other than 00 33. (uniswap)
const matches = [
...txDeployBytecode.matchAll(
/(64736f6c6343[\w]{6}[\w]{4})(?!.*\1)(.*)$/g,
),
];

// regex finds the LAST occurence of solc metadata bytes, result always in same position
// TODO: we currently don't handle error string embedded in the bytecode, need to strip ascii (upgradeableProxy) in patterns[2]
// https://etherscan.io/address/0xee6a57ec80ea46401049e92587e52f5ec1c24785#code
if (matches && matches[0] && matches[0][2]) {
constructorArgs = matches[0][2];
}
}

// third: attempt to guess it from the ABI inputs
if (!constructorArgs) {
// TODO: need to guess array / struct properly
const constructorParamByteLength = constructorParamTypes.length * 64;
constructorArgs = txDeployBytecode.substring(
txDeployBytecode.length - constructorParamByteLength,
);
}

try {
// sanity check that the constructor params are valid
// TODO: should we sanity check after each attempt?
decodeAbiParameters(constructorParamTypes, `0x${constructorArgs}`);
} catch (e) {
throw new Error(
"Verifying this contract requires it to be published. Run `npx thirdweb publish` to publish this contract, then try again.",
);
}

return constructorArgs;
}
218 changes: 218 additions & 0 deletions packages/thirdweb/src/contract/verification/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { download } from "../../storage/download.js";
import { extractIPFSUri } from "../../utils/bytecode/extractIPFS.js";
import { resolveImplementation } from "../../utils/bytecode/resolveImplementation.js";
import type { ThirdwebContract } from "../contract.js";
import { formatCompilerMetadata } from "../actions/compiler-metadata.js";
import { fetchConstructorParams } from "./constructor-params.js";
import { fetchSourceFilesFromMetadata } from "./source-files.js";
import type { Chain } from "../../chain/index.js";
import type { ThirdwebClient } from "../../client/client.js";

const RequestStatus = {
OK: "1",
NOTOK: "0",
};

export type VerifyContractOptions = {
contract: ThirdwebContract;
explorerApiUrl: string;
explorerApiKey: string;
encodedConstructorArgs?: string;
};

/**
* Verifies a contract by performing the following steps:
* 1. Resolves the implementation of the contract.
* 2. Extracts the IPFS URI from the contract bytecode.
* 3. Downloads the contract source code from the IPFS URI.
* 4. Fetches the source files from the compiler metadata.
* 5. Compiles the contract source code using the Solidity compiler.
* 6. Fetches the constructor parameters if not provided.
* 7. Sends a request to the contract verification API to verify the contract source code.
* @param options - The options for contract verification.
* @returns A promise that resolves to the verification result.
* @throws An error if any of the verification steps fail.
* @example
* ```ts
* import { getContract } from "thirdweb/contract";
* import { verifyContract } from "thirdweb/contract/verification";
*
* const contract = getContract({ ... });
* const verificationResult = await verifyContract({
* contract,
* explorerApiUrl: "https://api.polygonscan.com/api",
* explorerApiKey: "YOUR_API_KEY",
* });
* console.log(verificationResult);
* ```
*/
export async function verifyContract(
options: VerifyContractOptions,
): Promise<string | string[]> {
const implementation = await resolveImplementation(options.contract);
const ipfsUri = extractIPFSUri(implementation.bytecode);
if (!ipfsUri) {
throw new Error(
"Contract bytecode does not contain IPFS URI, cannot verify",
);
}
const res = await download({
uri: ipfsUri,
client: options.contract.client,
});
const compilerMetadata = formatCompilerMetadata(await res.json());

const sources = await fetchSourceFilesFromMetadata({
client: options.contract.client,
publishedMetadata: compilerMetadata,
});

const sourcesWithUrl = compilerMetadata.metadata.sources;
const sourcesWithContents: Record<string, { content: string }> = {};
for (const path of Object.keys(sourcesWithUrl)) {
const sourceCode = sources.find((source) => path === source.filename);
if (!sourceCode) {
throw new Error(`Could not find source file for ${path}`);
}
sourcesWithContents[path] = {
content: sourceCode.source,
};
}

const compilerInput = {
language: "Solidity",
sources: sourcesWithContents,
settings: {
optimizer: compilerMetadata.metadata.settings.optimizer,
evmVersion: compilerMetadata.metadata.settings.evmVersion,
remappings: compilerMetadata.metadata.settings.remappings,
outputSelection: {
"*": {
"*": [
"abi",
"evm.bytecode",
"evm.deployedBytecode",
"evm.methodIdentifiers",
"metadata",
],
"": ["ast"],
},
},
},
};

const compilationTarget =
compilerMetadata.metadata.settings.compilationTarget;
const targets = Object.keys(compilationTarget);
const contractPath = targets[0];

const encodedArgs = options.encodedConstructorArgs
? options.encodedConstructorArgs
: await fetchConstructorParams({
abi: compilerMetadata.metadata.output.abi,
contract: options.contract,
explorerApiUrl: options.explorerApiUrl,
explorerApiKey: options.explorerApiKey,
});

const requestBody: Record<string, string> = {
apikey: options.explorerApiKey,
module: "contract",
action: "verifysourcecode",
contractaddress: options.contract.address,
sourceCode: JSON.stringify(compilerInput),
codeformat: "solidity-standard-json-input",
contractname: `${contractPath}:${compilerMetadata.name}`,
compilerversion: `v${compilerMetadata.metadata.compiler.version}`,
constructorArguements: encodedArgs,
};

const parameters = new URLSearchParams({ ...requestBody });
const result = await fetch(options.explorerApiUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: parameters.toString(),
});

const data = await result.json();
if (data.status === RequestStatus.OK) {
return data.result;
} else {
throw new Error(`${data.result}`);
}
}

const VerificationStatus = {
FAILED: "Fail - Unable to verify",
SUCCESS: "Pass - Verified",
PENDING: "Pending in queue",
ALREADY_VERIFIED: "Contract source code already verified",
AUTOMATICALLY_VERIFIED: "Already Verified",
};

type CheckVerificationStatusOptions = {
explorerApiUrl: string;
explorerApiKey: string;
guid: string | string[];
};

/**
* Checks the verification status of a contract.
* @param options - The options for checking the verification status.
* @returns A promise that resolves with the verification status data.
* @throws An error if the verification status check fails.
* @example
* ```ts
* import { checkVerificationStatus } from "thirdweb/contract/verification";
* const verificationStatus = await checkVerificationStatus({
* explorerApiUrl: "https://api.polygonscan.com/api",
* explorerApiKey: "YOUR_API_KEY",
* guid: "YOUR_GUID",
* });
* console.log(verificationStatus);
* ```
*/
export async function checkVerificationStatus(
options: CheckVerificationStatusOptions,
): Promise<unknown> {
const endpoint = `${options.explorerApiUrl}?module=contract&action=checkverifystatus&guid=${options.guid}&apikey=${options.explorerApiKey}"`;
return new Promise((resolve, reject) => {
const intervalId = setInterval(async () => {
try {
const result = await fetch(endpoint, {
method: "GET",
});

const data = await result.json();

if (data?.result !== VerificationStatus.PENDING) {
clearInterval(intervalId);
resolve(data);
}
} catch (e) {
clearInterval(intervalId);
reject(e);
}
}, 3000);
});
}

export type VerifyThirdwebContractOptions = {
client: ThirdwebClient;
chain: Chain;
explorerApiUrl: string;
explorerApiKey: string;
contractName: string;
contractVersion?: string;
encodedConstructorArgs?: string;
};

// /**
// *
// * @internal
// */
// export async function verifyThirdwebContract(
// options: VerifyThirdwebContractOptions,
// ): Promise<string | string[]> {
// const contractAddress =
// }
Loading

0 comments on commit ece389c

Please sign in to comment.