From aa71fcb7cdac8c2dcc557a53431738b7be847da8 Mon Sep 17 00:00:00 2001 From: Shahar Kaminsky Date: Tue, 5 Dec 2023 11:50:45 +0200 Subject: [PATCH] feat(contracts): Add the WithdrawalFinalizer Contract (#314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # What ❔ This PR adds the WithdrawalFinalizer contract. ## Why ❔ To open source the already deployed contract. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [ ] Tests for the changes have been added / updated. - [ ] Documentation comments have been added / updated. - [ ] Code has been formatted via `cargo fmt`. --------- Co-authored-by: Fedor Sakharov --- .gitignore | 6 ++ README.md | 23 ++++++++ contracts/WithdrawalFinalizer.sol | 92 +++++++++++++++++++++++++++++++ hardhat.config.ts | 38 +++++++++++++ package.json | 38 +++++++++++++ scripts/deploy.ts | 26 +++++++++ 6 files changed, 223 insertions(+) create mode 100644 contracts/WithdrawalFinalizer.sol create mode 100644 hardhat.config.ts create mode 100644 package.json create mode 100644 scripts/deploy.ts diff --git a/.gitignore b/.gitignore index ea8c4bf7..2425f87f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ /target +node_modules +cache +artifacts +typechain-types +*.log +dev.env diff --git a/README.md b/README.md index 54a2e7da..250467d2 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,29 @@ You may specify `All`, `None`, `BlackList` or `WhiteList` as json documents: 1. `TOKENS_TO_FINALIZE = '{ "WhiteList":[ "0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4" ] }'` - Finalize only these tokens 1. `TOKENS_TO_FINALIZE = '{ "BlackList":[ "0x3355df6D4c9C3035724Fd0e3914dE96A5a83aaf4" ] }'` - Finalize all tokens but these +## Deploying the finalizer smart contract + +The finalizer smart contract needs to reference the addresses of the diamond proxy contract and l1 erc20 proxy contract. +You also need to know the key of the account you want to use to deploy the finalizer contract. + +When you know those to deploy the contract you need to run (assume you are running `anvil` in a separate terminal): + +``` +$ yarn +$ env CONTRACTS_DIAMOND_PROXY_ADDR="0x9A6DE0f62Aa270A8bCB1e2610078650D539B1Ef9" CONTRACTS_L1_ERC20_BRIDGE_PROXY_ADDR="0x2Ae09702F77a4940621572fBcDAe2382D44a2cbA" MNEMONIC="test test test test test test test test test test test junk" ETH_CLIENT_WEB3_URL="http://localhost:8545" npx hardhat run ./scripts/deploy.ts +``` + +If all goes well the the result would be + +``` +... +Compiled 18 Solidity files successfully (evm target: paris). +CONTRACTS_WITHDRAWAL_FINALIZER_ADDRESS=0x712516e61C8B383dF4A63CFe83d7701Bce54B03e +``` + +And so you know the address of the deployed contract. + + ## License zkSync Withdrawal Finalizer is distributed under the terms of either diff --git a/contracts/WithdrawalFinalizer.sol b/contracts/WithdrawalFinalizer.sol new file mode 100644 index 00000000..66503b7f --- /dev/null +++ b/contracts/WithdrawalFinalizer.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity ^0.8.0; + +import "@matterlabs/zksync-contracts/l1/contracts/bridge/interfaces/IL1Bridge.sol"; +import "@matterlabs/zksync-contracts/l1/contracts/zksync/interfaces/IMailbox.sol"; +import "@matterlabs/zksync-contracts/l1/contracts/common/libraries/UncheckedMath.sol"; + + +contract WithdrawalFinalizer { + using UncheckedMath for uint256; + IMailbox constant ZKSYNC_MAILBOX = IMailbox($(ZKSYNC_ADDRESS)); + IL1Bridge constant ERC20_BRIDGE = IL1Bridge($(ERC20_BRIDGE_ADDRESS)); + + struct RequestFinalizeWithdrawal { + uint256 _l2BlockNumber; + uint256 _l2MessageIndex; + uint16 _l2TxNumberInBlock; + bytes _message; + bytes32[] _merkleProof; + bool _isEth; + uint256 _gas; + } + + struct Result { + uint256 _l2BlockNumber; + uint256 _l2MessageIndex; + uint256 _gas; + bool success; + } + + function finalizeWithdrawals( + RequestFinalizeWithdrawal[] calldata requests + ) external returns (Result[] memory) { + uint256 requestsLength = requests.length; + Result[] memory results = new Result[](requestsLength); + for (uint256 i = 0; i < requestsLength; i = i.uncheckedInc()) { + require(gasleft() >= ((requests[i]._gas * 64) / 63) + 500, "i"); + uint256 gasBefore = gasleft(); + if (requests[i]._isEth) { + try + ZKSYNC_MAILBOX.finalizeEthWithdrawal{gas: requests[i]._gas}( + requests[i]._l2BlockNumber, + requests[i]._l2MessageIndex, + requests[i]._l2TxNumberInBlock, + requests[i]._message, + requests[i]._merkleProof + ) + { + results[i] = Result({ + _l2BlockNumber: requests[i]._l2BlockNumber, + _l2MessageIndex: requests[i]._l2MessageIndex, + _gas: gasBefore - gasleft(), + success: true + }); + } catch { + results[i] = Result({ + _l2BlockNumber: requests[i]._l2BlockNumber, + _l2MessageIndex: requests[i]._l2MessageIndex, + _gas: 0, + success: false + }); + } + } else { + try + ERC20_BRIDGE.finalizeWithdrawal{gas: requests[i]._gas}( + requests[i]._l2BlockNumber, + requests[i]._l2MessageIndex, + requests[i]._l2TxNumberInBlock, + requests[i]._message, + requests[i]._merkleProof + ) + { + results[i] = Result({ + _l2BlockNumber: requests[i]._l2BlockNumber, + _l2MessageIndex: requests[i]._l2MessageIndex, + _gas: gasBefore - gasleft(), + success: true + }); + } catch { + results[i] = Result({ + _l2BlockNumber: requests[i]._l2BlockNumber, + _l2MessageIndex: requests[i]._l2MessageIndex, + _gas: 0, + success: false + }); + } + } + } + return results; + } +} diff --git a/hardhat.config.ts b/hardhat.config.ts new file mode 100644 index 00000000..80e05e00 --- /dev/null +++ b/hardhat.config.ts @@ -0,0 +1,38 @@ +import '@nomiclabs/hardhat-solpp'; +import '@nomiclabs/hardhat-ethers'; +import '@nomiclabs/hardhat-etherscan'; +import '@typechain/hardhat'; + +const config = { + ZKSYNC_ADDRESS: process.env.CONTRACTS_DIAMOND_PROXY_ADDR, + ERC20_BRIDGE_ADDRESS: process.env.CONTRACTS_L1_ERC20_BRIDGE_PROXY_ADDR +}; + +export default { + solidity: { + version: '0.8.18', + settings: { + optimizer: { + enabled: true, + runs: 200 + }, + outputSelection: { + '*': { + '*': ['storageLayout'] + } + } + } + }, + contractSizer: { + runOnCompile: false + }, + paths: { + sources: './contracts' + }, + solpp: { + defs: config + }, + etherscan: { + apiKey: process.env.MISC_ETHERSCAN_API_KEY + } +}; diff --git a/package.json b/package.json new file mode 100644 index 00000000..6c7028cf --- /dev/null +++ b/package.json @@ -0,0 +1,38 @@ +{ + "name": "withdrawal-finalizer", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@matterlabs/zksync-contracts": "^0.5.2", + "@openzeppelin/contracts": "^4.8.1", + "@sentry/node": "^7.2.0", + "@sentry/tracing": "^7.2.0", + "ethers": "^5.6.0", + "node-fetch": "^2.6.1", + "prom-client": "^14.2.0", + "zksync-web3": "^0.13.3" + }, + "devDependencies": { + "@nomiclabs/hardhat-ethers": "^2.0.5", + "@nomiclabs/hardhat-etherscan": "^3.0.3", + "@nomiclabs/hardhat-solpp": "^2.0.1", + "@typechain/ethers-v5": "^9.0.0", + "@typechain/hardhat": "^5.0.0", + "@types/chai": "^4.2.0", + "@types/mocha": "^9.1.0", + "@types/node": "^14.14.5", + "chai": "^4.3.6", + "hardhat": "^2.12.7", + "mocha": "^9.2.1", + "prettier": "^2.5.1", + "prettier-plugin-solidity": "^1.1.2", + "ts-generator": "^0.1.1", + "ts-node": "^10.8.1", + "typechain": "^7.0.0", + "typescript": "^4.0.5" + }, + "scripts": { + "deploy": "ts-node scripts/deploy.ts", + "contracts:build": "hardhat compile" + } +} diff --git a/scripts/deploy.ts b/scripts/deploy.ts new file mode 100644 index 00000000..906bfb53 --- /dev/null +++ b/scripts/deploy.ts @@ -0,0 +1,26 @@ +import { Wallet } from 'ethers'; + +const hardhat = require('hardhat'); + +async function main() { + const provider = new hardhat.ethers.providers.JsonRpcProvider(process.env.ETH_CLIENT_WEB3_URL as string); + const wallet = Wallet.fromMnemonic( + process.env.MNEMONIC as string, + "m/44'/60'/0'/0/1" + ).connect(provider); + + + const contractFactory = await hardhat.ethers.getContractFactory("WithdrawalFinalizer", { + signer: wallet + }); + const contract = await contractFactory.deploy(); + await contract.deployTransaction.wait(); + console.log(`CONTRACTS_WITHDRAWAL_FINALIZER_ADDRESS=${contract.address}`); + } + + main() + .then(() => process.exit(0)) + .catch((error) => { + console.error(error); + process.exit(1); + });