zksync-ethers
Version:
A Web3 library for interacting with the ZkSync Layer 2 scaling solution.
276 lines (252 loc) • 9.14 kB
text/typescript
import {
BaseContract,
BytesLike,
ContractDeployTransaction,
ContractMethodArgs,
ContractRunner,
ContractTransactionResponse,
ethers,
Interface,
InterfaceAbi,
} from 'ethers';
import {
CONTRACT_DEPLOYER,
CONTRACT_2_FACTORY,
CONTRACT_DEPLOYER_ADDRESS,
CONTRACT_2_FACTORY_ADDRESS,
DEFAULT_GAS_PER_PUBDATA_LIMIT,
EIP712_TX_TYPE,
getDeployedContracts,
hashBytecode,
} from './utils';
import {
AccountAbstractionVersion,
DeploymentType,
TransactionReceipt,
} from './types';
/* c8 ignore next */
export {Contract} from 'ethers';
/**
* A `ContractFactory` is used to deploy a `Contract` to the blockchain.
*/
export class ContractFactory<
A extends Array<any> = Array<any>,
I = BaseContract,
> extends ethers.ContractFactory<A, I> {
/** The deployment type that is currently in use. */
readonly deploymentType: DeploymentType;
/**
* Create a new `ContractFactory` with `abi` and `bytecode`, optionally connected to `runner`.
* The `bytecode` may be the bytecode property within the standard Solidity JSON output.
*
* @param abi The ABI (Application Binary Interface) of the contract.
* @param bytecode The bytecode of the contract.
* @param [runner] The runner capable of interacting with a `Contract`on the network.
* @param [deploymentType] The deployment type, defaults to 'create'.
*/
constructor(
abi: Interface | InterfaceAbi,
bytecode: ethers.BytesLike,
runner?: ContractRunner,
deploymentType?: DeploymentType
) {
super(abi, bytecode, runner);
this.deploymentType = deploymentType || 'create';
}
protected encodeCalldata(
salt: BytesLike,
bytecodeHash: BytesLike,
constructorCalldata: BytesLike
): string {
const contractDeploymentArgs = [salt, bytecodeHash, constructorCalldata];
const accountDeploymentArgs = [
...contractDeploymentArgs,
AccountAbstractionVersion.Version1,
];
if (this.deploymentType === 'create') {
return CONTRACT_DEPLOYER.encodeFunctionData('create', [
...contractDeploymentArgs,
]);
} else if (this.deploymentType === 'createAccount') {
return CONTRACT_DEPLOYER.encodeFunctionData('createAccount', [
...accountDeploymentArgs,
]);
} else if (this.deploymentType === 'create2') {
return CONTRACT_2_FACTORY.encodeFunctionData('create2', [
...contractDeploymentArgs,
]);
} else if (this.deploymentType === 'create2Account') {
return CONTRACT_2_FACTORY.encodeFunctionData('create2Account', [
...accountDeploymentArgs,
]);
} else {
throw new Error(`Unsupported deployment type: ${this.deploymentType}!`);
}
}
/**
* Checks if the provided overrides are appropriately configured for a specific deployment type.
* @param overrides The overrides to be checked.
*
* @throws {Error} If:
* - `overrides.customData.salt` is not provided for `Create2` deployment type.
* - Provided `overrides.customData.salt` is not 32 bytes in hex format.
* - `overrides.customData.factoryDeps` is not array of bytecodes.
*/
protected checkOverrides(overrides: ethers.Overrides) {
if (
this.deploymentType === 'create2' ||
this.deploymentType === 'create2Account'
) {
if (!overrides.customData || !overrides.customData.salt) {
throw new Error('Salt is required for CREATE2 deployment!');
}
if (
!overrides.customData.salt.startsWith('0x') ||
overrides.customData.salt.length !== 66
) {
throw new Error('Invalid salt provided!');
}
}
if (
overrides.customData &&
overrides.customData.factoryDeps !== null &&
overrides.customData.factoryDeps !== undefined &&
!Array.isArray(overrides.customData.factoryDeps)
) {
throw new Error(
"Invalid 'factoryDeps' format! It should be an array of bytecodes."
);
}
}
override async getDeployTransaction(
...args: ContractMethodArgs<A>
): Promise<ContractDeployTransaction> {
let constructorArgs: any[];
let overrides: ethers.Overrides = {
customData: {factoryDeps: [], salt: ethers.ZeroHash},
};
// The overrides will be popped out in this call:
const txRequest = await super.getDeployTransaction(...args);
if (this.interface.deploy.inputs.length + 1 === args.length) {
constructorArgs = args.slice(0, args.length - 1);
overrides = args[args.length - 1] as ethers.Overrides;
overrides.customData ??= {};
overrides.customData.salt ??= ethers.ZeroHash;
this.checkOverrides(overrides);
overrides.customData.factoryDeps = (
overrides.customData.factoryDeps ?? []
).map(normalizeBytecode);
} else {
constructorArgs = args;
}
const bytecodeHash = hashBytecode(this.bytecode);
const constructorCalldata = ethers.getBytes(
this.interface.encodeDeploy(constructorArgs)
);
const deployCalldata = this.encodeCalldata(
overrides.customData.salt,
bytecodeHash,
constructorCalldata
);
// salt is no longer used and should not be present in customData of EIP712 transaction
if (txRequest.customData && txRequest.customData.salt)
delete txRequest.customData.salt;
const tx = {
...txRequest,
to:
this.deploymentType === 'create2' ||
this.deploymentType === 'create2Account'
? CONTRACT_2_FACTORY_ADDRESS
: CONTRACT_DEPLOYER_ADDRESS,
data: deployCalldata,
type: EIP712_TX_TYPE,
};
tx.customData ??= {};
tx.customData.factoryDeps ??= overrides.customData.factoryDeps;
tx.customData.gasPerPubdata ??= DEFAULT_GAS_PER_PUBDATA_LIMIT;
// The number of factory deps is relatively low, so it is efficient enough.
if (!tx.customData || !tx.customData.factoryDeps.includes(this.bytecode)) {
tx.customData.factoryDeps.push(this.bytecode);
}
return tx;
}
/**
* Deploys a new contract or account instance on the L2 blockchain.
* There is no need to wait for deployment with `waitForDeployment` method
* because **deploy** already waits for deployment to finish.
*
* @param args - Constructor arguments for the contract followed by optional
* {@link ethers.Overrides|overrides}. When deploying with Create2 method slat must be present in overrides.
*
* @example Deploy with constructor arguments only using `create` method
*
* const deployedContract = await contractFactory.deploy(arg1, arg2, ...);
*
* @example Deploy with constructor arguments, and factory dependencies using `create method
*
* const deployedContractWithSaltAndDeps = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* factoryDeps: ['0x...']
* }
* });
*
* @example Deploy with constructor arguments and custom salt using `create2` method
*
* const deployedContractWithSalt = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* salt: '0x...'
* }
* });
*
* @example Deploy with constructor arguments, custom salt, and factory dependencies using `create2` method
*
* const deployedContractWithSaltAndDeps = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* salt: '0x...',
* factoryDeps: ['0x...']
* }
* });
*/
override async deploy(...args: ContractMethodArgs<A>): Promise<
BaseContract & {
deploymentTransaction(): ContractTransactionResponse;
} & Omit<I, keyof BaseContract>
> {
const contract = await (await super.deploy(...args)).waitForDeployment();
const deployTxReceipt = (await this.runner?.provider?.getTransactionReceipt(
contract.deploymentTransaction()!.hash
)) as TransactionReceipt;
const deployedAddresses = getDeployedContracts(deployTxReceipt!).map(
info => info.deployedAddress
);
const contractWithCorrectAddress = new ethers.Contract(
deployedAddresses[deployedAddresses.length - 1],
contract.interface.fragments,
contract.runner
) as BaseContract & {
deploymentTransaction(): ContractTransactionResponse;
} & Omit<I, keyof BaseContract>;
const deploymentTx = contract.deploymentTransaction()!;
(deploymentTx as any).blockNumber = deployTxReceipt.blockNumber;
(deploymentTx as any).blockHash = deployTxReceipt.blockHash;
(deploymentTx as any).index = deployTxReceipt.index;
(deploymentTx as any).gasPrice = deployTxReceipt.gasPrice;
contractWithCorrectAddress.deploymentTransaction = () => deploymentTx;
return contractWithCorrectAddress;
}
}
function normalizeBytecode(bytecode: BytesLike | {object: string}) {
// Dereference Solidity bytecode objects and allow a missing `0x`-prefix
if (bytecode instanceof Uint8Array) {
bytecode = ethers.hexlify(ethers.getBytes(bytecode));
} else {
if (typeof bytecode === 'object') {
bytecode = bytecode.object;
}
if (!bytecode.startsWith('0x')) {
bytecode = '0x' + bytecode;
}
bytecode = ethers.hexlify(ethers.getBytes(bytecode));
}
return bytecode;
}