github.com/ethereum-optimism/optimism@v1.7.2/packages/sdk/tasks/deposit-erc20.ts (about) 1 import { promises as fs } from 'fs' 2 3 import { task, types } from 'hardhat/config' 4 import { HardhatRuntimeEnvironment } from 'hardhat/types' 5 import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers' 6 import '@nomiclabs/hardhat-ethers' 7 import 'hardhat-deploy' 8 import { Event, Contract, Wallet, providers, utils, ethers } from 'ethers' 9 import { predeploys, sleep } from '@eth-optimism/core-utils' 10 import Artifact__WETH9 from '@eth-optimism/contracts-bedrock/forge-artifacts/WETH9.sol/WETH9.json' 11 import Artifact__OptimismMintableERC20TokenFactory from '@eth-optimism/contracts-bedrock/forge-artifacts/OptimismMintableERC20Factory.sol/OptimismMintableERC20Factory.json' 12 import Artifact__OptimismMintableERC20Token from '@eth-optimism/contracts-bedrock/forge-artifacts/OptimismMintableERC20.sol/OptimismMintableERC20.json' 13 import Artifact__L2ToL1MessagePasser from '@eth-optimism/contracts-bedrock/forge-artifacts/L2ToL1MessagePasser.sol/L2ToL1MessagePasser.json' 14 import Artifact__L2CrossDomainMessenger from '@eth-optimism/contracts-bedrock/forge-artifacts/L2CrossDomainMessenger.sol/L2CrossDomainMessenger.json' 15 import Artifact__L2StandardBridge from '@eth-optimism/contracts-bedrock/forge-artifacts/L2StandardBridge.sol/L2StandardBridge.json' 16 import Artifact__OptimismPortal from '@eth-optimism/contracts-bedrock/forge-artifacts/OptimismPortal.sol/OptimismPortal.json' 17 import Artifact__L1CrossDomainMessenger from '@eth-optimism/contracts-bedrock/forge-artifacts/L1CrossDomainMessenger.sol/L1CrossDomainMessenger.json' 18 import Artifact__L1StandardBridge from '@eth-optimism/contracts-bedrock/forge-artifacts/L1StandardBridge.sol/L1StandardBridge.json' 19 import Artifact__L2OutputOracle from '@eth-optimism/contracts-bedrock/forge-artifacts/L2OutputOracle.sol/L2OutputOracle.json' 20 21 import { 22 CrossChainMessenger, 23 MessageStatus, 24 CONTRACT_ADDRESSES, 25 OEContractsLike, 26 DEFAULT_L2_CONTRACT_ADDRESSES, 27 } from '../src' 28 29 const deployWETH9 = async ( 30 hre: HardhatRuntimeEnvironment, 31 signer: SignerWithAddress, 32 wrap: boolean 33 ): Promise<Contract> => { 34 const Factory__WETH9 = new hre.ethers.ContractFactory( 35 Artifact__WETH9.abi, 36 Artifact__WETH9.bytecode.object, 37 signer 38 ) 39 40 console.log('Sending deployment transaction') 41 const WETH9 = await Factory__WETH9.deploy() 42 const receipt = await WETH9.deployTransaction.wait() 43 console.log(`WETH9 deployed: ${receipt.transactionHash}`) 44 45 if (wrap) { 46 const deposit = await signer.sendTransaction({ 47 value: utils.parseEther('1'), 48 to: WETH9.address, 49 }) 50 await deposit.wait() 51 } 52 53 return WETH9 54 } 55 56 const createOptimismMintableERC20 = async ( 57 hre: HardhatRuntimeEnvironment, 58 L1ERC20: Contract, 59 l2Signer: Wallet 60 ): Promise<Contract> => { 61 const OptimismMintableERC20TokenFactory = new Contract( 62 predeploys.OptimismMintableERC20Factory, 63 Artifact__OptimismMintableERC20TokenFactory.abi, 64 l2Signer 65 ) 66 67 const name = await L1ERC20.name() 68 const symbol = await L1ERC20.symbol() 69 70 const tx = 71 await OptimismMintableERC20TokenFactory.createOptimismMintableERC20( 72 L1ERC20.address, 73 `L2 ${name}`, 74 `L2-${symbol}` 75 ) 76 77 const receipt = await tx.wait() 78 const event = receipt.events.find( 79 (e: Event) => e.event === 'OptimismMintableERC20Created' 80 ) 81 82 if (!event) { 83 throw new Error('Unable to find OptimismMintableERC20Created event') 84 } 85 86 const l2WethAddress = event.args.localToken 87 console.log(`Deployed to ${l2WethAddress}`) 88 89 return new Contract( 90 l2WethAddress, 91 Artifact__OptimismMintableERC20Token.abi, 92 l2Signer 93 ) 94 } 95 96 // TODO(tynes): this task could be modularized in the future 97 // so that it can deposit an arbitrary token. Right now it 98 // deploys a WETH9 contract, mints some WETH9 and then 99 // deposits that into L2 through the StandardBridge. 100 task('deposit-erc20', 'Deposits WETH9 onto L2.') 101 .addParam( 102 'l2ProviderUrl', 103 'L2 provider URL.', 104 'http://localhost:9545', 105 types.string 106 ) 107 .addParam( 108 'opNodeProviderUrl', 109 'op-node provider URL', 110 'http://localhost:7545', 111 types.string 112 ) 113 .addOptionalParam( 114 'l1ContractsJsonPath', 115 'Path to a JSON with L1 contract addresses in it', 116 '', 117 types.string 118 ) 119 .addOptionalParam('signerIndex', 'Index of signer to use', 0, types.int) 120 .setAction(async (args, hre) => { 121 const signers = await hre.ethers.getSigners() 122 if (signers.length === 0) { 123 throw new Error('No configured signers') 124 } 125 if (args.signerIndex < 0 || signers.length <= args.signerIndex) { 126 throw new Error('Invalid signer index') 127 } 128 const signer = signers[args.signerIndex] 129 const address = await signer.getAddress() 130 console.log(`Using signer ${address}`) 131 132 // Ensure that the signer has a balance before trying to 133 // do anything 134 const balance = await signer.getBalance() 135 if (balance.eq(0)) { 136 throw new Error('Signer has no balance') 137 } 138 139 const l2Provider = new providers.StaticJsonRpcProvider(args.l2ProviderUrl) 140 141 const l2Signer = new hre.ethers.Wallet( 142 hre.network.config.accounts[args.signerIndex], 143 l2Provider 144 ) 145 146 const l2ChainId = await l2Signer.getChainId() 147 let contractAddrs = CONTRACT_ADDRESSES[l2ChainId] 148 if (args.l1ContractsJsonPath) { 149 const data = await fs.readFile(args.l1ContractsJsonPath) 150 const json = JSON.parse(data.toString()) 151 contractAddrs = { 152 l1: { 153 AddressManager: json.AddressManager, 154 L1CrossDomainMessenger: json.L1CrossDomainMessengerProxy, 155 L1StandardBridge: json.L1StandardBridgeProxy, 156 StateCommitmentChain: ethers.constants.AddressZero, 157 CanonicalTransactionChain: ethers.constants.AddressZero, 158 BondManager: ethers.constants.AddressZero, 159 OptimismPortal: json.OptimismPortalProxy, 160 L2OutputOracle: json.L2OutputOracleProxy, 161 OptimismPortal2: json.OptimismPortalProxy, 162 DisputeGameFactory: json.DisputeGameFactoryProxy, 163 }, 164 l2: DEFAULT_L2_CONTRACT_ADDRESSES, 165 } as OEContractsLike 166 } 167 168 console.log(`OptimismPortal: ${contractAddrs.l1.OptimismPortal}`) 169 const OptimismPortal = new hre.ethers.Contract( 170 contractAddrs.l1.OptimismPortal, 171 Artifact__OptimismPortal.abi, 172 signer 173 ) 174 175 console.log( 176 `L1CrossDomainMessenger: ${contractAddrs.l1.L1CrossDomainMessenger}` 177 ) 178 const L1CrossDomainMessenger = new hre.ethers.Contract( 179 contractAddrs.l1.L1CrossDomainMessenger, 180 Artifact__L1CrossDomainMessenger.abi, 181 signer 182 ) 183 184 console.log(`L1StandardBridge: ${contractAddrs.l1.L1StandardBridge}`) 185 const L1StandardBridge = new hre.ethers.Contract( 186 contractAddrs.l1.L1StandardBridge, 187 Artifact__L1StandardBridge.abi, 188 signer 189 ) 190 191 const L2OutputOracle = new hre.ethers.Contract( 192 contractAddrs.l1.L2OutputOracle, 193 Artifact__L2OutputOracle.abi, 194 signer 195 ) 196 197 const L2ToL1MessagePasser = new hre.ethers.Contract( 198 predeploys.L2ToL1MessagePasser, 199 Artifact__L2ToL1MessagePasser.abi 200 ) 201 202 const L2CrossDomainMessenger = new hre.ethers.Contract( 203 predeploys.L2CrossDomainMessenger, 204 Artifact__L2CrossDomainMessenger.abi 205 ) 206 207 const L2StandardBridge = new hre.ethers.Contract( 208 predeploys.L2StandardBridge, 209 Artifact__L2StandardBridge.abi 210 ) 211 212 const messenger = new CrossChainMessenger({ 213 l1SignerOrProvider: signer, 214 l2SignerOrProvider: l2Signer, 215 l1ChainId: await signer.getChainId(), 216 l2ChainId, 217 bedrock: true, 218 contracts: contractAddrs, 219 }) 220 221 const params = await OptimismPortal.params() 222 console.log('Intial OptimismPortal.params:') 223 console.log(params) 224 225 console.log('Deploying WETH9 to L1') 226 const WETH9 = await deployWETH9(hre, signer, true) 227 console.log(`Deployed to ${WETH9.address}`) 228 229 console.log('Creating L2 WETH9') 230 const OptimismMintableERC20 = await createOptimismMintableERC20( 231 hre, 232 WETH9, 233 l2Signer 234 ) 235 236 console.log(`Approving WETH9 for deposit`) 237 const approvalTx = await messenger.approveERC20( 238 WETH9.address, 239 OptimismMintableERC20.address, 240 hre.ethers.constants.MaxUint256 241 ) 242 await approvalTx.wait() 243 console.log('WETH9 approved') 244 245 console.log('Depositing WETH9 to L2') 246 const depositTx = await messenger.depositERC20( 247 WETH9.address, 248 OptimismMintableERC20.address, 249 utils.parseEther('1') 250 ) 251 await depositTx.wait() 252 console.log(`ERC20 deposited - ${depositTx.hash}`) 253 254 console.log('Checking to make sure deposit was successful') 255 // Deposit might get reorged, wait and also log for reorgs. 256 let prevBlockHash: string = '' 257 for (let i = 0; i < 12; i++) { 258 const messageReceipt = await signer.provider!.getTransactionReceipt( 259 depositTx.hash 260 ) 261 if (messageReceipt.status !== 1) { 262 console.log(`Deposit failed, retrying...`) 263 } 264 265 // Wait for stability, we want some amount of time after any reorg 266 if (prevBlockHash !== '' && messageReceipt.blockHash !== prevBlockHash) { 267 console.log( 268 `Block hash changed from ${prevBlockHash} to ${messageReceipt.blockHash}` 269 ) 270 i = 0 271 } else if (prevBlockHash !== '') { 272 console.log(`No reorg detected: ${i}`) 273 } 274 275 prevBlockHash = messageReceipt.blockHash 276 await sleep(1000) 277 } 278 console.log(`Deposit confirmed`) 279 280 const l2Balance = await OptimismMintableERC20.balanceOf(address) 281 if (l2Balance.lt(utils.parseEther('1'))) { 282 throw new Error( 283 `bad deposit. recipient balance on L2: ${utils.formatEther(l2Balance)}` 284 ) 285 } 286 console.log(`Deposit success`) 287 288 console.log('Starting withdrawal') 289 const preBalance = await WETH9.balanceOf(signer.address) 290 const withdraw = await messenger.withdrawERC20( 291 WETH9.address, 292 OptimismMintableERC20.address, 293 utils.parseEther('1') 294 ) 295 const withdrawalReceipt = await withdraw.wait() 296 for (const log of withdrawalReceipt.logs) { 297 switch (log.address) { 298 case L2ToL1MessagePasser.address: { 299 const parsed = L2ToL1MessagePasser.interface.parseLog(log) 300 console.log(`Log ${parsed.name} from ${log.address}`) 301 console.log(parsed.args) 302 console.log() 303 break 304 } 305 case L2StandardBridge.address: { 306 const parsed = L2StandardBridge.interface.parseLog(log) 307 console.log(`Log ${parsed.name} from ${log.address}`) 308 console.log(parsed.args) 309 console.log() 310 break 311 } 312 case L2CrossDomainMessenger.address: { 313 const parsed = L2CrossDomainMessenger.interface.parseLog(log) 314 console.log(`Log ${parsed.name} from ${log.address}`) 315 console.log(parsed.args) 316 console.log() 317 break 318 } 319 default: { 320 console.log(`Unknown log from ${log.address} - ${log.topics[0]}`) 321 } 322 } 323 } 324 325 setInterval(async () => { 326 const currentStatus = await messenger.getMessageStatus(withdraw) 327 console.log(`Message status: ${MessageStatus[currentStatus]}`) 328 const latest = await L2OutputOracle.latestBlockNumber() 329 console.log( 330 `Latest L2OutputOracle commitment number: ${latest.toString()}` 331 ) 332 const tip = await signer.provider!.getBlockNumber() 333 console.log(`L1 chain tip: ${tip.toString()}`) 334 }, 3000) 335 336 const now = Math.floor(Date.now() / 1000) 337 338 console.log('Waiting for message to be able to be proved') 339 await messenger.waitForMessageStatus(withdraw, MessageStatus.READY_TO_PROVE) 340 341 console.log('Proving withdrawal...') 342 const prove = await messenger.proveMessage(withdraw) 343 const proveReceipt = await prove.wait() 344 console.log(proveReceipt) 345 if (proveReceipt.status !== 1) { 346 throw new Error('Prove withdrawal transaction reverted') 347 } 348 349 console.log('Waiting for message to be able to be relayed') 350 await messenger.waitForMessageStatus( 351 withdraw, 352 MessageStatus.READY_FOR_RELAY 353 ) 354 355 console.log('Finalizing withdrawal...') 356 // TODO: Update SDK to properly estimate gas 357 const finalize = await messenger.finalizeMessage(withdraw, { 358 overrides: { gasLimit: 500_000 }, 359 }) 360 const finalizeReceipt = await finalize.wait() 361 console.log('finalizeReceipt:', finalizeReceipt) 362 console.log(`Took ${Math.floor(Date.now() / 1000) - now} seconds`) 363 364 for (const log of finalizeReceipt.logs) { 365 switch (log.address) { 366 case OptimismPortal.address: { 367 const parsed = OptimismPortal.interface.parseLog(log) 368 console.log(`Log ${parsed.name} from OptimismPortal (${log.address})`) 369 console.log(parsed.args) 370 console.log() 371 break 372 } 373 case L1CrossDomainMessenger.address: { 374 const parsed = L1CrossDomainMessenger.interface.parseLog(log) 375 console.log( 376 `Log ${parsed.name} from L1CrossDomainMessenger (${log.address})` 377 ) 378 console.log(parsed.args) 379 console.log() 380 break 381 } 382 case L1StandardBridge.address: { 383 const parsed = L1StandardBridge.interface.parseLog(log) 384 console.log( 385 `Log ${parsed.name} from L1StandardBridge (${log.address})` 386 ) 387 console.log(parsed.args) 388 console.log() 389 break 390 } 391 case WETH9.address: { 392 const parsed = WETH9.interface.parseLog(log) 393 console.log(`Log ${parsed.name} from WETH9 (${log.address})`) 394 console.log(parsed.args) 395 console.log() 396 break 397 } 398 default: 399 console.log( 400 `Unknown log emitted from ${log.address} - ${log.topics[0]}` 401 ) 402 } 403 } 404 405 const postBalance = await WETH9.balanceOf(signer.address) 406 407 const expectedBalance = preBalance.add(utils.parseEther('1')) 408 if (!expectedBalance.eq(postBalance)) { 409 throw new Error( 410 `Balance mismatch, expected: ${expectedBalance}, actual: ${postBalance}` 411 ) 412 } 413 console.log('Withdrawal success') 414 })