Commit 532214d0 authored by Mark Tyneway's avatar Mark Tyneway Committed by GitHub

monorepo: remove `devnet-tasks` (#11257)

Now `make devnet-test` runs go based devnet tests
rather than the legacy hardhat based ones.

```make
devnet-test: pre-devnet ## Runs tests on the local devnet
	make -C op-e2e test-devnet
```

This removes `devnet-tasks` from the monorepo, allowing
us to delete many dead dependencies.
parent db6d9adf
......@@ -81,7 +81,6 @@ The Optimism Immunefi program offers up to $2,000,042 for in-scope critical vuln
├── <a href="./ops-bedrock">ops-bedrock</a>: Bedrock devnet work
├── <a href="./packages">packages</a>
│ ├── <a href="./packages/contracts-bedrock">contracts-bedrock</a>: OP Stack smart contracts
│ ├── <a href="./packages/devnet-tasks">devnet-tasks</a>: Legacy Hardhat tasks used within devnet CI tests
├── <a href="./proxyd">proxyd</a>: Configurable RPC request router and proxy
├── <a href="./specs">specs</a>: Specs of the rollup starting at the Bedrock upgrade
</pre>
......
......@@ -18,7 +18,6 @@ pjoin = os.path.join
parser = argparse.ArgumentParser(description='Bedrock devnet launcher')
parser.add_argument('--monorepo-dir', help='Directory of the monorepo', default=os.getcwd())
parser.add_argument('--allocs', help='Only create the allocs and exit', type=bool, action=argparse.BooleanOptionalAction)
parser.add_argument('--test', help='Tests the deployment, must already be deployed', type=bool, action=argparse.BooleanOptionalAction)
log = logging.getLogger()
......@@ -70,7 +69,6 @@ def main():
devnet_config_path = pjoin(deploy_config_dir, 'devnetL1.json')
devnet_config_template_path = pjoin(deploy_config_dir, 'devnetL1-template.json')
ops_chain_ops = pjoin(monorepo_dir, 'op-chain-ops')
tasks_dir = pjoin(monorepo_dir, 'packages', 'devnet-tasks')
paths = Bunch(
mono_repo_dir=monorepo_dir,
......@@ -85,7 +83,6 @@ def main():
op_node_dir=op_node_dir,
ops_bedrock_dir=ops_bedrock_dir,
ops_chain_ops=ops_chain_ops,
tasks_dir=tasks_dir,
genesis_l1_path=pjoin(devnet_dir, 'genesis-l1.json'),
genesis_l2_path=pjoin(devnet_dir, 'genesis-l2.json'),
allocs_l1_path=pjoin(devnet_dir, 'allocs-l1.json'),
......@@ -94,11 +91,6 @@ def main():
rollup_config_path=pjoin(devnet_dir, 'rollup.json')
)
if args.test:
log.info('Testing deployed devnet')
devnet_test(paths)
return
os.makedirs(devnet_dir, exist_ok=True)
if args.allocs:
......@@ -330,21 +322,6 @@ def wait_for_rpc_server(url):
CommandPreset = namedtuple('Command', ['name', 'args', 'cwd', 'timeout'])
def devnet_test(paths):
# Run the two commands with different signers, so the ethereum nonce management does not conflict
# And do not use devnet system addresses, to avoid breaking fee-estimation or nonce values.
run_commands([
CommandPreset('erc20-test',
['npx', 'hardhat', 'deposit-erc20', '--network', 'devnetL1',
'--l1-contracts-json-path', paths.addresses_json_path, '--signer-index', '14'],
cwd=paths.tasks_dir, timeout=8*60),
CommandPreset('eth-test',
['npx', 'hardhat', 'deposit-eth', '--network', 'devnetL1',
'--l1-contracts-json-path', paths.addresses_json_path, '--signer-index', '15'],
cwd=paths.tasks_dir, timeout=8*60)
], max_workers=1)
def run_commands(commands: list[CommandPreset], max_workers=2):
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
futures = [executor.submit(run_command_preset, cmd) for cmd in commands]
......
......@@ -58,7 +58,6 @@
"nx": "18.2.2",
"nx-cloud": "latest",
"prettier": "^2.8.0",
"rimraf": "^5.0.5",
"typescript": "^5.5.4"
},
"dependencies": {
......
ignores: [
"@babel/eslint-parser",
"@typescript-eslint/parser",
"eslint-plugin-import",
"eslint-plugin-unicorn",
"eslint-plugin-jsdoc",
"eslint-plugin-prefer-arrow",
"eslint-plugin-react",
"@typescript-eslint/eslint-plugin",
"eslint-config-prettier",
"eslint-plugin-prettier",
"chai",
"ts-node",
"typedoc",
"typescript",
"ethereum-waffle",
"nyc"
]
module.exports = {
extends: '../../.eslintrc.js',
overrides: [],
}
PRIVATE_KEY_DEPLOYER=
L1_RPC=
\ No newline at end of file
module.exports = {
...require('../../.prettierrc.js'),
};
\ No newline at end of file
(The MIT License)
Copyright 2020-2024 Optimism
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# @eth-optimism/devnet-tasks
`@eth-optimism/devnet-tasks` is a temporary package that hosts two [Hardhat](https://hardhat.org/) tasks used within the [`bedrock-devnet`](/bedrock-devnet/README.md) and is not meant to be published to NPM.
You can generally disregard this package unless you are working on the `bedrock-devnet`.
import 'dotenv/config'
import { HardhatUserConfig } from 'hardhat/types'
import { ethers } from 'ethers'
import '@nomiclabs/hardhat-ethers'
import '@nomiclabs/hardhat-waffle'
import 'hardhat-deploy'
import './src/tasks'
const config: HardhatUserConfig = {
solidity: {
version: '0.8.9',
},
paths: {
sources: './test/contracts',
},
networks: {
mainnet: {
url: process.env.L1_RPC || 'https://mainnet-l1-rehearsal.optimism.io',
accounts: [
'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',
],
},
devnetL1: {
url: 'http://localhost:8545',
accounts: [
// warning: keys 0 - 12 (incl) are used by the system
'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', // 0
'59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d', // 1
'5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a', // 2
'7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6', // 3
'47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a', // 4
'8b3a350cf5c34c9194ca85829a2df0ec3153be0318b5e2d3348e872092edffba', // 5
'92db14e403b83dfe3df233f83dfa3a0d7096f21ca9b0d6d6b8d88b2b4ec1564e', // 6
'4bbbf85ce3377467afe5d46f804f221813b2bb87f24d81f60f1fcdbf7cbf4356', // 7
'dbda1821b80551c9d65939329250298aa3472ba22feea921c0cf5d620ea67b97', // 8
'2a871d0798f97d79848a013d4936a73bf4cc922c825d33c1cf7073dff6d409c6', // 9
'f214f2b2cd398c806f84e317254e0f0b801d0643303237d97a22a48e01628897', // 10
'701b615bbdfb9de65240bc28bd21bbc0d996645a3dd57e7b12bc2bdf6f192c82', // 11
'a267530f49f8280200edf313ee7af6b827f2a8bce2897751d06a843f644967b1', // 12
'47c99abed3324a2707c28affff1267e45918ec8c3f20b8aa892e8b065d2942dd', // 13
'c526ee95bf44d8fc405a158bb884d9d1238d99f0612e9f33d006bb0789009aaa', // 14
'8166f546bab6da521a8369cab06c5d2b9e46670292d85c875ee9ec20e84ffb61', // 15
'ea6c44ac03bff858b476bba40716402b03e41b8e97e276d1baec7c37d42484a0', // 16
'689af8efa8c651a91ad287602527f3af2fe9f6501a7ac4b061667b5a93e037fd', // 17
'de9be858da4a475276426320d5e9262ecfc3ba460bfac56360bfa6c4c28b4ee0', // 18
'df57089febbacf7ba0bc227dafbffa9fc08a93fdc68e1e42411a14efcf23656e', // 19
],
},
hivenet: {
url: process.env.L1_RPC || '',
accounts: [process.env.PRIVATE_KEY_DEPLOYER || ethers.constants.HashZero],
},
goerli: {
url: process.env.L1_RPC || '',
accounts: [process.env.PRIVATE_KEY_DEPLOYER || ethers.constants.HashZero],
},
sepolia: {
url: process.env.L1_RPC || '',
accounts: [process.env.PRIVATE_KEY_DEPLOYER || ethers.constants.HashZero],
},
},
external: {
contracts: [
{
artifacts: '../contracts-bedrock/artifacts',
},
],
deployments: {
mainnet: [
'../contracts/deployments/mainnet',
'../contracts-bedrock/deployments/mainnet',
],
hivenet: ['../contracts-bedrock/deployments/hivenet'],
devnetL1: ['../contracts-bedrock/deployments/devnetL1'],
goerli: [
'../contracts-bedrock/deployments/goerli',
'../contracts/deployments/goerli',
],
sepolia: [
'../contracts-bedrock/deployments/sepolia',
'../contracts/deployments/sepolia',
],
},
},
}
export default config
{
"private": true,
"name": "@eth-optimism/devnet-tasks",
"version": "3.3.1",
"description": "[Optimism] Hardhat devnet testing tasks",
"main": "dist/index",
"types": "dist/index",
"files": [
"dist/*",
"src/*"
],
"scripts": {
"all": "pnpm clean && pnpm build && pnpm test && pnpm lint:fix && pnpm lint",
"build": "tsc -p tsconfig.json",
"clean": "rimraf dist/ ./tsconfig.tsbuildinfo",
"lint": "pnpm lint:fix && pnpm lint:check",
"lint:check": "eslint . --max-warnings=0",
"lint:fix": "pnpm lint:check --fix"
},
"keywords": [
"optimism",
"ethereum",
"devnet"
],
"homepage": "https://github.com/ethereum-optimism/optimism/tree/develop/packages/devnet-tasks#readme",
"license": "MIT",
"author": "Optimism PBC",
"repository": {
"type": "git",
"url": "https://github.com/ethereum-optimism/optimism.git"
},
"devDependencies": {
"@nomiclabs/hardhat-ethers": "^2.2.3",
"@nomiclabs/hardhat-waffle": "^2.0.1",
"@types/node": "^20.14.12",
"ethers": "^5.7.2",
"hardhat": "^2.20.1",
"hardhat-deploy": "^0.12.4",
"typescript": "^5.5.4"
},
"dependencies": {
"@eth-optimism/core-utils": "^0.13.2",
"@eth-optimism/sdk": "^3.3.2",
"dotenv": "^16.4.5"
},
"peerDependencies": {
"ethers": "^5"
}
}
import { promises as fs } from 'fs'
import { task, types } from 'hardhat/config'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import { Event, Contract, Wallet, providers, utils, ethers } from 'ethers'
import { predeploys, sleep } from '@eth-optimism/core-utils'
import Artifact__WETH9 from '@eth-optimism/sdk/dist/forge-artifacts/WETH9.json'
import Artifact__OptimismMintableERC20TokenFactory from '@eth-optimism/sdk/dist/forge-artifacts/OptimismMintableERC20Factory.json'
import Artifact__OptimismMintableERC20Token from '@eth-optimism/sdk/dist/forge-artifacts/OptimismMintableERC20.json'
import Artifact__L2ToL1MessagePasser from '@eth-optimism/sdk/dist/forge-artifacts/L2ToL1MessagePasser.json'
import Artifact__L2CrossDomainMessenger from '@eth-optimism/sdk/dist/forge-artifacts/L2CrossDomainMessenger.json'
import Artifact__L2StandardBridge from '@eth-optimism/sdk/dist/forge-artifacts/L2StandardBridge.json'
import Artifact__OptimismPortal from '@eth-optimism/sdk/dist/forge-artifacts/OptimismPortal.json'
import Artifact__L1CrossDomainMessenger from '@eth-optimism/sdk/dist/forge-artifacts/L1CrossDomainMessenger.json'
import Artifact__L1StandardBridge from '@eth-optimism/sdk/dist/forge-artifacts/L1StandardBridge.json'
import Artifact__L2OutputOracle from '@eth-optimism/sdk/dist/forge-artifacts/L2OutputOracle.json'
import {
CrossChainMessenger,
MessageStatus,
CONTRACT_ADDRESSES,
OEContractsLike,
DEFAULT_L2_CONTRACT_ADDRESSES,
} from '@eth-optimism/sdk/dist'
const deployWETH9 = async (
hre: HardhatRuntimeEnvironment,
signer: SignerWithAddress,
wrap: boolean
): Promise<Contract> => {
const Factory__WETH9 = new hre.ethers.ContractFactory(
Artifact__WETH9.abi,
Artifact__WETH9.bytecode.object,
signer
)
console.log('Sending deployment transaction')
const WETH9 = await Factory__WETH9.deploy()
const receipt = await WETH9.deployTransaction.wait()
console.log(`WETH9 deployed: ${receipt.transactionHash}`)
if (wrap) {
const deposit = await signer.sendTransaction({
value: utils.parseEther('1'),
to: WETH9.address,
})
await deposit.wait()
}
return WETH9
}
const createOptimismMintableERC20 = async (
hre: HardhatRuntimeEnvironment,
L1ERC20: Contract,
l2Signer: Wallet
): Promise<Contract> => {
const OptimismMintableERC20TokenFactory = new Contract(
predeploys.OptimismMintableERC20Factory,
Artifact__OptimismMintableERC20TokenFactory.abi,
l2Signer
)
const name = await L1ERC20.name()
const symbol = await L1ERC20.symbol()
const tx =
await OptimismMintableERC20TokenFactory.createOptimismMintableERC20(
L1ERC20.address,
`L2 ${name}`,
`L2-${symbol}`
)
const receipt = await tx.wait()
const event = receipt.events.find(
(e: Event) => e.event === 'OptimismMintableERC20Created'
)
if (!event) {
throw new Error('Unable to find OptimismMintableERC20Created event')
}
const l2WethAddress = event.args.localToken
console.log(`Deployed to ${l2WethAddress}`)
return new Contract(
l2WethAddress,
Artifact__OptimismMintableERC20Token.abi,
l2Signer
)
}
// TODO(tynes): this task could be modularized in the future
// so that it can deposit an arbitrary token. Right now it
// deploys a WETH9 contract, mints some WETH9 and then
// deposits that into L2 through the StandardBridge.
task('deposit-erc20', 'Deposits WETH9 onto L2.')
.addParam(
'l2ProviderUrl',
'L2 provider URL.',
'http://localhost:9545',
types.string
)
.addParam(
'opNodeProviderUrl',
'op-node provider URL',
'http://localhost:7545',
types.string
)
.addOptionalParam(
'l1ContractsJsonPath',
'Path to a JSON with L1 contract addresses in it',
'',
types.string
)
.addOptionalParam('signerIndex', 'Index of signer to use', 0, types.int)
.setAction(async (args, hre) => {
const signers = await hre.ethers.getSigners()
if (signers.length === 0) {
throw new Error('No configured signers')
}
if (args.signerIndex < 0 || signers.length <= args.signerIndex) {
throw new Error('Invalid signer index')
}
const signer = signers[args.signerIndex]
const address = await signer.getAddress()
console.log(`Using signer ${address}`)
// Ensure that the signer has a balance before trying to
// do anything
const balance = await signer.getBalance()
if (balance.eq(0)) {
throw new Error('Signer has no balance')
}
const l2Provider = new providers.StaticJsonRpcProvider(args.l2ProviderUrl)
const l2Signer = new hre.ethers.Wallet(
hre.network.config.accounts[args.signerIndex],
l2Provider
)
const l2ChainId = await l2Signer.getChainId()
let contractAddrs = CONTRACT_ADDRESSES[l2ChainId]
if (args.l1ContractsJsonPath) {
const data = await fs.readFile(args.l1ContractsJsonPath)
const json = JSON.parse(data.toString())
contractAddrs = {
l1: {
AddressManager: json.AddressManager,
L1CrossDomainMessenger: json.L1CrossDomainMessengerProxy,
L1StandardBridge: json.L1StandardBridgeProxy,
StateCommitmentChain: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
OptimismPortal: json.OptimismPortalProxy,
L2OutputOracle: json.L2OutputOracleProxy,
OptimismPortal2: json.OptimismPortalProxy,
DisputeGameFactory: json.DisputeGameFactoryProxy,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
} as OEContractsLike
}
console.log(`OptimismPortal: ${contractAddrs.l1.OptimismPortal}`)
const OptimismPortal = new hre.ethers.Contract(
contractAddrs.l1.OptimismPortal,
Artifact__OptimismPortal.abi,
signer
)
console.log(
`L1CrossDomainMessenger: ${contractAddrs.l1.L1CrossDomainMessenger}`
)
const L1CrossDomainMessenger = new hre.ethers.Contract(
contractAddrs.l1.L1CrossDomainMessenger,
Artifact__L1CrossDomainMessenger.abi,
signer
)
console.log(`L1StandardBridge: ${contractAddrs.l1.L1StandardBridge}`)
const L1StandardBridge = new hre.ethers.Contract(
contractAddrs.l1.L1StandardBridge,
Artifact__L1StandardBridge.abi,
signer
)
const L2OutputOracle = new hre.ethers.Contract(
contractAddrs.l1.L2OutputOracle,
Artifact__L2OutputOracle.abi,
signer
)
const L2ToL1MessagePasser = new hre.ethers.Contract(
predeploys.L2ToL1MessagePasser,
Artifact__L2ToL1MessagePasser.abi
)
const L2CrossDomainMessenger = new hre.ethers.Contract(
predeploys.L2CrossDomainMessenger,
Artifact__L2CrossDomainMessenger.abi
)
const L2StandardBridge = new hre.ethers.Contract(
predeploys.L2StandardBridge,
Artifact__L2StandardBridge.abi
)
const messenger = new CrossChainMessenger({
l1SignerOrProvider: signer,
l2SignerOrProvider: l2Signer,
l1ChainId: await signer.getChainId(),
l2ChainId,
bedrock: true,
contracts: contractAddrs,
})
const params = await OptimismPortal.params()
console.log('Intial OptimismPortal.params:')
console.log(params)
console.log('Deploying WETH9 to L1')
const WETH9 = await deployWETH9(hre, signer, true)
console.log(`Deployed to ${WETH9.address}`)
console.log('Creating L2 WETH9')
const OptimismMintableERC20 = await createOptimismMintableERC20(
hre,
WETH9,
l2Signer
)
console.log(`Approving WETH9 for deposit`)
const approvalTx = await messenger.approveERC20(
WETH9.address,
OptimismMintableERC20.address,
hre.ethers.constants.MaxUint256
)
await approvalTx.wait()
console.log('WETH9 approved')
console.log('Depositing WETH9 to L2')
const depositTx = await messenger.depositERC20(
WETH9.address,
OptimismMintableERC20.address,
utils.parseEther('1')
)
await depositTx.wait()
console.log(`ERC20 deposited - ${depositTx.hash}`)
console.log('Checking to make sure deposit was successful')
// Deposit might get reorged, wait and also log for reorgs.
let prevBlockHash: string = ''
for (let i = 0; i < 12; i++) {
const messageReceipt = await signer.provider!.getTransactionReceipt(
depositTx.hash
)
if (messageReceipt.status !== 1) {
console.log(`Deposit failed, retrying...`)
}
// Wait for stability, we want some amount of time after any reorg
if (prevBlockHash !== '' && messageReceipt.blockHash !== prevBlockHash) {
console.log(
`Block hash changed from ${prevBlockHash} to ${messageReceipt.blockHash}`
)
i = 0
} else if (prevBlockHash !== '') {
console.log(`No reorg detected: ${i}`)
}
prevBlockHash = messageReceipt.blockHash
await sleep(1000)
}
console.log(`Deposit confirmed`)
const l2Balance = await OptimismMintableERC20.balanceOf(address)
if (l2Balance.lt(utils.parseEther('1'))) {
throw new Error(
`bad deposit. recipient balance on L2: ${utils.formatEther(l2Balance)}`
)
}
console.log(`Deposit success`)
console.log('Starting withdrawal')
const preBalance = await WETH9.balanceOf(signer.address)
const withdraw = await messenger.withdrawERC20(
WETH9.address,
OptimismMintableERC20.address,
utils.parseEther('1')
)
const withdrawalReceipt = await withdraw.wait()
for (const log of withdrawalReceipt.logs) {
switch (log.address) {
case L2ToL1MessagePasser.address: {
const parsed = L2ToL1MessagePasser.interface.parseLog(log)
console.log(`Log ${parsed.name} from ${log.address}`)
console.log(parsed.args)
console.log()
break
}
case L2StandardBridge.address: {
const parsed = L2StandardBridge.interface.parseLog(log)
console.log(`Log ${parsed.name} from ${log.address}`)
console.log(parsed.args)
console.log()
break
}
case L2CrossDomainMessenger.address: {
const parsed = L2CrossDomainMessenger.interface.parseLog(log)
console.log(`Log ${parsed.name} from ${log.address}`)
console.log(parsed.args)
console.log()
break
}
default: {
console.log(`Unknown log from ${log.address} - ${log.topics[0]}`)
}
}
}
setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(withdraw)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
const latest = await L2OutputOracle.latestBlockNumber()
console.log(
`Latest L2OutputOracle commitment number: ${latest.toString()}`
)
const tip = await signer.provider!.getBlockNumber()
console.log(`L1 chain tip: ${tip.toString()}`)
}, 3000)
const now = Math.floor(Date.now() / 1000)
console.log('Waiting for message to be able to be proved')
await messenger.waitForMessageStatus(withdraw, MessageStatus.READY_TO_PROVE)
console.log('Proving withdrawal...')
const prove = await messenger.proveMessage(withdraw)
const proveReceipt = await prove.wait()
console.log(proveReceipt)
if (proveReceipt.status !== 1) {
throw new Error('Prove withdrawal transaction reverted')
}
console.log('Waiting for message to be able to be relayed')
await messenger.waitForMessageStatus(
withdraw,
MessageStatus.READY_FOR_RELAY
)
console.log('Finalizing withdrawal...')
// TODO: Update SDK to properly estimate gas
const finalize = await messenger.finalizeMessage(withdraw, {
overrides: { gasLimit: 500_000 },
})
const finalizeReceipt = await finalize.wait()
console.log('finalizeReceipt:', finalizeReceipt)
console.log(`Took ${Math.floor(Date.now() / 1000) - now} seconds`)
for (const log of finalizeReceipt.logs) {
switch (log.address) {
case OptimismPortal.address: {
const parsed = OptimismPortal.interface.parseLog(log)
console.log(`Log ${parsed.name} from OptimismPortal (${log.address})`)
console.log(parsed.args)
console.log()
break
}
case L1CrossDomainMessenger.address: {
const parsed = L1CrossDomainMessenger.interface.parseLog(log)
console.log(
`Log ${parsed.name} from L1CrossDomainMessenger (${log.address})`
)
console.log(parsed.args)
console.log()
break
}
case L1StandardBridge.address: {
const parsed = L1StandardBridge.interface.parseLog(log)
console.log(
`Log ${parsed.name} from L1StandardBridge (${log.address})`
)
console.log(parsed.args)
console.log()
break
}
case WETH9.address: {
const parsed = WETH9.interface.parseLog(log)
console.log(`Log ${parsed.name} from WETH9 (${log.address})`)
console.log(parsed.args)
console.log()
break
}
default:
console.log(
`Unknown log emitted from ${log.address} - ${log.topics[0]}`
)
}
}
const postBalance = await WETH9.balanceOf(signer.address)
const expectedBalance = preBalance.add(utils.parseEther('1'))
if (!expectedBalance.eq(postBalance)) {
throw new Error(
`Balance mismatch, expected: ${expectedBalance}, actual: ${postBalance}`
)
}
console.log('Withdrawal success')
})
import { promises as fs } from 'fs'
import { task, types } from 'hardhat/config'
import { Deployment } from 'hardhat-deploy/types'
import '@nomiclabs/hardhat-ethers'
import 'hardhat-deploy'
import { predeploys } from '@eth-optimism/core-utils'
import { providers, utils, ethers } from 'ethers'
import Artifact__L2ToL1MessagePasser from '@eth-optimism/sdk/dist/forge-artifacts/L2ToL1MessagePasser.json'
import Artifact__L2CrossDomainMessenger from '@eth-optimism/sdk/dist/forge-artifacts/L2CrossDomainMessenger.json'
import Artifact__L2StandardBridge from '@eth-optimism/sdk/dist/forge-artifacts/L2StandardBridge.json'
import Artifact__OptimismPortal from '@eth-optimism/sdk/dist/forge-artifacts/OptimismPortal.json'
import Artifact__L1CrossDomainMessenger from '@eth-optimism/sdk/dist/forge-artifacts/L1CrossDomainMessenger.json'
import Artifact__L1StandardBridge from '@eth-optimism/sdk/dist/forge-artifacts/L1StandardBridge.json'
import Artifact__L2OutputOracle from '@eth-optimism/sdk/dist/forge-artifacts/L2OutputOracle.json'
import {
CrossChainMessenger,
MessageStatus,
CONTRACT_ADDRESSES,
OEContractsLike,
DEFAULT_L2_CONTRACT_ADDRESSES,
} from '@eth-optimism/sdk'
const { formatEther } = utils
task('deposit-eth', 'Deposits ether to L2.')
.addParam(
'l2ProviderUrl',
'L2 provider URL.',
'http://localhost:9545',
types.string
)
.addOptionalParam('to', 'Recipient of the ether', '', types.string)
.addOptionalParam(
'amount',
'Amount of ether to send (in ETH)',
'',
types.string
)
.addOptionalParam(
'withdraw',
'Follow up with a withdrawal',
true,
types.boolean
)
.addOptionalParam(
'l1ContractsJsonPath',
'Path to a JSON with L1 contract addresses in it',
'',
types.string
)
.addOptionalParam('signerIndex', 'Index of signer to use', 0, types.int)
.addOptionalParam('withdrawAmount', 'Amount to withdraw', '', types.string)
.setAction(async (args, hre) => {
const signers = await hre.ethers.getSigners()
if (signers.length === 0) {
throw new Error('No configured signers')
}
if (args.signerIndex < 0 || signers.length <= args.signerIndex) {
throw new Error('Invalid signer index')
}
const signer = signers[args.signerIndex]
const address = await signer.getAddress()
console.log(`Using signer ${address}`)
// Ensure that the signer has a balance before trying to
// do anything
const balance = await signer.getBalance()
if (balance.eq(0)) {
throw new Error('Signer has no balance')
}
console.log(`Signer balance: ${formatEther(balance.toString())}`)
const l2Provider = new providers.StaticJsonRpcProvider(args.l2ProviderUrl)
// send to self if not specified
const to = args.to ? args.to : address
const amount = args.amount
? utils.parseEther(args.amount)
: utils.parseEther('1')
const withdrawAmount = args.withdrawAmount
? utils.parseEther(args.withdrawAmount)
: amount.div(2)
const l2Signer = new hre.ethers.Wallet(
hre.network.config.accounts[args.signerIndex],
l2Provider
)
const l2ChainId = await l2Signer.getChainId()
let contractAddrs = CONTRACT_ADDRESSES[l2ChainId]
if (args.l1ContractsJsonPath) {
const data = await fs.readFile(args.l1ContractsJsonPath)
const json = JSON.parse(data.toString())
contractAddrs = {
l1: {
AddressManager: json.AddressManager,
L1CrossDomainMessenger: json.L1CrossDomainMessengerProxy,
L1StandardBridge: json.L1StandardBridgeProxy,
StateCommitmentChain: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
OptimismPortal: json.OptimismPortalProxy,
L2OutputOracle: json.L2OutputOracleProxy,
OptimismPortal2: json.OptimismPortalProxy,
DisputeGameFactory: json.DisputeGameFactoryProxy,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
} as OEContractsLike
} else if (!contractAddrs) {
// If the contract addresses have not been hardcoded,
// attempt to read them from deployment artifacts
let Deployment__AddressManager: Deployment
try {
Deployment__AddressManager = await hre.deployments.get('AddressManager')
} catch (e) {
Deployment__AddressManager = await hre.deployments.get(
'Lib_AddressManager'
)
}
let Deployment__L1CrossDomainMessenger: Deployment
try {
Deployment__L1CrossDomainMessenger = await hre.deployments.get(
'L1CrossDomainMessengerProxy'
)
} catch (e) {
Deployment__L1CrossDomainMessenger = await hre.deployments.get(
'Proxy__OVM_L1CrossDomainMessenger'
)
}
let Deployment__L1StandardBridge: Deployment
try {
Deployment__L1StandardBridge = await hre.deployments.get(
'L1StandardBridgeProxy'
)
} catch (e) {
Deployment__L1StandardBridge = await hre.deployments.get(
'Proxy__OVM_L1StandardBridge'
)
}
const Deployment__OptimismPortal = await hre.deployments.get(
'OptimismPortalProxy'
)
const Deployment__L2OutputOracle = await hre.deployments.get(
'L2OutputOracleProxy'
)
contractAddrs = {
l1: {
AddressManager: Deployment__AddressManager.address,
L1CrossDomainMessenger: Deployment__L1CrossDomainMessenger.address,
L1StandardBridge: Deployment__L1StandardBridge.address,
StateCommitmentChain: ethers.constants.AddressZero,
CanonicalTransactionChain: ethers.constants.AddressZero,
BondManager: ethers.constants.AddressZero,
OptimismPortal: Deployment__OptimismPortal.address,
L2OutputOracle: Deployment__L2OutputOracle.address,
},
l2: DEFAULT_L2_CONTRACT_ADDRESSES,
}
}
console.log(`OptimismPortal: ${contractAddrs.l1.OptimismPortal}`)
const OptimismPortal = new hre.ethers.Contract(
contractAddrs.l1.OptimismPortal,
Artifact__OptimismPortal.abi,
signer
)
console.log(
`L1CrossDomainMessenger: ${contractAddrs.l1.L1CrossDomainMessenger}`
)
const L1CrossDomainMessenger = new hre.ethers.Contract(
contractAddrs.l1.L1CrossDomainMessenger,
Artifact__L1CrossDomainMessenger.abi,
signer
)
console.log(`L1StandardBridge: ${contractAddrs.l1.L1StandardBridge}`)
const L1StandardBridge = new hre.ethers.Contract(
contractAddrs.l1.L1StandardBridge,
Artifact__L1StandardBridge.abi,
signer
)
console.log(`L2OutputOracle: ${contractAddrs.l1.L2OutputOracle}`)
const L2OutputOracle = new hre.ethers.Contract(
contractAddrs.l1.L2OutputOracle,
Artifact__L2OutputOracle.abi,
signer
)
const L2ToL1MessagePasser = new hre.ethers.Contract(
predeploys.L2ToL1MessagePasser,
Artifact__L2ToL1MessagePasser.abi
)
const L2CrossDomainMessenger = new hre.ethers.Contract(
predeploys.L2CrossDomainMessenger,
Artifact__L2CrossDomainMessenger.abi
)
const L2StandardBridge = new hre.ethers.Contract(
predeploys.L2StandardBridge,
Artifact__L2StandardBridge.abi
)
const messenger = new CrossChainMessenger({
l1SignerOrProvider: signer,
l2SignerOrProvider: l2Signer,
l1ChainId: await signer.getChainId(),
l2ChainId,
bedrock: true,
contracts: contractAddrs,
})
const opBalanceBefore = await signer!.provider!.getBalance(
OptimismPortal.address
)
const l1BridgeBalanceBefore = await signer!.provider!.getBalance(
L1StandardBridge.address
)
// Deposit ETH
console.log('Depositing ETH through StandardBridge')
console.log(`Sending ${formatEther(amount)} ether`)
const ethDeposit = await messenger.depositETH(amount, { recipient: to })
console.log(`Transaction hash: ${ethDeposit.hash}`)
const depositMessageReceipt = await messenger.waitForMessageReceipt(
ethDeposit
)
if (depositMessageReceipt.receiptStatus !== 1) {
throw new Error('deposit failed')
}
console.log(
`Deposit complete - included in block ${depositMessageReceipt.transactionReceipt.blockNumber}`
)
const opBalanceAfter = await signer!.provider!.getBalance(
OptimismPortal.address
)
const l1BridgeBalanceAfter = await signer!.provider!.getBalance(
L1StandardBridge.address
)
console.log(
`L1StandardBridge balance before: ${formatEther(l1BridgeBalanceBefore)}`
)
console.log(
`L1StandardBridge balance after: ${formatEther(l1BridgeBalanceAfter)}`
)
console.log(
`OptimismPortal balance before: ${formatEther(opBalanceBefore)}`
)
console.log(`OptimismPortal balance after: ${formatEther(opBalanceAfter)}`)
if (!opBalanceBefore.add(amount).eq(opBalanceAfter)) {
throw new Error(`OptimismPortal balance mismatch`)
}
const l2Balance = await l2Provider.getBalance(to)
console.log(
`L2 balance of deposit recipient: ${utils.formatEther(
l2Balance.toString()
)}`
)
if (!args.withdraw) {
return
}
console.log('Withdrawing ETH')
const ethWithdraw = await messenger.withdrawETH(withdrawAmount)
console.log(`Transaction hash: ${ethWithdraw.hash}`)
const ethWithdrawReceipt = await ethWithdraw.wait()
console.log(
`ETH withdrawn on L2 - included in block ${ethWithdrawReceipt.blockNumber}`
)
{
// check the logs
for (const log of ethWithdrawReceipt.logs) {
switch (log.address) {
case L2ToL1MessagePasser.address: {
const parsed = L2ToL1MessagePasser.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
break
}
case L2StandardBridge.address: {
const parsed = L2StandardBridge.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
break
}
case L2CrossDomainMessenger.address: {
const parsed = L2CrossDomainMessenger.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
break
}
default: {
console.log(`Unknown log from ${log.address} - ${log.topics[0]}`)
}
}
}
}
console.log('Waiting to be able to prove withdrawal')
const proveInterval = setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(ethWithdrawReceipt)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
const latest = await L2OutputOracle.latestBlockNumber()
console.log(
`Latest L2OutputOracle commitment number: ${latest.toString()}`
)
const tip = await signer.provider!.getBlockNumber()
console.log(`L1 chain tip: ${tip.toString()}`)
}, 3000)
try {
await messenger.waitForMessageStatus(
ethWithdrawReceipt,
MessageStatus.READY_TO_PROVE
)
} finally {
clearInterval(proveInterval)
}
console.log('Proving eth withdrawal...')
const ethProve = await messenger.proveMessage(ethWithdrawReceipt)
console.log(`Transaction hash: ${ethProve.hash}`)
const ethProveReceipt = await ethProve.wait()
if (ethProveReceipt.status !== 1) {
throw new Error('Prove withdrawal transaction reverted')
}
console.log('Successfully proved withdrawal')
console.log('Waiting to be able to finalize withdrawal')
const finalizeInterval = setInterval(async () => {
const currentStatus = await messenger.getMessageStatus(ethWithdrawReceipt)
console.log(`Message status: ${MessageStatus[currentStatus]}`)
}, 3000)
try {
await messenger.waitForMessageStatus(
ethWithdrawReceipt,
MessageStatus.READY_FOR_RELAY
)
} finally {
clearInterval(finalizeInterval)
}
console.log('Finalizing eth withdrawal...')
const ethFinalize = await messenger.finalizeMessage(ethWithdrawReceipt)
console.log(`Transaction hash: ${ethFinalize.hash}`)
const ethFinalizeReceipt = await ethFinalize.wait()
if (ethFinalizeReceipt.status !== 1) {
throw new Error('Finalize withdrawal reverted')
}
console.log(
`ETH withdrawal complete - included in block ${ethFinalizeReceipt.blockNumber}`
)
{
// Check that the logs are correct
for (const log of ethFinalizeReceipt.logs) {
switch (log.address) {
case L1StandardBridge.address: {
const parsed = L1StandardBridge.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
if (
parsed.name !== 'ETHBridgeFinalized' &&
parsed.name !== 'ETHWithdrawalFinalized'
) {
throw new Error('Wrong event name from L1StandardBridge')
}
if (!parsed.args.amount.eq(withdrawAmount)) {
throw new Error('Wrong amount in event')
}
if (parsed.args.from !== address) {
throw new Error('Wrong to in event')
}
if (parsed.args.to !== address) {
throw new Error('Wrong from in event')
}
break
}
case L1CrossDomainMessenger.address: {
const parsed = L1CrossDomainMessenger.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
if (parsed.name !== 'RelayedMessage') {
throw new Error('Wrong event from L1CrossDomainMessenger')
}
break
}
case OptimismPortal.address: {
const parsed = OptimismPortal.interface.parseLog(log)
console.log(parsed.name)
console.log(parsed.args)
console.log()
// TODO: remove this if check
if (parsed.name === 'WithdrawalFinalized') {
if (parsed.args.success !== true) {
throw new Error('Unsuccessful withdrawal call')
}
}
break
}
default: {
console.log(`Unknown log from ${log.address} - ${log.topics[0]}`)
}
}
}
}
const opBalanceFinally = await signer!.provider!.getBalance(
OptimismPortal.address
)
if (!opBalanceFinally.add(withdrawAmount).eq(opBalanceAfter)) {
throw new Error('OptimismPortal balance mismatch')
}
console.log('Withdraw success')
})
import './deposit-eth'
import './deposit-erc20'
{
"compilerOptions": {
"lib": ["ES2021"],
"rootDir": "./src",
"outDir": "./dist",
"skipLibCheck": true,
"module": "commonjs",
"target": "es2017",
"sourceMap": true,
"esModuleInterop": true,
"composite": true,
"resolveJsonModule": true,
"declaration": true,
"noImplicitAny": false,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"typeRoots": [
"node_modules/@types"
]
},
"exclude": [
"node_modules",
"dist"
],
"include": [
"src/**/*",
"src/forge-artifacts/*.json"
]
}
This source diff could not be displayed because it is too large. You can view the blob instead.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment