diff --git a/README.md b/README.md index 5e7627c..2f979fe 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # withdrawer -Golang utility for proving and finalizing ETH withdrawals from op-stack chains. +Golang utility for proving and finalizing ETH withdrawals from OP Stack chains. @@ -16,7 +16,7 @@ Golang utility for proving and finalizing ETH withdrawals from op-stack chains. [![Blog](https://img.shields.io/badge/blog-up-green)](https://base.mirror.xyz/) [![Docs](https://img.shields.io/badge/docs-up-green)](https://docs.base.org/) [![Discord](https://img.shields.io/discord/1067165013397213286?label=discord)](https://base.org/discord) -[![Twitter BuildOnBase](https://img.shields.io/twitter/follow/BuildOnBase?style=social)](https://twitter.com/BuildOnBase) +[![Twitter BuildOnBase](https://img.shields.io/twitter/follow/BuildOnBase?style=social)](https://x.com/BuildOnBase) @@ -25,7 +25,7 @@ Golang utility for proving and finalizing ETH withdrawals from op-stack chains. ## Installation -``` +```bash git clone https://github.com/base-org/withdrawer.git cd withdrawer go install . @@ -33,109 +33,107 @@ go install . ## Usage -> [!CAUTION] -> Do not send ERC-20 or other tokens to the L2StandardBridge, only native ETH is supported. +> **CAUTION:** +> Do not send ERC-20 or other tokens to the L2StandardBridge. Only native ETH is supported. ### Without Fault Proofs #### Step 1 -Initiate a withdrawal on L2 by sending ETH to the `L2StandardBridge` contract at `0x4200000000000000000000000000000000000010`, and note the tx hash. +Initiate a withdrawal on L2 by sending ETH to the `L2StandardBridge` contract at `0x4200000000000000000000000000000000000010`, and note the transaction hash. + Example on Base Sepolia: [0x5e47346867cf87d8e8c82cae1d30a94b8d5587dc9d354aef5c5a7b4c84ad9463](https://sepolia.basescan.org/tx/0x5e47346867cf87d8e8c82cae1d30a94b8d5587dc9d354aef5c5a7b4c84ad9463). -> [!NOTE] -> Users are required to wait for a period of seven days when moving assets out of Base mainnet into the Ethereum mainnet. This period of time is called the Challenge Period and serves to help secure the assets stored on Base mainnet. +> **NOTE:** +> Users are required to wait for a period of seven days when moving assets out of Base mainnet into the Ethereum mainnet. This period is called the Challenge Period and helps secure the assets stored on Base mainnet. #### Step 2 Prove your withdrawal: -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --private-key ``` or use a ledger: -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --ledger ``` Example output: -``` +```text Proved withdrawal for 0xc4055dcb2e4647c37166caba8c7392625c2b62f9117a8bc4d96270da24b38f13: 0x6b6d1cc45b6601a30646847f638847feb629221ee71bbe6a3de7e6d0fbfe8fad waiting for tx confirmation 0x6b6d1cc45b6601a30646847f638847feb629221ee71bbe6a3de7e6d0fbfe8fad confirmed ``` -_Note: this can be called from any L1 address, it does not have to be the same address that initiated the withdrawal on the L2._ +_Note: This can be called from any L1 address. It does not have to be the same address that initiated the withdrawal on the L2._ #### Step 3 After the finalization period, finalize your withdrawal (same command as above): -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --private-key ``` Example output: -``` +```text Completed withdrawal for 0xc4055dcb2e4647c37166caba8c7392625c2b62f9117a8bc4d96270da24b38f13: 0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea waiting for tx confirmation 0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea confirmed ``` -_Note: this can be called from any L1 address, it does not have to be the same address that initiated the withdrawal on the L2._ - ### With Fault Proofs -> [!NOTE] -> With the recent fault proofs upgrade for Base on Sepolia testnet, withdrawals are required to wait for a period of seven days. This mirrors the Challenge Period that exists for Base mainnet. Additionally, withdrawals are required to be finalized against dispute games that resolve in favor of the output root claim. If the dispute game is blacklisted, resolves against the output root claim (challenger wins), or the respected game type is changed, then the withdrawal will need to be re-proven. +> **NOTE:** +> With the recent fault proofs upgrade for Base on Sepolia testnet, withdrawals require a seven-day waiting period. This mirrors the Challenge Period on Base mainnet. Withdrawals must also be finalized against dispute games that resolve in favor of the output root claim. If the dispute game is blacklisted or resolves against the root claim, the withdrawal must be re-proven. #### Step 1 -Initiate a withdrawal on L2 by sending ETH to the `L2StandardBridge` contract at `0x4200000000000000000000000000000000000010`, and note the tx hash. +Initiate a withdrawal on L2 by sending ETH to the `L2StandardBridge` contract at `0x4200000000000000000000000000000000000010`, and note the transaction hash. + Example on Base Sepolia: [0x5e47346867cf87d8e8c82cae1d30a94b8d5587dc9d354aef5c5a7b4c84ad9463](https://sepolia.basescan.org/tx/0x5e47346867cf87d8e8c82cae1d30a94b8d5587dc9d354aef5c5a7b4c84ad9463). #### Step 2 Prove your withdrawal: -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --private-key --fault-proofs ``` or use a ledger: -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --ledger --fault-proofs ``` Example output: -``` +```text Proved withdrawal for 0xc4055dcb2e4647c37166caba8c7392625c2b62f9117a8bc4d96270da24b38f13: 0x6b6d1cc45b6601a30646847f638847feb629221ee71bbe6a3de7e6d0fbfe8fad waiting for tx confirmation 0x6b6d1cc45b6601a30646847f638847feb629221ee71bbe6a3de7e6d0fbfe8fad confirmed ``` -_Note: this can be called from any L1 address, it does not have to be the same address that initiated the withdrawal on the L2._ - #### Step 3 -> [!IMPORTANT] -> Unlike the non fault proof withdrawal flow, you MUST use the same address that proved the withdrawal to finalize the withdrawal. +> **IMPORTANT:** +> Unlike non-fault proof withdrawals, you MUST use the same address that proved the withdrawal to finalize it. -After the dispute game has resolved in favor of the root claim AND the finalization period has elapsed, finalize your withdrawal (same command as above): +After the dispute game resolves in favor of the root claim and the finalization period elapses, finalize your withdrawal: -``` +```bash withdrawer --network base-mainnet --withdrawal --rpc --private-key --fault-proofs ``` Example output: -``` +```text Completed withdrawal for 0xc4055dcb2e4647c37166caba8c7392625c2b62f9117a8bc4d96270da24b38f13: 0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea waiting for tx confirmation 0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea confirmed @@ -143,30 +141,29 @@ waiting for tx confirmation ## Flags -``` +```bash Usage of withdrawer: -rpc string - Ethereum L1 RPC url + Ethereum L1 RPC URL -network string - op-stack network to withdraw.go from (one of: base-mainnet, base-sepolia, op-mainnet, op-sepolia) (default "base-mainnet") + OP Stack network to withdraw from (e.g., base-mainnet, base-sepolia, op-mainnet, op-sepolia) (default: "base-mainnet") -withdrawal string TX hash of the L2 withdrawal transaction -fault-proofs - Use fault proofs withdrawal flow (only for networks that support fault proofs) + Use fault proof withdrawal flow (only for networks that support fault proofs) -private-key string - Private key to use for signing transactions + Private key for signing transactions -mnemonic string - Mnemonic to use for signing transactions + Mnemonic for signing transactions -ledger - Use ledger device for signing transactions + Use a Ledger device for signing transactions -hd-path string - Hierarchical deterministic derivation path for mnemonic or ledger (default "m/44'/60'/0'/0/0") + HD derivation path for mnemonic or Ledger (default: "m/44'/60'/0'/0/0") -l2-rpc string - Custom network L2 RPC url + Custom L2 RPC URL -l2oo-address string - Custom network L2OutputOracle address + Custom L2OutputOracle address -portal-address string - Custom network OptimismPortal address + Custom OptimismPortal address -dfg-address string - Custom network DisputeGameFactory address (only for networks that support fault proofs) -``` + Custom DisputeGameFactory address (only for networks that support fault proofs) diff --git a/withdraw/withdraw.go b/withdraw/withdraw.go index f3da435..7c84a0d 100644 --- a/withdraw/withdraw.go +++ b/withdraw/withdraw.go @@ -16,6 +16,8 @@ import ( "github.com/ethereum/go-ethereum/rpc" ) +// Withdrawer encapsulates the logic for managing and proving withdrawals +// between L2 and L1 Ethereum networks. type Withdrawer struct { Ctx context.Context L1Client *ethclient.Client @@ -26,8 +28,8 @@ type Withdrawer struct { Opts *bind.TransactOpts } +// CheckIfProvable ensures that the withdrawal is ready for proof generation. func (w *Withdrawer) CheckIfProvable() error { - // check to make sure it is possible to prove the provided withdrawal submissionInterval, err := w.Oracle.SUBMISSIONINTERVAL(&bind.CallOpts{}) if err != nil { return fmt.Errorf("error querying output proposal submission interval: %w", err) @@ -49,12 +51,16 @@ func (w *Withdrawer) CheckIfProvable() error { } if l2OutputBlock.Uint64() < l2WithdrawalBlock.Uint64() { - return fmt.Errorf("the latest L2 output is %d and is not past L2 block %d that includes the withdrawal, no withdrawal can be proved yet - please wait for the next proposal submission, which happens every %v", - l2OutputBlock.Uint64(), l2WithdrawalBlock.Uint64(), time.Duration(submissionInterval.Int64()*l2BlockTime.Int64())*time.Second) + return fmt.Errorf( + "the latest L2 output is %d and is not past L2 block %d that includes the withdrawal, no withdrawal can be proved yet - please wait for the next proposal submission, which happens every %v", + l2OutputBlock.Uint64(), l2WithdrawalBlock.Uint64(), + time.Duration(submissionInterval.Int64()*l2BlockTime.Int64())*time.Second, + ) } return nil } +// GetProvenWithdrawalTime retrieves the timestamp when the withdrawal was proven. func (w *Withdrawer) GetProvenWithdrawalTime() (uint64, error) { l2 := ethclient.NewClient(w.L2Client) receipt, err := l2.TransactionReceipt(w.Ctx, w.L2TxHash) @@ -80,6 +86,7 @@ func (w *Withdrawer) GetProvenWithdrawalTime() (uint64, error) { return provenWithdrawal.Timestamp.Uint64(), nil } +// ProveWithdrawal generates and submits a proof for the withdrawal transaction. func (w *Withdrawer) ProveWithdrawal() error { l2 := ethclient.NewClient(w.L2Client) l2g := gethclient.New(w.L2Client) @@ -89,17 +96,16 @@ func (w *Withdrawer) ProveWithdrawal() error { return err } - // We generate a proof for the latest L2 output, which shouldn't require archive-node data if it's recent enough. header, err := l2.HeaderByNumber(w.Ctx, l2OutputBlock) if err != nil { return err } + params, err := withdrawals.ProveWithdrawalParameters(w.Ctx, l2g, l2, l2, w.L2TxHash, header, &w.Oracle.L2OutputOracleCaller) if err != nil { return err } - // Create the prove tx tx, err := w.Portal.ProveWithdrawalTransaction( w.Opts, bindings.TypesWithdrawalTransaction{ @@ -120,21 +126,21 @@ func (w *Withdrawer) ProveWithdrawal() error { fmt.Printf("Proved withdrawal for %s: %s\n", w.L2TxHash.String(), tx.Hash().String()) - // Wait 5 mins max for confirmation ctxWithTimeout, cancel := context.WithTimeout(w.Ctx, 5*time.Minute) defer cancel() return waitForConfirmation(ctxWithTimeout, w.L1Client, tx.Hash()) } +// IsProofFinalized checks whether the withdrawal proof has been finalized on L1. func (w *Withdrawer) IsProofFinalized() (bool, error) { return w.Portal.FinalizedWithdrawals(&bind.CallOpts{}, w.L2TxHash) } +// FinalizeWithdrawal completes the withdrawal process on L1. func (w *Withdrawer) FinalizeWithdrawal() error { l2 := ethclient.NewClient(w.L2Client) l2g := gethclient.New(w.L2Client) - // Figure out when our withdrawal was included receipt, err := l2.TransactionReceipt(w.Ctx, w.L2TxHash) if err != nil { return fmt.Errorf("cannot get receipt for withdrawal tx %s: %v", w.L2TxHash, err) @@ -148,7 +154,6 @@ func (w *Withdrawer) FinalizeWithdrawal() error { return fmt.Errorf("error getting header by number for block %s: %v", receipt.BlockNumber, err) } - // Figure out what the Output oracle on L1 has seen so far l2OutputBlockNr, err := w.Oracle.LatestBlockNumber(&bind.CallOpts{}) if err != nil { return err @@ -159,9 +164,11 @@ func (w *Withdrawer) FinalizeWithdrawal() error { return fmt.Errorf("error getting header by number for latest block %s: %v", l2OutputBlockNr, err) } - // Check if the L2 output is even old enough to include the withdrawal if l2OutputBlock.Number.Uint64() < l2WithdrawalBlock.Number.Uint64() { - return fmt.Errorf("the latest L2 output is %d and is not past L2 block %d that includes the withdrawal yet, no withdrawal can be completed yet", l2OutputBlock.Number.Uint64(), l2WithdrawalBlock.Number.Uint64()) + return fmt.Errorf( + "the latest L2 output is %d and is not past L2 block %d that includes the withdrawal yet, no withdrawal can be completed yet", + l2OutputBlock.Number.Uint64(), l2WithdrawalBlock.Number.Uint64(), + ) } l1Head, err := w.L1Client.HeaderByNumber(w.Ctx, nil) @@ -169,20 +176,20 @@ func (w *Withdrawer) FinalizeWithdrawal() error { return err } - // Check if the withdrawal may be completed yet finalizationPeriod, err := w.Oracle.FINALIZATIONPERIODSECONDS(&bind.CallOpts{}) if err != nil { return err } if l2WithdrawalBlock.Time+finalizationPeriod.Uint64() >= l1Head.Time { - return fmt.Errorf("withdrawal tx %s was included in L2 block %d (time %d) but L1 only knows of L2 proposal %d (time %d) at head %d (time %d) which has not reached output confirmation yet (period is %d)", - w.L2TxHash, l2WithdrawalBlock.Number.Uint64(), l2WithdrawalBlock.Time, l2OutputBlock.Number.Uint64(), l2OutputBlock.Time, l1Head.Number.Uint64(), l1Head.Time, finalizationPeriod.Uint64()) + return fmt.Errorf( + "withdrawal tx %s was included in L2 block %d (time %d) but L1 only knows of L2 proposal %d (time %d) at head %d (time %d) which has not reached output confirmation yet (period is %d)", + w.L2TxHash, l2WithdrawalBlock.Number.Uint64(), l2WithdrawalBlock.Time, + l2OutputBlock.Number.Uint64(), l2OutputBlock.Time, l1Head.Number.Uint64(), + l1Head.Time, finalizationPeriod.Uint64(), + ) } - // We generate a proof for the latest L2 output, which shouldn't require archive-node data if it's recent enough. - // Note that for the `FinalizeWithdrawalTransaction` function, this proof isn't needed. We simply use some of the - // params for the `WithdrawalTransaction` type generated in the bindings. header, err := l2.HeaderByNumber(w.Ctx, l2OutputBlockNr) if err != nil { return err @@ -193,7 +200,6 @@ func (w *Withdrawer) FinalizeWithdrawal() error { return err } - // Create the withdrawal tx tx, err := w.Portal.FinalizeWithdrawalTransaction( w.Opts, bindings.TypesWithdrawalTransaction{ @@ -211,7 +217,6 @@ func (w *Withdrawer) FinalizeWithdrawal() error { fmt.Printf("Completed withdrawal for %s: %s\n", w.L2TxHash.String(), tx.Hash().String()) - // Wait 5 mins max for confirmation ctxWithTimeout, cancel := context.WithTimeout(w.Ctx, 5*time.Minute) defer cancel() return waitForConfirmation(ctxWithTimeout, w.L1Client, tx.Hash())