Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(builder): handling of errors when deploying contracts (create2) #839

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
6 changes: 2 additions & 4 deletions packages/builder/src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { estimateContractGas, estimateGas, prepareTransactionRequest, simulateCo
import { parseContractErrorReason, renderTrace, TraceEntry } from '../trace';
import { ChainArtifacts, ContractData } from '../types';

const NONCE_EXPIRED = 'NONCE_EXPIRED';
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
const debug = Debug('cannon:builder:error');

export function traceActions(artifacts: ChainArtifacts) {
Expand Down Expand Up @@ -134,9 +134,7 @@ export async function handleTxnError(
class CannonTraceError extends Error {
error: Error;

// this is needed here to prevent ethers from intercepting the error
// `NONCE_EXPIRED` is a very innocent looking error, so ethers will simply forward it.
code: string = NONCE_EXPIRED;
code: string = UNKNOWN_ERROR;

constructor(error: Error, ctx: ChainArtifacts, errorCodeHex: viem.Hex | null, traces: TraceEntry[]) {
let contractName = 'unknown';
Expand Down
139 changes: 86 additions & 53 deletions packages/builder/src/steps/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
PackageState,
} from '../types';
import { encodeDeployData, getContractDefinitionFromPath, getMergedAbiFromContractPaths } from '../util';
import { handleTxnError } from '../error';

const debug = Debug('cannon:builder:contract');

Expand Down Expand Up @@ -248,7 +249,7 @@ const deploySpec = {
}),
};

const overrides: any = {}; // TODO
const overrides: any = {};

if (config.overrides?.gasLimit) {
overrides.gasLimit = config.overrides.gasLimit;
Expand All @@ -269,70 +270,102 @@ const deploySpec = {
let receipt: viem.TransactionReceipt | null = null;
let deployAddress: viem.Address;

if (config.create2) {
const arachnidDeployerAddress = await ensureArachnidCreate2Exists(
runtime,
typeof config.create2 === 'string' ? (config.create2 as viem.Address) : ARACHNID_DEFAULT_DEPLOY_ADDR
);
try {
if (config.create2) {
const arachnidDeployerAddress = await ensureArachnidCreate2Exists(
runtime,
typeof config.create2 === 'string' ? (config.create2 as viem.Address) : ARACHNID_DEFAULT_DEPLOY_ADDR
);

debug('performing arachnid create2');
const [create2Txn, addr] = makeArachnidCreate2Txn(config.salt || '', txn.data!, arachnidDeployerAddress);
debug(`create2: deploy ${addr} by ${arachnidDeployerAddress}`);
debug('performing arachnid create2');
const [create2Txn, addr] = makeArachnidCreate2Txn(config.salt || '', txn.data!, arachnidDeployerAddress);
debug(`create2: deploy ${addr} by ${arachnidDeployerAddress}`);

const bytecode = await runtime.provider.getBytecode({ address: addr });
const bytecode = await runtime.provider.getBytecode({ address: addr });

if (bytecode && bytecode !== '0x') {
debug('create2 contract already completed');
// our work is done for us. unfortunately, its not easy to figure out what the transaction hash was
} else {
const signer = config.from
? await runtime.getSigner(config.from as viem.Address)
: await runtime.getDefaultSigner!(txn, config.salt);
if (bytecode && bytecode !== '0x') {
debug('create2 contract already completed');
// our work is done for us. unfortunately, its not easy to figure out what the transaction hash was
} else {
const signer = config.from
? await runtime.getSigner(config.from as viem.Address)
: await runtime.getDefaultSigner!(txn, config.salt);

const fullCreate2Txn = _.assign(create2Txn, overrides, { account: signer.wallet.account || signer.address });
debug('final create2 txn', fullCreate2Txn);
const fullCreate2Txn = _.assign(create2Txn, overrides, { account: signer.wallet.account || signer.address });
debug('final create2 txn', fullCreate2Txn);

const preparedTxn = await runtime.provider.prepareTransactionRequest(fullCreate2Txn);
const hash = await signer.wallet.sendTransaction(preparedTxn as any);
receipt = await runtime.provider.waitForTransactionReceipt({ hash });
debug('arachnid create2 complete', receipt);
}
deployAddress = addr;
} else {
const curAccountNonce = config.from
? await runtime.provider.getTransactionCount({ address: config.from as viem.Address })
: 0;
if (config.from && config.nonce?.length && parseInt(config.nonce) < curAccountNonce) {
const contractAddress = viem.getContractAddress({ from: config.from as viem.Address, nonce: BigInt(config.nonce) });

debug(`contract appears already deployed to address ${contractAddress} (nonce too high)`);

// check that the contract bytecode that was deployed matches the requested
const actualBytecode = await runtime.provider.getBytecode({ address: contractAddress });
// we only check the length because solidity puts non-substantial changes (ex. comments) in bytecode and that
// shouldn't trigger any significant change. And also this is just kind of a sanity check so just verifying the
// length should be sufficient
if (!actualBytecode || artifactData.deployedBytecode.length !== actualBytecode.length) {
debug('bytecode does not match up', artifactData.deployedBytecode, actualBytecode);
// this can happen normally. for now lets just disable it for now
/*throw new Error(
const preparedTxn = await runtime.provider.prepareTransactionRequest(fullCreate2Txn);
const hash = await signer.wallet.sendTransaction(preparedTxn as any);
receipt = await runtime.provider.waitForTransactionReceipt({ hash });
debug('arachnid create2 complete', receipt);
}
deployAddress = addr;
} else {
const curAccountNonce = config.from
? await runtime.provider.getTransactionCount({ address: config.from as viem.Address })
: 0;
if (config.from && config.nonce?.length && parseInt(config.nonce) < curAccountNonce) {
const contractAddress = viem.getContractAddress({
from: config.from as viem.Address,
nonce: BigInt(config.nonce),
});

debug(`contract appears already deployed to address ${contractAddress} (nonce too high)`);

// check that the contract bytecode that was deployed matches the requested
const actualBytecode = await runtime.provider.getBytecode({ address: contractAddress });
// we only check the length because solidity puts non-substantial changes (ex. comments) in bytecode and that
// shouldn't trigger any significant change. And also this is just kind of a sanity check so just verifying the
// length should be sufficient
if (!actualBytecode || artifactData.deployedBytecode.length !== actualBytecode.length) {
debug('bytecode does not match up', artifactData.deployedBytecode, actualBytecode);
// this can happen normally. for now lets just disable it for now
/*throw new Error(
`the address at ${config.from!} should have deployed a contract at nonce ${config.nonce!} at address ${contractAddress}, but the bytecode does not match up. actual bytecode length: ${
(actualBytecode || '').length
}`
);*/
}

deployAddress = contractAddress;
} else {
const signer = config.from
? await runtime.getSigner(config.from as viem.Address)
: await runtime.getDefaultSigner!(txn, config.salt);
const preparedTxn = await runtime.provider.prepareTransactionRequest(
_.assign(txn, overrides, { account: signer.wallet.account || signer.address })
);
const hash = await signer.wallet.sendTransaction(preparedTxn as any);
receipt = await runtime.provider.waitForTransactionReceipt({ hash });
deployAddress = receipt.contractAddress!;
}

deployAddress = contractAddress;
}
} catch (error: any) {
// catch an error when it comes from create2 deployer
if (config.create2) {
// arachnid create2 does not return the underlying revert message.
// ref: https://github.com/Arachnid/deterministic-deployment-proxy/blob/master/source/deterministic-deployment-proxy.yul#L13

// in order to get the underlying revert message, try perform a normal deployment
const simulateConfig = {
...config,
create2: false,
};

return await this.exec(runtime, ctx, simulateConfig, packageState);
} else {
const signer = config.from
? await runtime.getSigner(config.from as viem.Address)
: await runtime.getDefaultSigner!(txn, config.salt);
const preparedTxn = await runtime.provider.prepareTransactionRequest(
_.assign(txn, overrides, { account: signer.wallet.account || signer.address })
// catch an error when it comes from normal deployment
const contractArtifact = generateOutputs(
config,
ctx,
artifactData,
receipt,
// note: send zero address since there is no contract address
viem.zeroAddress,
packageState.currentLabel
);
const hash = await signer.wallet.sendTransaction(preparedTxn as any);
receipt = await runtime.provider.waitForTransactionReceipt({ hash });
deployAddress = receipt.contractAddress!;

return await handleTxnError(contractArtifact, runtime.provider, error);
}
}

Expand Down
Loading