-
Notifications
You must be signed in to change notification settings - Fork 52
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Automate withdrawing ETH from OpStack chains (#1866)
* feat: Automate withdrawing ETH from OpStack chains - Adds a new script to withdraw ETH via the OVM standard L2 bridge to the signer's address on L1 - Adds a feature to the OP stack finalizer that lets the user specify addresses that they want to finalize OVM withdrawals for, in addition to any existing SpokePool withdrawals. This pairs with the above script to allow us to one-step manually move ETH from OP stack chains back to the same EOA on Ethereum - To use this new feature, we need to add new chains to the finalizer config (i.e. 7777777 and 1135) and also set `WITHDRAWAL_TO_ADDRESSES=[x]` where `x` are addresses that we plan to execute manual withdrawals from * Update opStack.ts * Update OpStackStandardBridgeL2.json * Update opStack.ts * Update Constants.ts * Update scripts/withdrawFromOpStack.ts Co-authored-by: Paul <[email protected]> * Add safety checks * Update opStack.ts * Update opStack.ts --------- Co-authored-by: Paul <[email protected]>
- Loading branch information
1 parent
3de3d54
commit 119ca6b
Showing
6 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
// Submits a bridge from OpStack L2 to L1. | ||
// For now, this script only supports ETH withdrawals. | ||
|
||
import { | ||
ethers, | ||
retrieveSignerFromCLIArgs, | ||
getProvider, | ||
ERC20, | ||
WETH9, | ||
TOKEN_SYMBOLS_MAP, | ||
assert, | ||
getL1TokenInfo, | ||
Contract, | ||
fromWei, | ||
blockExplorerLink, | ||
CHAIN_IDs, | ||
} from "../src/utils"; | ||
import { CONTRACT_ADDRESSES } from "../src/common"; | ||
import { askYesNoQuestion, getOvmSpokePoolContract } from "./utils"; | ||
|
||
import minimist from "minimist"; | ||
|
||
const cliArgs = ["amount", "chainId"]; | ||
const args = minimist(process.argv.slice(2), { | ||
string: cliArgs, | ||
}); | ||
|
||
// Example run: | ||
// ts-node ./scripts/withdrawFromOpStack.ts | ||
// \ --amount 3000000000000000000 | ||
// \ --chainId 1135 | ||
// \ --wallet gckms | ||
// \ --keys bot1 | ||
|
||
export async function run(): Promise<void> { | ||
assert( | ||
cliArgs.every((cliArg) => Object.keys(args).includes(cliArg)), | ||
`Missing cliArg, expected: ${cliArgs}` | ||
); | ||
const baseSigner = await retrieveSignerFromCLIArgs(); | ||
const signerAddr = await baseSigner.getAddress(); | ||
const chainId = parseInt(args.chainId); | ||
const connectedSigner = baseSigner.connect(await getProvider(chainId)); | ||
const l2Token = TOKEN_SYMBOLS_MAP.WETH?.addresses[chainId]; | ||
assert(l2Token, `WETH not found on chain ${chainId} in TOKEN_SYMBOLS_MAP`); | ||
const l1TokenInfo = getL1TokenInfo(l2Token, chainId); | ||
console.log("Fetched L1 token info:", l1TokenInfo); | ||
assert(l1TokenInfo.symbol === "ETH", "Only WETH withdrawals are supported for now."); | ||
const amount = args.amount; | ||
const amountFromWei = ethers.utils.formatUnits(amount, l1TokenInfo.decimals); | ||
console.log(`Amount to bridge from chain ${chainId}: ${amountFromWei} ${l2Token}`); | ||
|
||
const erc20 = new Contract(l2Token, ERC20.abi, connectedSigner); | ||
const currentBalance = await erc20.balanceOf(signerAddr); | ||
const currentEthBalance = await connectedSigner.getBalance(); | ||
console.log( | ||
`Current WETH balance for account ${signerAddr}: ${fromWei(currentBalance, l1TokenInfo.decimals)} ${l2Token}` | ||
); | ||
console.log(`Current ETH balance for account ${signerAddr}: ${fromWei(currentEthBalance, l1TokenInfo.decimals)}`); | ||
|
||
// First offer user option to unwrap WETH into ETH. | ||
const weth = new Contract(l2Token, WETH9.abi, connectedSigner); | ||
if (await askYesNoQuestion(`\nUnwrap ${amount} of WETH @ ${weth.address}?`)) { | ||
const unwrap = await weth.withdraw(amount); | ||
console.log(`Submitted transaction: ${blockExplorerLink(unwrap.hash, chainId)}.`); | ||
const receipt = await unwrap.wait(); | ||
console.log("Unwrap complete...", receipt); | ||
} | ||
|
||
// Now, submit a withdrawal: | ||
const ovmStandardBridgeObj = CONTRACT_ADDRESSES[chainId].ovmStandardBridge; | ||
assert(CONTRACT_ADDRESSES[chainId].ovmStandardBridge, "ovmStandardBridge for chain not found in CONTRACT_ADDRESSES"); | ||
const ovmStandardBridge = new Contract(ovmStandardBridgeObj.address, ovmStandardBridgeObj.abi, connectedSigner); | ||
const bridgeETHToArgs = [ | ||
signerAddr, // to | ||
200_000, // minGasLimit | ||
"0x", // extraData | ||
{ value: amount }, // msg.value | ||
]; | ||
|
||
console.log( | ||
`Submitting bridgeETHTo on the OVM standard bridge @ ${ovmStandardBridge.address} with the following args: `, | ||
...bridgeETHToArgs | ||
); | ||
|
||
// Sanity check that the ovmStandardBridge contract is the one we expect by comparing its stored addresses | ||
// with the ones we have recorded. | ||
const spokePool = await getOvmSpokePoolContract(chainId, connectedSigner); | ||
const expectedL2Messenger = await spokePool.MESSENGER(); | ||
const l2Messenger = await ovmStandardBridge.MESSENGER(); | ||
assert( | ||
l2Messenger === expectedL2Messenger, | ||
`Unexpected L2 messenger address in ovmStandardBridge contract, expected: ${expectedL2Messenger}, got: ${l2Messenger}` | ||
); | ||
const l1StandardBridge = await ovmStandardBridge.l1TokenBridge(); | ||
const expectedL1StandardBridge = CONTRACT_ADDRESSES[CHAIN_IDs.MAINNET][`ovmStandardBridge_${chainId}`].address; | ||
assert( | ||
l1StandardBridge === expectedL1StandardBridge, | ||
`Unexpected L1 standard bridge address in ovmStandardBridge contract, expected: ${expectedL1StandardBridge}, got: ${l1StandardBridge}` | ||
); | ||
if (!(await askYesNoQuestion("\nDo you want to proceed?"))) { | ||
return; | ||
} | ||
const withdrawal = await ovmStandardBridge.bridgeETHTo(...bridgeETHToArgs); | ||
console.log(`Submitted withdrawal: ${blockExplorerLink(withdrawal.hash, chainId)}.`); | ||
const receipt = await withdrawal.wait(); | ||
console.log("Receipt", receipt); | ||
} | ||
|
||
if (require.main === module) { | ||
run() | ||
.then(async () => { | ||
// eslint-disable-next-line no-process-exit | ||
process.exit(0); | ||
}) | ||
.catch(async (error) => { | ||
console.error("Process exited with", error); | ||
// eslint-disable-next-line no-process-exit | ||
process.exit(1); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters