L1-L2 transactions
Deploy a contract in ZKsync that has a counter.
Call L2 Contract from L1
Now, let's call the increment
method on Layer 2 from Layer 1.
- Create a new file in the
scripts
folder namedincrement-counter.ts
.
touch scripts/increment-counter.ts
- Copy and paste in the following code, replacing the following
details:
- GOVERNANCE-ADDRESS: the address of the contract deployed in L1.
- COUNTER-ADDRESS: the address of the contract deployed in L2.
L2-counter/scripts/increment-counter.tsimport { ethers } from 'hardhat'; import { utils } from 'zksync-ethers'; import * as GOVERNANCE_ABI_JSON from '../../L1-governance/artifacts/contracts/Governance.sol/Governance.json'; import * as COUNTER_ABI_JSON from '../artifacts-zk/contracts/Counter.sol/Counter.json'; const COUNTER_ADDRESS = process.env.COUNTER_ADDRESS ?? '<COUNTER_ADDRESS>'; const GOVERNANCE_ADDRESS = process.env.GOVERNANCE_ADDRESS ?? '<GOVERNANCE-ADDRESS>'; async function main() { const l1Provider = ethers.providerL1; // Set up the Governor wallet to be the same as the one that deployed the governance contract. const [wallet] = await ethers.getWallets(); // Set a constant that accesses the Layer 1 contract. const govcontract = await ethers.getContractAt(GOVERNANCE_ABI_JSON.abi, GOVERNANCE_ADDRESS, wallet); // Initialize the L2 provider. const l2Provider = ethers.providerL2; // Get the current address of the ZKsync L1 bridge. const zkSyncAddress = await l2Provider.getMainContractAddress(); // Get the `Contract` object of the ZKsync bridge. // const zkSyncContract = new Contract(zkSyncAddress, utils.ZKSYNC_MAIN_ABI, wallet); // eslint-disable-next-line @typescript-eslint/no-explicit-any const zksyncABI: any = utils.ZKSYNC_MAIN_ABI; // eslint-disable-next-line @typescript-eslint/no-explicit-any const zkSyncContract = await ethers.getContractAt(zksyncABI as any[], zkSyncAddress, wallet); // Encoding the L1 transaction is done in the same way as it is done on Ethereum. // Use an Interface which gives access to the contract functions. const counterInterface = new ethers.Interface(COUNTER_ABI_JSON.abi); const data = counterInterface.encodeFunctionData('increment', []); // The price of an L1 transaction depends on the gas price used. // You should explicitly fetch the gas price before making the call. const gasPrice = await l1Provider.getGasPrice(); // Define a constant for gas limit which estimates the limit for the L1 to L2 transaction. const gasLimit = await l2Provider.estimateL1ToL2Execute({ contractAddress: COUNTER_ADDRESS, calldata: data, caller: utils.applyL1ToL2Alias(GOVERNANCE_ADDRESS), }); // baseCost takes the price and limit and formats the total in wei. // For more information on `REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT` see the [fee model documentation](../developer-guides/transactions/fee-model.md). const baseCost = await zkSyncContract.l2TransactionBaseCost( gasPrice, gasLimit, utils.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT ); // !! If you don't include the gasPrice and baseCost in the transaction, a re-estimation of fee may generate errors. const tx = await govcontract.callZkSync( zkSyncAddress, COUNTER_ADDRESS, data, gasLimit, utils.REQUIRED_L1_TO_L2_GAS_PER_PUBDATA_LIMIT, { // Pass the necessary ETH `value` to cover the fee for the operation value: baseCost, gasPrice, } ); // Wait until the L1 tx is complete. await tx.wait(); // Get the TransactionResponse object for the L2 transaction corresponding to the execution call. const l2Response = await l2Provider.getL2TransactionFromPriorityOp(tx); // Output the receipt of the L2 transaction corresponding to the call to the counter contract. const l2Receipt = await l2Response.wait(); console.log(l2Receipt); } // We recommend always using this async/await pattern to properly handle errors. main().catch((error) => { console.error(error); process.exitCode = 1; });
Executing transactions from L1 requires the caller to pay a fee to the L2 operator. The fee depends on the length of the calldata and thegasLimit
. This is similar to thegasLimit
on Ethereum.You can read more about the ZKsync fee model here. The fee also depends on the gas price that is used during the transaction call. So to have a predictable fee for the call, the gas price should be fetched from the L1 provider. - Run the script with the following command:
npx hardhat run ./scripts/increment-counter.ts
In the output, you should see the full transaction receipt in L2. You can take thetransactionHash
and track it in the ZKsync explorer. It should look something like this:{ to: '0x9b379893bfAD08c12C2167C3e3dBf591BeD9410a', from: '0xE2EA97507a6cb610c81c4A9c157B8060E2ED7036', contractAddress: null, transactionIndex: 0, root: '0xb9ca78c288163a322a797ee671db8e9ab430eb00e38c4a989f2246ea22493945', gasUsed: BigNumber { _hex: '0x05c3df', _isBigNumber: true }, logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', blockHash: '0xb9ca78c288163a322a797ee671db8e9ab430eb00e38c4a989f2246ea22493945', transactionHash: '0x1fb19cc0aca8fcccaf5fbafd9174550f3151d0d2aa15d99eb820e0394313e409', logs: [ { transactionIndex: 0, blockNumber: 4119331, transactionHash: '0x1fb19cc0aca8fcccaf5fbafd9174550f3151d0d2aa15d99eb820e0394313e409', address: '0x000000000000000000000000000000000000800A', topics: [Array], ...
- Verify that the transaction was successful by running the
display-value
script again.npx hardhat run ./scripts/display-value.ts
You should see an incremented value in the output:The counter value is 1
Learn More
- To learn more about L1->L2 interaction on ZKsync, check out the documentation.
- To learn more about the
zksync-ethers
SDK, check out its documentation. - To learn more about the ZKsync hardhat plugins, check out their documentation.