Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: update Twitter URL to x.com format #30

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 39 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.

<!-- Badge row 1 - status -->

Expand All @@ -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)

<!-- Badge row 3 - detailed status -->

Expand All @@ -25,148 +25,145 @@ 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 .
```

## 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 <withdrawal tx hash> --rpc <L1 RPC URL> --private-key <L1 private key>
```

or use a ledger:

```
```bash
withdrawer --network base-mainnet --withdrawal <withdrawal tx hash> --rpc <L1 RPC URL> --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 <withdrawal tx hash> --rpc <L1 RPC URL> --private-key <L1 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 <withdrawal tx hash> --rpc <L1 RPC URL> --private-key <L1 private key> --fault-proofs
```

or use a ledger:

```
```bash
withdrawer --network base-mainnet --withdrawal <withdrawal tx hash> --rpc <L1 RPC URL> --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 <withdrawal tx hash> --rpc <L1 RPC URL> --private-key <L1 private key> --fault-proofs
```

Example output:

```
```text
Completed withdrawal for 0xc4055dcb2e4647c37166caba8c7392625c2b62f9117a8bc4d96270da24b38f13: 0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea
waiting for tx confirmation
0x1c457f1992f48f1f959ceaee5b3c7e699a26f6f05d93997d49dafe703fd66dea confirmed
```

## 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)
41 changes: 23 additions & 18 deletions withdraw/withdraw.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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{
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -159,30 +164,32 @@ 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)
if err != nil {
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
Expand All @@ -193,7 +200,6 @@ func (w *Withdrawer) FinalizeWithdrawal() error {
return err
}

// Create the withdrawal tx
tx, err := w.Portal.FinalizeWithdrawalTransaction(
w.Opts,
bindings.TypesWithdrawalTransaction{
Expand All @@ -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())
Expand Down