Skip to content

Commit

Permalink
Contract deployment (#38)
Browse files Browse the repository at this point in the history
* draft for contract deployment

* fix build

* relocate register transaction type

* fix contract deploy. estimate gas. populate. format with customData

* fix paymaster params

* Updates to `ContractFactory` Docs

* remove a TODO and fix 2 tests at contract deployment

* estimate gas for deploy transaction

---------

Co-authored-by: Muhammad-Altabba <[email protected]>
Co-authored-by: Dan Forbes <[email protected]>
  • Loading branch information
3 people authored Aug 19, 2024
1 parent 16d318f commit e805619
Show file tree
Hide file tree
Showing 27 changed files with 2,878 additions and 1,142 deletions.
8 changes: 1 addition & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,7 @@
"dependencies": {
"ethereum-cryptography": "^2.1.3",
"hardhat": "^2.19.4",
"web3": "^4.11.1",
"web3-core": "^4.5.0",
"web3-eth-abi": "^4.2.2",
"web3-eth-accounts": "^4.1.3",
"web3-eth-contract": "4.6.0",
"web3-types": "1.7.0",
"web3-utils": "4.3.1"
"web3": "^4.11.1"
},
"devDependencies": {
"@chainsafe/eslint-config": "^2.1.1",
Expand Down
4 changes: 4 additions & 0 deletions src/Eip712.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ export class EIP712Transaction extends BaseTransaction<EIP712Transaction> {
return `${msg} (${this.errorStr()})`;
}

public static fromTxData(txData: Eip712TxData, _ = {}) {
return new EIP712Transaction(txData);
}

errorStr(): string {
return '';
}
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type * as web3Types from 'web3-types';

export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
export const ZERO_HASH = '0x0000000000000000000000000000000000000000000000000000000000000000';

/**
* The address of the L1 `ETH` token.
Expand Down
314 changes: 314 additions & 0 deletions src/contract-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import type { ContractAbi } from 'web3';
import { Web3Context, eth } from 'web3';
import { TransactionFactory } from 'web3-eth-accounts';
import * as web3Utils from 'web3-utils';
import type * as web3Types from 'web3-types';
import { Contract } from 'web3-eth-contract';

import { ContractDeployerContract, getDeployedContracts, hashBytecode } from './utils';
import {
CONTRACT_DEPLOYER_ADDRESS,
DEFAULT_GAS_PER_PUBDATA_LIMIT,
EIP712_TX_TYPE,
ZERO_HASH,
} from './constants';
import type { DeploymentType } from './types';
import { AccountAbstractionVersion } from './types';
import type { ZKsyncWallet } from './zksync-wallet';
import * as constants from './constants';
import { EIP712Transaction } from './Eip712';

interface CustomData {
factoryDeps?: (string | { object: string })[];
salt?: string;
}

interface Overrides {
customData?: CustomData;
}

// @ts-ignore-next-line
TransactionFactory.registerTransactionType(constants.EIP712_TX_TYPE, EIP712Transaction);

/**
* A `ContractFactory` is used to deploy a `Contract` to the blockchain.
*/
export class ContractFactory<Abi extends ContractAbi> extends Web3Context {
/**
* The Contract Interface.
*/
readonly abi!: Abi;

/**
* The Contract deployment bytecode. Often called the initcode.
*/
readonly bytecode!: string;

readonly zkWallet: ZKsyncWallet;

/** The deployment type that is currently in use. */
readonly deploymentType: DeploymentType;

readonly contractToBeDeployed: Contract<Abi>;

/**
* Create a new `ContractFactory` for a contract with the provided ABI and bytecode.
*
* @param abi The ABI (Application Binary Interface) of the contract.
* @param bytecode The bytecode of the contract.
* @param [deploymentType] The deployment type, defaults to 'create'.
*/
constructor(
abi: Abi,
bytecode: web3Types.Bytes,
zkWallet: ZKsyncWallet,
deploymentType?: DeploymentType,
) {
super(zkWallet.provider);

this.abi = abi;

// Dereference Solidity bytecode objects and allow a missing `0x`-prefix
if (bytecode instanceof Uint8Array) {
this.bytecode = web3Utils.bytesToHex(bytecode);
} else {
if (typeof bytecode === 'object' && (bytecode as { object: web3Types.Bytes }).object) {
bytecode = (bytecode as { object: web3Types.Bytes }).object;
}
if (typeof bytecode === 'string' && !bytecode.startsWith('0x')) {
bytecode = '0x' + bytecode;
}
this.bytecode = web3Utils.bytesToHex(bytecode);
}

this.zkWallet = zkWallet;

this.deploymentType = deploymentType || 'create';

this.contractToBeDeployed = new zkWallet.provider!.eth.Contract(this.abi);
}

private encodeCalldata(
salt: string,
bytecodeHash: web3Types.Bytes,
constructorCalldata: web3Types.Bytes,
): string {
const contractDeploymentArgs = [
salt,
web3Utils.bytesToHex(bytecodeHash),
constructorCalldata,
];
const accountDeploymentArgs = [
...contractDeploymentArgs,
AccountAbstractionVersion.Version1,
];

switch (this.deploymentType) {
case 'create':
return ContractDeployerContract.methods
.create(...contractDeploymentArgs)
.encodeABI();
case 'createAccount':
return ContractDeployerContract.methods
.createAccount(...accountDeploymentArgs)
.encodeABI();
case 'create2':
return ContractDeployerContract.methods
.create2(...contractDeploymentArgs)
.encodeABI();
case 'create2Account':
return ContractDeployerContract.methods
.create2Account(...accountDeploymentArgs)
.encodeABI();
default:
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?: Overrides) {
if (this.deploymentType === 'create2' || this.deploymentType === 'create2Account') {
if (!overrides || !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 &&
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.");
}
}

async getDeployTransaction(
args: web3Types.ContractConstructorArgs<Abi> = [] as web3Types.ContractConstructorArgs<Abi>,
overrides?: Overrides,
): Promise<Omit<web3Types.Transaction, 'to'>> {
let constructorArgs: any[];

// The overrides will be popped out in this call:
const txRequest: web3Types.TransactionCall & { customData?: any } = this.contractToBeDeployed
.deploy({
data: this.bytecode,
arguments: args,
})
.populateTransaction({
from: this.zkWallet.getAddress() ?? this.defaultAccount ?? undefined,
});

this.checkOverrides(overrides);
let overridesCopy: Overrides = overrides ?? {
customData: { factoryDeps: [], salt: ZERO_HASH },
};
if (overrides) {
overridesCopy = overrides;
overridesCopy.customData ??= {};
overridesCopy.customData.salt ??= ZERO_HASH;
overridesCopy.customData.factoryDeps = (overridesCopy.customData.factoryDeps ?? []).map(
normalizeBytecode,
);
} else {
overridesCopy = {
customData: { factoryDeps: [], salt: ZERO_HASH },
};
}
constructorArgs = args as any[];

const bytecodeHash = hashBytecode(this.bytecode);
const constructorCalldata = web3Utils.hexToBytes(
eth.abi.encodeParameters(
(this.contractToBeDeployed.options.jsonInterface
.filter(item => item.type === 'constructor')
.map(item => item.inputs)[0] as web3Types.AbiParameter[]) ?? [], // TOOD: check this line
constructorArgs,
),
);
const deployCalldata = this.encodeCalldata(
overridesCopy?.customData?.salt as string,
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: CONTRACT_DEPLOYER_ADDRESS,
data: deployCalldata,
type: EIP712_TX_TYPE,
};

tx.customData ??= {};
tx.customData.factoryDeps ??= overridesCopy?.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);
}

// to fill the gas estimation:
this.zkWallet.populateTransaction(txRequest);

const txNoUndefined = Object.entries(tx)
.filter(([, value]) => value !== undefined)
.reduce((obj: { [key: string]: any }, [key, value]) => {
obj[key] = value;
return obj;
}, {});
return txNoUndefined;
}

/**
* Deploys a new contract or account instance on the L2 blockchain.
*
* @param args - Constructor arguments for the contract
* @param overrides - Used for providing salt to create2 and create2Account deployment types and for providing custom factoryDeps
*
* @example <caption>Deploy only with constructor arguments</caption>
*
* const deployedContract = await contractFactory.deploy(arg1, arg2, ...);
*
* @example <caption>Deploy with constructor arguments and custom factoryDeps</caption>
*
* const deployedContractWithSaltAndDeps = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* factoryDeps: ['0x...']
* }
* });
*
* @example <caption>Deploy with constructor arguments and salt for create2 or create2Account deployment types</caption>
*
* const deployedContractWithSalt = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* salt: '0x...'
* }
* });
*
* @example <caption>Deploy with constructor arguments, salt for create2 or create2Account deployment types, and custom factoryDeps</caption>
*
* const deployedContractWithSaltAndDeps = await contractFactory.deploy(arg1, arg2, ..., {
* customData: {
* salt: '0x...',
* factoryDeps: ['0x...']
* }
* });
*/
async deploy(
args?: web3Types.ContractConstructorArgs<Abi>,
overrides?: Overrides,
): Promise<
Contract<Abi> & {
deploymentTransaction(): web3Types.TransactionReceipt;
}
> {
const tx = await this.getDeployTransaction(args, overrides);

const receipt = await (await this.zkWallet?.sendTransaction(tx)).wait();

const deployedAddresses = getDeployedContracts(receipt).map(info => info.deployedAddress);

this.contractToBeDeployed.options.address = deployedAddresses[deployedAddresses.length - 1];
const contractWithCorrectAddress: Contract<Abi> & {
deploymentTransaction(): web3Types.TransactionReceipt;
} = this.contractToBeDeployed.clone() as Contract<Abi> & {
deploymentTransaction(): web3Types.TransactionReceipt;
};
contractWithCorrectAddress.deploymentTransaction = () => receipt;
return contractWithCorrectAddress;
}
}

function normalizeBytecode(bytecode: web3Types.Bytes | { object: string }): string {
// Dereference Solidity bytecode objects and allow a missing `0x`-prefix
if (bytecode instanceof Uint8Array) {
bytecode = web3Utils.bytesToHex(bytecode);
} else {
if (typeof bytecode === 'object') {
bytecode = bytecode.object;
}
if (!bytecode.startsWith('0x')) {
bytecode = '0x' + bytecode;
}
bytecode = web3Utils.bytesToHex(web3Utils.hexToBytes(bytecode));
}
return bytecode;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export { Web3ZKsyncL2 } from './web3zksync-l2';
export { ZKsyncWallet } from './zksync-wallet';
export { RpcMethods } from './rpc.methods';
export { TypedDataEncoder } from './TypedDataEncoder';
export { ContractFactory } from './contract-factory';
export * from './Eip712';
export * from './paymaster-utils';
export * from './smart-account';
Expand Down
Loading

0 comments on commit e805619

Please sign in to comment.