Skip to content

Commit

Permalink
Enable ETH deposits through L1StandardBridge (#302)
Browse files Browse the repository at this point in the history
  • Loading branch information
natebeauregard authored Nov 26, 2024
1 parent 81caf73 commit 72e601d
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 82 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
91 changes: 58 additions & 33 deletions e2e/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 //////
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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{},
Expand All @@ -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))

Expand Down Expand Up @@ -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
Expand Down
32 changes: 27 additions & 5 deletions testutils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)

Expand Down
96 changes: 65 additions & 31 deletions x/rollup/keeper/deposits.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,23 +125,23 @@ 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)
}
}
}

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

0 comments on commit 72e601d

Please sign in to comment.