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

Support ERC-20 deposits in x/rollup #223

Merged
merged 4 commits into from
Sep 20, 2024
Merged
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
25 changes: 25 additions & 0 deletions bindings/erc20_deposit_args.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package bindings

import (
"math/big"

"github.com/ethereum/go-ethereum/common"
)

type RelayMessageArgs struct {
Nonce *big.Int
Sender common.Address
Target common.Address
Value *big.Int
MinGasLimit *big.Int
Message []byte
}

type FinalizeBridgeERC20Args struct {
RemoteToken common.Address
LocalToken common.Address
From common.Address
To common.Address
Amount *big.Int
ExtraData []byte
}
18 changes: 14 additions & 4 deletions e2e/stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
dbm "github.com/cosmos/cosmos-db"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/types/module/testutil"
opbindings "github.com/ethereum-optimism/optimism/op-bindings/bindings"
opgenesis "github.com/ethereum-optimism/optimism/op-chain-ops/genesis"
ope2econfig "github.com/ethereum-optimism/optimism/op-e2e/config"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils"
Expand All @@ -42,9 +43,11 @@ type EventListener interface {

type StackConfig struct {
Ctx context.Context
User *ecdsa.PrivateKey
Users []*ecdsa.PrivateKey
L1Client *L1Client
L1Portal *bindings.OptimismPortal
L1Deployments *opgenesis.L1Deployments
OptimismPortal *bindings.OptimismPortal
L1StandardBridge *opbindings.L1StandardBridge
L2OutputOracleCaller *bindings.L2OutputOracleCaller
L2Client *bftclient.HTTP
MonomerClient *MonomerClient
Expand Down Expand Up @@ -159,6 +162,11 @@ func (s *stack) run(ctx context.Context, env *environment.Env) (*StackConfig, er
return nil, fmt.Errorf("new optimism portal: %v", err)
}

l1StandardBridge, err := opbindings.NewL1StandardBridge(ope2econfig.L1Deployments.L1StandardBridgeProxy, l1Client)
if err != nil {
return nil, fmt.Errorf("new l1 standard bridge: %v", err)
}

l2OutputOracleCaller, err := bindings.NewL2OutputOracleCaller(ope2econfig.L1Deployments.L2OutputOracleProxy, l1Client)
if err != nil {
return nil, fmt.Errorf("new l2 output oracle caller: %v", err)
Expand Down Expand Up @@ -225,11 +233,13 @@ func (s *stack) run(ctx context.Context, env *environment.Env) (*StackConfig, er
return &StackConfig{
Ctx: ctx,
L1Client: l1Client,
L1Portal: opPortal,
L1Deployments: ope2econfig.L1Deployments,
OptimismPortal: opPortal,
L1StandardBridge: l1StandardBridge,
L2OutputOracleCaller: l2OutputOracleCaller,
L2Client: l2Client,
MonomerClient: monomerClient,
User: secrets.Alice,
Users: []*ecdsa.PrivateKey{secrets.Alice, secrets.Bob},
RollupConfig: rollupConfig,
WaitL1: func(numBlocks int) error {
return wait(numBlocks, 1)
Expand Down
147 changes: 127 additions & 20 deletions e2e/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import (
"github.com/cometbft/cometbft/config"
cometcore "github.com/cometbft/cometbft/rpc/core/types"
bfttypes "github.com/cometbft/cometbft/types"
opbindings "github.com/ethereum-optimism/optimism/op-bindings/bindings"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/receipts"
"github.com/ethereum-optimism/optimism/op-e2e/e2eutils/wait"
"github.com/ethereum-optimism/optimism/op-node/bindings"
"github.com/ethereum-optimism/optimism/op-node/rollup"
"github.com/ethereum-optimism/optimism/op-node/rollup/derive"
Expand All @@ -40,7 +43,6 @@ import (

const (
artifactsDirectoryName = "artifacts"
oneEth = 1e18
)

func openLogFile(t *testing.T, env *environment.Env, name string) *os.File {
Expand All @@ -56,8 +58,12 @@ var e2eTests = []struct {
run func(t *testing.T, stack *e2e.StackConfig)
}{
{
name: "L1 Deposits and L2 Withdrawals",
run: rollupFlow,
name: "ETH L1 Deposits and L2 Withdrawals",
run: ethRollupFlow,
},
{
name: "ERC-20 L1 Deposits",
run: erc20RollupFlow,
},
{
name: "CometBFT Txs",
Expand Down Expand Up @@ -230,7 +236,7 @@ func cometBFTtx(t *testing.T, stack *e2e.StackConfig) {
require.Len(t, txBlock.Transactions(), 2) // 1 deposit tx + 1 cometbft tx
}

func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) {
l1Client := stack.L1Client
monomerClient := stack.MonomerClient

Expand All @@ -242,24 +248,24 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
require.NoError(t, err, "chain id")

// instantiate L1 user, tx signer.
userPrivKey := stack.User
userPrivKey := stack.Users[0]
userAddress := crypto.PubkeyToAddress(userPrivKey.PublicKey)
l1signer := types.NewEIP155Signer(l1ChainID)

//////////////////////
////// DEPOSITS //////
//////////////////////

l2GasLimit := l2blockGasLimit / 10
l1GasLimit := l2GasLimit * 2 // must be higher than l2Gaslimit, because of l1 gas burn (cross-chain gas accounting)

//////////////////////////
////// ETH DEPOSITS //////
//////////////////////////

// get the user's balance before the deposit has been processed
balanceBeforeDeposit, err := l1Client.BalanceAt(stack.Ctx, userAddress, nil)
require.NoError(t, err)

// send user Deposit Tx
depositAmount := big.NewInt(oneEth)
depositTx, err := stack.L1Portal.DepositTransaction(
depositAmount := big.NewInt(params.Ether)
depositTx, err := stack.OptimismPortal.DepositTransaction(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, depositAmount),
userAddress,
big.NewInt(0),
Expand All @@ -281,7 +287,7 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
require.NotNil(t, receipt, "deposit tx receipt")
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "deposit tx reverted")

depositLogs, err := stack.L1Portal.FilterTransactionDeposited(
depositLogs, err := stack.OptimismPortal.FilterTransactionDeposited(
&bind.FilterOpts{
Start: 0,
End: nil,
Expand Down Expand Up @@ -316,9 +322,9 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {

t.Log("Monomer can ingest user deposit txs from L1 and mint ETH on L2")

/////////////////////////
////// WITHDRAWALS //////
/////////////////////////
/////////////////////////////
////// ETH WITHDRAWALS //////
/////////////////////////////

// create a withdrawal tx to withdraw the deposited amount from L2 back to L1
withdrawalTx := e2e.NewWithdrawalTx(0, userAddress, userAddress, depositAmount, new(big.Int).SetUint64(params.TxGas))
Expand Down Expand Up @@ -351,7 +357,7 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
require.NoError(t, err)

// send a withdrawal proving tx to prove the withdrawal on L1
proveWithdrawalTx, err := stack.L1Portal.ProveWithdrawalTransaction(
proveWithdrawalTx, err := stack.OptimismPortal.ProveWithdrawalTransaction(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, nil),
withdrawalTx.WithdrawalTransaction(),
provenWithdrawalParams.L2OutputIndex,
Expand All @@ -371,7 +377,7 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {

withdrawalTxHash, err := withdrawalTx.Hash()
require.NoError(t, err)
proveWithdrawalLogs, err := stack.L1Portal.FilterWithdrawalProven(
proveWithdrawalLogs, err := stack.OptimismPortal.FilterWithdrawalProven(
&bind.FilterOpts{
Start: 0,
End: nil,
Expand All @@ -395,7 +401,7 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
require.NoError(t, err)

// send a withdrawal finalizing tx to finalize the withdrawal on L1
finalizeWithdrawalTx, err := stack.L1Portal.FinalizeWithdrawalTransaction(
finalizeWithdrawalTx, err := stack.OptimismPortal.FinalizeWithdrawalTransaction(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, nil),
withdrawalTx.WithdrawalTransaction(),
)
Expand All @@ -410,7 +416,7 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
require.NotNil(t, receipt, "finalize withdrawal tx receipt")
require.Equal(t, types.ReceiptStatusSuccessful, receipt.Status, "finalize withdrawal tx failed")

finalizeWithdrawalLogs, err := stack.L1Portal.FilterWithdrawalFinalized(
finalizeWithdrawalLogs, err := stack.OptimismPortal.FilterWithdrawalFinalized(
&bind.FilterOpts{
Start: 0,
End: nil,
Expand Down Expand Up @@ -441,6 +447,94 @@ func rollupFlow(t *testing.T, stack *e2e.StackConfig) {
t.Log("Monomer can initiate withdrawals on L2 and can generate proofs for verifying the withdrawal on L1")
}

func erc20RollupFlow(t *testing.T, stack *e2e.StackConfig) {
l1Client := stack.L1Client
monomerClient := stack.MonomerClient

b, err := monomerClient.BlockByNumber(stack.Ctx, nil)
require.NoError(t, err, "monomer block by number")
l2blockGasLimit := b.GasLimit()

l1ChainID, err := l1Client.ChainID(stack.Ctx)
require.NoError(t, err, "chain id")

// instantiate L1 user, tx signer.
userPrivKey := stack.Users[1]
userAddress := crypto.PubkeyToAddress(userPrivKey.PublicKey)
l1signer := types.NewEIP155Signer(l1ChainID)

l2GasLimit := l2blockGasLimit / 10
l1GasLimit := l2GasLimit * 2 // must be higher than l2Gaslimit, because of l1 gas burn (cross-chain gas accounting)

/////////////////////////////
////// ERC-20 DEPOSITS //////
/////////////////////////////

// deploy the WETH9 ERC-20 contract on L1
weth9Address, tx, WETH9, err := opbindings.DeployWETH9(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, nil),
l1Client,
)
require.NoError(t, err)
// TODO: we should use wait.ForReceiptOK for all L1 tx receipts in the e2e tests
_, err = wait.ForReceiptOK(stack.Ctx, l1Client.Client, tx.Hash())
require.NoError(t, err, "waiting for deposit tx on L1")

// mint some WETH to the user
wethL1Amount := big.NewInt(params.Ether)
tx, err = WETH9.Deposit(createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, wethL1Amount))
require.NoError(t, err)
_, err = wait.ForReceiptOK(stack.Ctx, l1Client.Client, tx.Hash())
require.NoError(t, err)
wethBalance, err := WETH9.BalanceOf(&bind.CallOpts{}, userAddress)
require.NoError(t, err)
require.Equal(t, wethL1Amount, wethBalance)

// approve WETH9 transfer with the L1StandardBridge address
tx, err = WETH9.Approve(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, nil),
stack.L1Deployments.L1StandardBridgeProxy,
wethL1Amount,
)
require.NoError(t, err)
_, err = wait.ForReceiptOK(stack.Ctx, l1Client.Client, tx.Hash())
require.NoError(t, err)

// bridge the WETH9
wethL2Amount := big.NewInt(100)
tx, err = stack.L1StandardBridge.BridgeERC20(
createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, nil),
weth9Address,
weth9Address,
wethL2Amount,
100_000,
[]byte{},
)
require.NoError(t, err)
depositReceipt, err := wait.ForReceiptOK(stack.Ctx, l1Client.Client, tx.Hash())
require.NoError(t, err)

// check that the deposit tx went through the OptimismPortal successfully
_, err = receipts.FindLog(depositReceipt.Logs, stack.OptimismPortal.ParseTransactionDeposited)
require.NoError(t, err, "should emit deposit event")

// assert the user's bridged WETH is no longer on L1
wethBalance, err = WETH9.BalanceOf(&bind.CallOpts{}, userAddress)
require.NoError(t, err)
require.Equal(t, new(big.Int).Sub(wethL1Amount, wethL2Amount), wethBalance)

// wait for tx to be processed
// 1 L1 block to process the tx on L1 +
// 1 L2 block to process the tx on L2
require.NoError(t, stack.WaitL1(1))
require.NoError(t, stack.WaitL2(1))

// assert the user's bridged WETH is on L2
requireERC20IsMinted(t, stack, utils.EvmToCosmosAddress(userAddress).String(), weth9Address.String(), hexutil.Encode(wethL2Amount.Bytes()))

t.Log("Monomer can ingest ERC-20 deposit txs from L1 and mint ERC-20 tokens on L2")
}

func requireEthIsMinted(t *testing.T, stack *e2e.StackConfig, userAddress, valueHex string) {
query := fmt.Sprintf(
"%s.%s='%s' AND %s.%s='%s' AND %s.%s='%s'",
Expand All @@ -465,6 +559,19 @@ func requireEthIsBurned(t *testing.T, stack *e2e.StackConfig, userAddress, value
require.NotEmpty(t, result.Txs, "burn_eth event not found")
}

func requireERC20IsMinted(t *testing.T, stack *e2e.StackConfig, userAddress, tokenAddress, valueHex string) {
query := fmt.Sprintf(
"%s.%s='%s' AND %s.%s='%s' AND %s.%s='%s' AND %s.%s='%s'",
rolluptypes.EventTypeMintERC20, rolluptypes.AttributeKeyL1DepositTxType, rolluptypes.L1UserDepositTxType,
rolluptypes.EventTypeMintERC20, rolluptypes.AttributeKeyToCosmosAddress, userAddress,
rolluptypes.EventTypeMintERC20, rolluptypes.AttributeKeyERC20Address, tokenAddress,
rolluptypes.EventTypeMintERC20, rolluptypes.AttributeKeyValue, valueHex,
)
result := l2TxSearch(t, stack, query)

require.NotEmpty(t, result.Txs, "mint_erc20 event not found")
}

func l2TxSearch(t *testing.T, stack *e2e.StackConfig, query string) *cometcore.ResultTxSearch {
page := 1
perPage := 100
Expand Down Expand Up @@ -508,7 +615,7 @@ func createL1TransactOpts(
}

func getCurrentUserNonce(t *testing.T, stack *e2e.StackConfig, userAddress common.Address) *big.Int {
nonce, err := stack.L1Client.NonceAt(stack.Ctx, userAddress, nil)
nonce, err := stack.L1Client.PendingNonceAt(stack.Ctx, userAddress)
require.NoError(t, err)
return new(big.Int).SetUint64(nonce)
}
Expand Down
Loading
Loading