From 72e601d8fbbf04dc5854466db11b31b77f3e086c Mon Sep 17 00:00:00 2001 From: Nate Beauregard <51711291+natebeauregard@users.noreply.github.com> Date: Mon, 25 Nov 2024 20:30:35 -0500 Subject: [PATCH] Enable ETH deposits through L1StandardBridge (#302) --- ...0_deposit_args.go => cross_domain_args.go} | 7 ++ e2e/stack_test.go | 91 +++++++++++------- testutils/utils.go | 32 ++++++- x/rollup/keeper/deposits.go | 96 +++++++++++++------ x/rollup/tests/integration/rollup_test.go | 32 ++++--- 5 files changed, 176 insertions(+), 82 deletions(-) rename bindings/{erc20_deposit_args.go => cross_domain_args.go} (77%) diff --git a/bindings/erc20_deposit_args.go b/bindings/cross_domain_args.go similarity index 77% rename from bindings/erc20_deposit_args.go rename to bindings/cross_domain_args.go index 1279445a..2e25e9be 100644 --- a/bindings/erc20_deposit_args.go +++ b/bindings/cross_domain_args.go @@ -15,6 +15,13 @@ type RelayMessageArgs struct { Message []byte } +type FinalizeBridgeETHArgs struct { + From common.Address + To common.Address + Amount *big.Int + ExtraData []byte +} + type FinalizeBridgeERC20Args struct { RemoteToken common.Address LocalToken common.Address diff --git a/e2e/stack_test.go b/e2e/stack_test.go index 5a20c74a..7ff244b8 100644 --- a/e2e/stack_test.go +++ b/e2e/stack_test.go @@ -206,40 +206,37 @@ func ethRollupFlow(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.OptimismPortal.FilterTransactionDeposited( - &bind.FilterOpts{ - Start: 0, - End: nil, - Context: stack.Ctx, - }, - []common.Address{userETHAddress}, - []common.Address{common.Address(userCosmosETHAddress)}, - []*big.Int{big.NewInt(0)}, - ) - require.NoError(t, err, "configuring 'TransactionDeposited' event listener") - require.True(t, depositLogs.Next(), "finding deposit event") - require.NoError(t, depositLogs.Close()) + requireExpectedBalanceAfterDeposit(t, stack, receipt, userETHAddress, balanceBeforeDeposit, depositAmount) + + userCosmosAddr, err := userCosmosETHAddress.Encode("e2e") + require.NoError(t, err) + requireEthIsMinted(t, stack, userCosmosAddr, hexutil.EncodeBig(depositAmount)) + + t.Log("Monomer can ingest OptimismPortal user deposit txs from L1 and mint ETH on L2") // get the user's balance after the deposit has been processed - balanceAfterDeposit, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) + balanceBeforeDeposit, err = stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) - //nolint:gocritic - // gasCost = gasUsed * gasPrice - gasCost := new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), receipt.EffectiveGasPrice) + bridgeDepositAmount := big.NewInt(params.Ether * 2) + bridgeDepositTx, err := stack.L1StandardBridge.DepositETHTo( + createL1TransactOpts(t, stack, userPrivKey, l1signer, bridgeDepositAmount), + common.Address(userCosmosETHAddress), + 100_000, // l2GasLimit, + []byte{}, // no data + ) + require.NoError(t, err) + receipt, err = wait.ForReceiptOK(stack.Ctx, l1Client.Client, bridgeDepositTx.Hash()) + require.NoError(t, err) - //nolint:gocritic - // expectedBalance = balanceBeforeDeposit - depositAmount - gasCost - expectedBalance := new(big.Int).Sub(new(big.Int).Sub(balanceBeforeDeposit, depositAmount), gasCost) + requireExpectedBalanceAfterDeposit(t, stack, receipt, userETHAddress, balanceBeforeDeposit, bridgeDepositAmount) - // check that the user's balance has been updated on L1 - require.Equal(t, expectedBalance, balanceAfterDeposit) + // wait for tx to be processed on L2 + require.NoError(t, stack.WaitL2(2)) - userCosmosAddr, err := userCosmosETHAddress.Encode("e2e") - require.NoError(t, err) - requireEthIsMinted(t, stack, userCosmosAddr, hexutil.EncodeBig(depositAmount)) + requireEthIsMinted(t, stack, userCosmosAddr, hexutil.EncodeBig(bridgeDepositAmount)) - t.Log("Monomer can ingest user deposit txs from L1 and mint ETH on L2") + t.Log("Monomer can ingest L1StandardBridge user deposit txs from L1 and mint ETH on L2") ///////////////////////////// ////// ETH WITHDRAWALS ////// @@ -373,13 +370,9 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { balanceAfterFinalization, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) - //nolint:gocritic - // gasCost = gasUsed * gasPrice - gasCost = new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), receipt.EffectiveGasPrice) - //nolint:gocritic // expectedBalance = balanceBeforeFinalization + withdrawalAmount - gasCost - expectedBalance = new(big.Int).Sub(new(big.Int).Add(balanceBeforeFinalization, withdrawalAmount), gasCost) + expectedBalance := new(big.Int).Sub(new(big.Int).Add(balanceBeforeFinalization, withdrawalAmount), getGasCost(receipt)) // check that the user's balance has been updated on L1 require.Equal(t, expectedBalance, balanceAfterFinalization) @@ -509,12 +502,15 @@ func erc20RollupFlow(t *testing.T, stack *e2e.StackConfig) { _, err = wait.ForReceiptOK(stack.Ctx, l1Client.Client, tx.Hash()) require.NoError(t, err) + userCosmosETHAddr := monomer.PubkeyToCosmosETHAddress(&userPrivKey.PublicKey) + // bridge the WETH9 wethL2Amount := big.NewInt(100) - tx, err = stack.L1StandardBridge.BridgeERC20( + tx, err = stack.L1StandardBridge.DepositERC20To( createL1TransactOpts(t, stack, userPrivKey, l1signer, nil), weth9Address, weth9Address, + common.Address(userCosmosETHAddr), wethL2Amount, 100_000, []byte{}, @@ -539,7 +535,7 @@ func erc20RollupFlow(t *testing.T, stack *e2e.StackConfig) { require.NoError(t, stack.WaitL2(1)) // assert the user's bridged WETH is on L2 - userAddr, err := monomer.CosmosETHAddress(userAddress).Encode("e2e") + userAddr, err := userCosmosETHAddr.Encode("e2e") require.NoError(t, err) requireERC20IsMinted(t, stack, userAddr, weth9Address.String(), hexutil.EncodeBig(wethL2Amount)) @@ -583,6 +579,35 @@ func requireERC20IsMinted(t *testing.T, stack *e2e.StackConfig, userAddress, tok require.NotEmpty(t, result.Txs, "mint_erc20 event not found") } +func requireExpectedBalanceAfterDeposit( + t *testing.T, + stack *e2e.StackConfig, + receipt *types.Receipt, + userETHAddress common.Address, + balanceBeforeDeposit, depositAmount *big.Int, +) { + // check that the deposit tx went through the OptimismPortal successfully + _, err := receipts.FindLog(receipt.Logs, stack.OptimismPortal.ParseTransactionDeposited) + require.NoError(t, err, "should emit deposit event") + + // get the user's balance after the deposit has been processed + balanceAfterDeposit, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) + require.NoError(t, err) + + //nolint:gocritic + // expectedBalance = balanceBeforeDeposit - bridgeDepositAmount - gasCost + expectedBalance := new(big.Int).Sub(new(big.Int).Sub(balanceBeforeDeposit, depositAmount), getGasCost(receipt)) + + // check that the user's balance has been updated on L1 + require.Equal(t, expectedBalance, balanceAfterDeposit) +} + +func getGasCost(receipt *types.Receipt) *big.Int { + //nolint:gocritic + // gasCost = gasUsed * gasPrice + return new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), receipt.EffectiveGasPrice) +} + func l2TxSearch(t *testing.T, stack *e2e.StackConfig, query string) *cometcore.ResultTxSearch { page := 1 perPage := 100 diff --git a/testutils/utils.go b/testutils/utils.go index 3bcdc74d..08bc211b 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -87,12 +87,26 @@ func GenerateEthTxs(t *testing.T) (*gethtypes.Transaction, *gethtypes.Transactio return l1InfoTx, depositTx, cosmosEthTx } -func GenerateERC20DepositTx(t *testing.T, tokenAddr, userAddr common.Address, amount *big.Int) *gethtypes.Transaction { - crossDomainMessengerABI, err := abi.JSON(strings.NewReader(opbindings.CrossDomainMessengerMetaData.ABI)) - require.NoError(t, err) +func GenerateEthBridgeDepositTx(t *testing.T, userAddr common.Address, amount *big.Int) *gethtypes.Transaction { standardBridgeABI, err := abi.JSON(strings.NewReader(opbindings.StandardBridgeMetaData.ABI)) require.NoError(t, err) + rng := rand.New(rand.NewSource(1234)) + + finalizeBridgeETHBz, err := standardBridgeABI.Pack( + "finalizeBridgeETH", + testutils.RandomAddress(rng), // from + userAddr, // to + amount, // amount + []byte{}, // extra data + ) + require.NoError(t, err) + return generateCrossDomainDepositTx(t, finalizeBridgeETHBz) +} + +func GenerateERC20DepositTx(t *testing.T, tokenAddr, userAddr common.Address, amount *big.Int) *gethtypes.Transaction { + standardBridgeABI, err := abi.JSON(strings.NewReader(opbindings.StandardBridgeMetaData.ABI)) + require.NoError(t, err) rng := rand.New(rand.NewSource(1234)) finalizeBridgeERC20Bz, err := standardBridgeABI.Pack( @@ -106,14 +120,22 @@ func GenerateERC20DepositTx(t *testing.T, tokenAddr, userAddr common.Address, am ) require.NoError(t, err) + return generateCrossDomainDepositTx(t, finalizeBridgeERC20Bz) +} + +func generateCrossDomainDepositTx(t *testing.T, crossDomainMessageBz []byte) *gethtypes.Transaction { + crossDomainMessengerABI, err := abi.JSON(strings.NewReader(opbindings.CrossDomainMessengerMetaData.ABI)) + require.NoError(t, err) + rng := rand.New(rand.NewSource(1234)) + relayMessageBz, err := crossDomainMessengerABI.Pack( "relayMessage", big.NewInt(0), // nonce testutils.RandomAddress(rng), // sender testutils.RandomAddress(rng), // target - amount, // value + big.NewInt(0), // value big.NewInt(0), // min gas limit - finalizeBridgeERC20Bz, // message + crossDomainMessageBz, // message ) require.NoError(t, err) diff --git a/x/rollup/keeper/deposits.go b/x/rollup/keeper/deposits.go index 01ee31fb..39343308 100644 --- a/x/rollup/keeper/deposits.go +++ b/x/rollup/keeper/deposits.go @@ -125,12 +125,12 @@ func (k *Keeper) processL1UserDepositTxs( // Check if the tx is a cross domain message from the aliased L1CrossDomainMessenger address if from == aliasedL1CrossDomainMessengerAddress && tx.Data() != nil { - erc20mintEvent, err := k.parseAndExecuteCrossDomainMessage(ctx, tx.Data()) + crossDomainMessageEvent, err := k.processCrossDomainMessage(ctx, tx.Data()) // TODO: Investigate when to return an error if a cross domain message can't be parsed or executed - look at OP Spec if err != nil { return nil, types.WrapError(types.ErrInvalidL1Txs, "failed to parse or execute cross domain message: %v", err) - } else { - mintEvents = append(mintEvents, *erc20mintEvent) + } else if crossDomainMessageEvent != nil { + mintEvents = append(mintEvents, *crossDomainMessageEvent) } } } @@ -138,10 +138,10 @@ func (k *Keeper) processL1UserDepositTxs( return mintEvents, nil } -// parseAndExecuteCrossDomainMessage parses the tx data of a cross domain message and applies state transitions for recognized messages. -// Currently, only finalizeBridgeERC20 messages from the L1StandardBridge are recognized for minting ERC-20 tokens on the Cosmos chain. -// If a message is not recognized, it returns nil and does not error. -func (k *Keeper) parseAndExecuteCrossDomainMessage(ctx sdk.Context, txData []byte) (*sdk.Event, error) { //nolint:gocritic // hugeParam +// processCrossDomainMessage parses the tx data of a cross domain message and applies state transitions for recognized messages. +// Currently, only finalizeBridgeETH and finalizeBridgeERC20 messages from the L1StandardBridge are recognized for minting tokens +// on the Cosmos chain. If a message is not recognized, it returns nil and does not error. +func (k *Keeper) processCrossDomainMessage(ctx sdk.Context, txData []byte) (*sdk.Event, error) { //nolint:gocritic // hugeParam crossDomainMessengerABI, err := abi.JSON(strings.NewReader(opbindings.CrossDomainMessengerMetaData.ABI)) if err != nil { return nil, fmt.Errorf("failed to parse CrossDomainMessenger ABI: %v", err) @@ -156,37 +156,71 @@ func (k *Keeper) parseAndExecuteCrossDomainMessage(ctx sdk.Context, txData []byt return nil, fmt.Errorf("failed to unpack tx data into relayMessage interface: %v", err) } - // Check if the relayed message is a finalizeBridgeERC20 message from the L1StandardBridge - if bytes.Equal(relayMessage.Message[:4], standardBridgeABI.Methods["finalizeBridgeERC20"].ID) { - var finalizeBridgeERC20 bindings.FinalizeBridgeERC20Args - if err = unpackInputsIntoInterface( - &standardBridgeABI, - "finalizeBridgeERC20", - relayMessage.Message, - &finalizeBridgeERC20, - ); err != nil { - return nil, fmt.Errorf("failed to unpack relay message into finalizeBridgeERC20 interface: %v", err) - } - - toAddr, err := monomer.CosmosETHAddress(finalizeBridgeERC20.To).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) + // Check if the relayed message is a supported message from the L1StandardBridge + l1StandardBridgeMethodID := relayMessage.Message[:4] + if bytes.Equal(l1StandardBridgeMethodID, standardBridgeABI.Methods["finalizeBridgeETH"].ID) { + mintEvent, err := k.processFinalizeBridgeETH(ctx, &standardBridgeABI, &relayMessage) if err != nil { - return nil, fmt.Errorf("evm to cosmos address: %v", err) + return nil, fmt.Errorf("failed to process finalizeBridgeETH method: %v", err) } - // Mint the ERC-20 token to the specified Cosmos address - mintEvent, err := k.mintERC20( - ctx, - toAddr, - finalizeBridgeERC20.RemoteToken.String(), - sdkmath.NewIntFromBigInt(finalizeBridgeERC20.Amount), - ) + return mintEvent, nil + } else if bytes.Equal(l1StandardBridgeMethodID, standardBridgeABI.Methods["finalizeBridgeERC20"].ID) { + mintEvent, err := k.processFinalizeBridgeERC20(ctx, &standardBridgeABI, &relayMessage) if err != nil { - return nil, fmt.Errorf("failed to mint ERC-20 token: %v", err) + return nil, fmt.Errorf("failed to process finalizeBridgeERC20 method: %v", err) } - return mintEvent, nil } - return nil, fmt.Errorf("tx data not recognized as a cross domain message: %v", txData) + ctx.Logger().Debug("Unsupported cross domain message", "methodID", hexutil.Encode(l1StandardBridgeMethodID)) + return nil, nil +} + +func (k *Keeper) processFinalizeBridgeETH( + ctx sdk.Context, //nolint:gocritic // hugeParam + standardBridgeABI *abi.ABI, + relayMessage *bindings.RelayMessageArgs, +) (*sdk.Event, error) { + var finalizeBridgeETH bindings.FinalizeBridgeETHArgs + if err := unpackInputsIntoInterface(standardBridgeABI, "finalizeBridgeETH", relayMessage.Message, &finalizeBridgeETH); err != nil { + return nil, fmt.Errorf("failed to unpack relay message into finalizeBridgeETH interface: %v", err) + } + + toAddr, err := monomer.CosmosETHAddress(finalizeBridgeETH.To).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) + if err != nil { + return nil, fmt.Errorf("evm to cosmos address: %v", err) + } + + amount := sdkmath.NewIntFromBigInt(finalizeBridgeETH.Amount) + mintEvent, err := k.mintETH(ctx, toAddr, toAddr, amount, amount) + if err != nil { + return nil, fmt.Errorf("failed to mint ETH: %v", err) + } + + return mintEvent, nil +} + +func (k *Keeper) processFinalizeBridgeERC20( + ctx sdk.Context, //nolint:gocritic // hugeParam + standardBridgeABI *abi.ABI, + relayMessage *bindings.RelayMessageArgs, +) (*sdk.Event, error) { + var finalizeBridgeERC20 bindings.FinalizeBridgeERC20Args + if err := unpackInputsIntoInterface(standardBridgeABI, "finalizeBridgeERC20", relayMessage.Message, &finalizeBridgeERC20); err != nil { + return nil, fmt.Errorf("failed to unpack relay message into finalizeBridgeERC20 interface: %v", err) + } + + toAddr, err := monomer.CosmosETHAddress(finalizeBridgeERC20.To).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) + if err != nil { + return nil, fmt.Errorf("evm to cosmos address: %v", err) + } + + mintEvent, err := k.mintERC20(ctx, toAddr, finalizeBridgeERC20.RemoteToken.String(), sdkmath.NewIntFromBigInt(finalizeBridgeERC20.Amount)) + if err != nil { + return nil, fmt.Errorf("failed to mint ERC-20 token: %v", err) + } + + return mintEvent, nil } // mintETH mints ETH to an account where the amount is in wei and returns the associated event. diff --git a/x/rollup/tests/integration/rollup_test.go b/x/rollup/tests/integration/rollup_test.go index 05eed162..dcbe612f 100644 --- a/x/rollup/tests/integration/rollup_test.go +++ b/x/rollup/tests/integration/rollup_test.go @@ -43,22 +43,25 @@ func TestRollup(t *testing.T) { erc20userAddr := common.HexToAddress("0x123456abcdef") erc20depositAmount := big.NewInt(100) - l1AttributesTx, depositTx, _ := monomertestutils.GenerateEthTxs(t) + l1AttributesTx, ethDepositTx, _ := monomertestutils.GenerateEthTxs(t) + ethMintAmount := ethDepositTx.Mint() + ethTransferAmount := ethDepositTx.Value() + + ethBridgeDepositTx := monomertestutils.GenerateEthBridgeDepositTx(t, *ethDepositTx.To(), ethTransferAmount) erc20DepositTx := monomertestutils.GenerateERC20DepositTx(t, erc20tokenAddr, erc20userAddr, erc20depositAmount) l1WithdrawalAddr := common.HexToAddress("0x112233445566").String() l1AttributesTxBz := monomertestutils.TxToBytes(t, l1AttributesTx) - depositTxBz := monomertestutils.TxToBytes(t, depositTx) + ethDepositTxBz := monomertestutils.TxToBytes(t, ethDepositTx) erc20DepositTxBz := monomertestutils.TxToBytes(t, erc20DepositTx) + ethBridgeDepositTxBz := monomertestutils.TxToBytes(t, ethBridgeDepositTx) - mintAmount := depositTx.Mint() - transferAmount := depositTx.Value() - from, err := gethtypes.NewCancunSigner(depositTx.ChainId()).Sender(depositTx) + from, err := gethtypes.NewCancunSigner(ethDepositTx.ChainId()).Sender(ethDepositTx) require.NoError(t, err) mintAddr, err := monomer.CosmosETHAddress(from).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) - userCosmosAddr, err := monomer.CosmosETHAddress(*depositTx.To()).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) + userCosmosAddr, err := monomer.CosmosETHAddress(*ethDepositTx.To()).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) // query the mint address ETH balance and assert it's zero @@ -80,24 +83,27 @@ func TestRollup(t *testing.T) { // send a successful MsgApplyL1Txs and mint ETH to user _, err = integrationApp.RunMsg(&rolluptypes.MsgApplyL1Txs{ - TxBytes: [][]byte{l1AttributesTxBz, depositTxBz, erc20DepositTxBz}, + TxBytes: [][]byte{l1AttributesTxBz, ethDepositTxBz, ethBridgeDepositTxBz, erc20DepositTxBz}, }) require.NoError(t, err) // query the mint address ETH balance and assert it's equal to the mint amount minus the transfer amount - require.Equal(t, new(big.Int).Sub(mintAmount, transferAmount), queryETHBalance(t, bankQueryClient, mintAddr, integrationApp).BigInt()) + require.Equal(t, new(big.Int).Sub(ethMintAmount, ethTransferAmount), queryETHBalance(t, bankQueryClient, mintAddr, integrationApp).BigInt()) + + // userEthAmount is ethTransferAmount * 2 to account for the separate OptimismPortal deposit (ethDepositTx) and the L1StandardBridge deposit tx (ethBridgeDepositTx) + userEthAmount := new(big.Int).Mul(ethTransferAmount, big.NewInt(2)) - // query the recipient address ETH balance and assert it's equal to the transfer amount - require.Equal(t, transferAmount, queryETHBalance(t, bankQueryClient, userCosmosAddr, integrationApp).BigInt()) + // query the recipient address ETH balance and assert it's equal to the expected transfer amount + require.Equal(t, userEthAmount, queryETHBalance(t, bankQueryClient, userCosmosAddr, integrationApp).BigInt()) // query the user's ERC20 balance and assert it's equal to the deposit amount require.Equal(t, erc20depositAmount, queryERC20Balance(t, bankQueryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp).BigInt()) // try to withdraw more than deposited and assert error _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateWithdrawal{ - Sender: mintAddr, + Sender: userCosmosAddr, Target: l1WithdrawalAddr, - Value: math.NewIntFromBigInt(transferAmount).Add(math.OneInt()), + Value: math.NewIntFromBigInt(userEthAmount).Add(math.OneInt()), GasLimit: new(big.Int).SetUint64(100_000_000).Bytes(), Data: []byte{0x01, 0x02, 0x03}, }) @@ -107,7 +113,7 @@ func TestRollup(t *testing.T) { _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateWithdrawal{ Sender: userCosmosAddr, Target: l1WithdrawalAddr, - Value: math.NewIntFromBigInt(transferAmount), + Value: math.NewIntFromBigInt(userEthAmount), GasLimit: new(big.Int).SetUint64(100_000_000).Bytes(), Data: []byte{0x01, 0x02, 0x03}, })