diff --git a/bindings/erc20_deposit_args.go b/bindings/erc20_deposit_args.go new file mode 100644 index 00000000..1279445a --- /dev/null +++ b/bindings/erc20_deposit_args.go @@ -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 +} diff --git a/e2e/stack.go b/e2e/stack.go index 85cc569c..d2c00b40 100644 --- a/e2e/stack.go +++ b/e2e/stack.go @@ -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" @@ -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 @@ -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) @@ -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) diff --git a/e2e/stack_test.go b/e2e/stack_test.go index 0522882e..49af0a40 100644 --- a/e2e/stack_test.go +++ b/e2e/stack_test.go @@ -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" @@ -40,7 +43,6 @@ import ( const ( artifactsDirectoryName = "artifacts" - oneEth = 1e18 ) func openLogFile(t *testing.T, env *environment.Env, name string) *os.File { @@ -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", @@ -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 @@ -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), @@ -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, @@ -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)) @@ -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, @@ -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, @@ -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(), ) @@ -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, @@ -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'", @@ -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 @@ -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) } diff --git a/testutils/utils.go b/testutils/utils.go index fbc8736e..a65bc8ae 100644 --- a/testutils/utils.go +++ b/testutils/utils.go @@ -3,6 +3,7 @@ package testutils import ( "math/big" "math/rand" + "strings" "testing" "github.com/cockroachdb/pebble" @@ -12,10 +13,13 @@ import ( dbm "github.com/cosmos/cosmos-db" sdk "github.com/cosmos/cosmos-sdk/types" sdktx "github.com/cosmos/cosmos-sdk/types/tx" + opbindings "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-node/rollup" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum-optimism/optimism/op-service/testutils" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core/rawdb" @@ -84,6 +88,47 @@ 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) + standardBridgeABI, err := abi.JSON(strings.NewReader(opbindings.StandardBridgeMetaData.ABI)) + require.NoError(t, err) + + rng := rand.New(rand.NewSource(1234)) + + finalizeBridgeERC20Bz, err := standardBridgeABI.Pack( + "finalizeBridgeERC20", + tokenAddr, // l1 token address + testutils.RandomAddress(rng), // l2 token address + testutils.RandomAddress(rng), // from + userAddr, // to + amount, // amount + []byte{}, // extra data + ) + require.NoError(t, err) + + relayMessageBz, err := crossDomainMessengerABI.Pack( + "relayMessage", + big.NewInt(0), // nonce + testutils.RandomAddress(rng), // sender + testutils.RandomAddress(rng), // target + amount, // value + big.NewInt(0), // min gas limit + finalizeBridgeERC20Bz, // message + ) + require.NoError(t, err) + + to := testutils.RandomAddress(rng) + depositTx := &gethtypes.DepositTx{ + // TODO: remove hardcoded address once a genesis state is configured + // L2 aliased L1CrossDomainMessenger proxy address + From: crossdomain.ApplyL1ToL2Alias(common.HexToAddress("0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE")), + To: &to, + Data: relayMessageBz, + } + return gethtypes.NewTx(depositTx) +} + func TxToBytes(t *testing.T, tx *gethtypes.Transaction) []byte { txBytes, err := tx.MarshalBinary() require.NoError(t, err) diff --git a/x/rollup/keeper/deposits.go b/x/rollup/keeper/deposits.go index 6732f264..746ea134 100644 --- a/x/rollup/keeper/deposits.go +++ b/x/rollup/keeper/deposits.go @@ -1,16 +1,24 @@ package keeper import ( + "bytes" "encoding/json" "fmt" "math/big" + "reflect" + "strings" sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" + opbindings "github.com/ethereum-optimism/optimism/op-bindings/bindings" + "github.com/ethereum-optimism/optimism/op-chain-ops/crossdomain" "github.com/ethereum-optimism/optimism/op-node/rollup/derive" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/polymerdao/monomer" + "github.com/polymerdao/monomer/bindings" "github.com/polymerdao/monomer/utils" "github.com/polymerdao/monomer/x/rollup/types" "github.com/samber/lo" @@ -102,19 +110,82 @@ func (k *Keeper) processL1UserDepositTxs( return nil, types.WrapError(types.ErrMintETH, "failed to mint ETH for cosmosAddress: %v; err: %v", cosmAddr, err) } mintEvents = append(mintEvents, *mintEvent) + + // TODO: remove hardcoded address once a genesis state is configured + // Convert the L1CrossDomainMessenger address to its L2 aliased address + aliasedL1CrossDomainMessengerAddress := crossdomain.ApplyL1ToL2Alias(common.HexToAddress("0x9A9f2CCfdE556A7E9Ff0848998Aa4a0CFD8863AE")) + + // 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()) + // 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 { + ctx.Logger().Error("Failed to parse or execute cross domain message", "err", err) + return nil, types.WrapError(types.ErrInvalidL1Txs, "failed to parse or execute cross domain message: %v", err) + } else { + mintEvents = append(mintEvents, *erc20mintEvent) + } + } } 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 + crossDomainMessengerABI, err := abi.JSON(strings.NewReader(opbindings.CrossDomainMessengerMetaData.ABI)) + if err != nil { + return nil, fmt.Errorf("failed to parse CrossDomainMessenger ABI: %v", err) + } + standardBridgeABI, err := abi.JSON(strings.NewReader(opbindings.StandardBridgeMetaData.ABI)) + if err != nil { + return nil, fmt.Errorf("failed to parse StandardBridge ABI: %v", err) + } + + var relayMessage bindings.RelayMessageArgs + if err = unpackInputsIntoInterface(&crossDomainMessengerABI, "relayMessage", txData, &relayMessage); err != nil { + 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) + } + + // Mint the ERC-20 token to the specified Cosmos address + mintEvent, err := k.mintERC20( + ctx, + utils.EvmToCosmosAddress(finalizeBridgeERC20.To), + 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 + } + + return nil, fmt.Errorf("tx data not recognized as a cross domain message: %v", txData) +} + // mintETH mints ETH to an account where the amount is in wei and returns the associated event. func (k *Keeper) mintETH(ctx sdk.Context, addr sdk.AccAddress, amount sdkmath.Int) (*sdk.Event, error) { //nolint:gocritic // hugeParam coin := sdk.NewCoin(types.ETH, amount) if err := k.bankkeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { - return nil, fmt.Errorf("failed to mint deposit coins to the rollup module: %v", err) + return nil, fmt.Errorf("failed to mint ETH deposit coins to the rollup module: %v", err) } if err := k.bankkeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, sdk.NewCoins(coin)); err != nil { - return nil, fmt.Errorf("failed to send deposit coins from rollup module to user account %v: %v", addr, err) + return nil, fmt.Errorf("failed to send ETH deposit coins from rollup module to user account %v: %v", addr, err) } mintEvent := sdk.NewEvent( @@ -126,3 +197,59 @@ func (k *Keeper) mintETH(ctx sdk.Context, addr sdk.AccAddress, amount sdkmath.In return &mintEvent, nil } + +// mintERC20 mints a bridged ERC-20 token to an account and returns the associated event. +func (k *Keeper) mintERC20( + ctx sdk.Context, //nolint:gocritic // hugeParam + userAddr sdk.AccAddress, + erc20addr string, + amount sdkmath.Int, +) (*sdk.Event, error) { + // use the "erc20/{l1erc20addr}" format for the coin denom + coin := sdk.NewCoin("erc20/"+erc20addr[2:], amount) + if err := k.bankkeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { + return nil, fmt.Errorf("failed to mint ERC-20 deposit coins to the rollup module: %v", err) + } + if err := k.bankkeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, userAddr, sdk.NewCoins(coin)); err != nil { + return nil, fmt.Errorf("failed to send ERC-20 deposit coins from rollup module to user account %v: %v", userAddr, err) + } + + mintEvent := sdk.NewEvent( + types.EventTypeMintERC20, + sdk.NewAttribute(types.AttributeKeyL1DepositTxType, types.L1UserDepositTxType), + sdk.NewAttribute(types.AttributeKeyToCosmosAddress, userAddr.String()), + sdk.NewAttribute(types.AttributeKeyERC20Address, erc20addr), + sdk.NewAttribute(types.AttributeKeyValue, hexutil.Encode(amount.BigInt().Bytes())), + ) + + return &mintEvent, nil +} + +// unpackInputsIntoInterface unpacks the input data of a function call into an interface. This function behaves +// similarly to the geth abi UnpackIntoInterface function but unpacks method inputs instead of outputs. +func unpackInputsIntoInterface(contractABI *abi.ABI, methodName string, inputData []byte, outputInterface interface{}) error { + method := contractABI.Methods[methodName] + + // Check if the function selector matches the method ID + functionSelector := inputData[:4] + if !bytes.Equal(functionSelector, method.ID) { + return fmt.Errorf("unexpected function selector: got %x, expected %x", functionSelector, method.ID) + } + + inputs, err := method.Inputs.Unpack(inputData[4:]) + if err != nil { + return fmt.Errorf("failed to unpack input data: %v", err) + } + + outputVal := reflect.ValueOf(outputInterface).Elem() + for i, input := range inputs { + field := outputVal.Field(i) + if field.CanSet() { + val := reflect.ValueOf(input) + field.Set(val) + } else { + return fmt.Errorf("field %d can not be set for method %v", i, methodName) + } + } + return nil +} diff --git a/x/rollup/tests/integration/rollup_test.go b/x/rollup/tests/integration/rollup_test.go index 58dfb7fc..3829f1a7 100644 --- a/x/rollup/tests/integration/rollup_test.go +++ b/x/rollup/tests/integration/rollup_test.go @@ -36,11 +36,17 @@ func TestRollup(t *testing.T) { queryClient := banktypes.NewQueryClient(integrationApp.QueryHelper()) monomerSigner := "cosmos1fl48vsnmsdzcv85q5d2q4z5ajdha8yu34mf0eh" + erc20tokenAddr := common.HexToAddress("0xabcdef123456") + erc20userAddr := common.HexToAddress("0x123456abcdef") + erc20depositAmount := big.NewInt(100) + l1AttributesTx, depositTx, _ := monomertestutils.GenerateEthTxs(t) - l1WithdrawalAddr := common.HexToAddress("0x12345").String() + erc20DepositTx := monomertestutils.GenerateERC20DepositTx(t, erc20tokenAddr, erc20userAddr, erc20depositAmount) + l1WithdrawalAddr := common.HexToAddress("0x112233445566").String() l1AttributesTxBz := monomertestutils.TxToBytes(t, l1AttributesTx) depositTxBz := monomertestutils.TxToBytes(t, depositTx) + erc20DepositTxBz := monomertestutils.TxToBytes(t, erc20DepositTx) depositAmount := depositTx.Mint() from, err := gethtypes.NewCancunSigner(depositTx.ChainId()).Sender(depositTx) @@ -50,6 +56,9 @@ func TestRollup(t *testing.T) { // query the user's ETH balance and assert it's zero require.Equal(t, math.ZeroInt(), queryUserETHBalance(t, queryClient, userAddr, integrationApp)) + // query the user's ERC20 balance and assert it's zero + require.Equal(t, math.ZeroInt(), queryUserERC20Balance(t, queryClient, utils.EvmToCosmosAddress(erc20userAddr), erc20tokenAddr, integrationApp)) + // send an invalid MsgApplyL1Txs and assert error _, err = integrationApp.RunMsg(&rolluptypes.MsgApplyL1Txs{ TxBytes: [][]byte{l1AttributesTxBz, l1AttributesTxBz}, @@ -59,7 +68,7 @@ func TestRollup(t *testing.T) { // send a successful MsgApplyL1Txs and mint ETH to user _, err = integrationApp.RunMsg(&rolluptypes.MsgApplyL1Txs{ - TxBytes: [][]byte{l1AttributesTxBz, depositTxBz}, + TxBytes: [][]byte{l1AttributesTxBz, depositTxBz, erc20DepositTxBz}, FromAddress: monomerSigner, }) require.NoError(t, err) @@ -67,6 +76,9 @@ func TestRollup(t *testing.T) { // query the user's ETH balance and assert it's equal to the deposit amount require.Equal(t, depositAmount, queryUserETHBalance(t, queryClient, userAddr, integrationApp).BigInt()) + // query the user's ERC20 balance and assert it's equal to the deposit amount + require.Equal(t, erc20depositAmount, queryUserERC20Balance(t, queryClient, utils.EvmToCosmosAddress(erc20userAddr), erc20tokenAddr, integrationApp).BigInt()) + // try to withdraw more than deposited and assert error _, err = integrationApp.RunMsg(&rolluptypes.MsgInitiateWithdrawal{ Sender: userAddr.String(), @@ -144,11 +156,19 @@ func setupIntegrationApp(t *testing.T) *integration.App { return integrationApp } -func queryUserETHBalance(t *testing.T, queryClient banktypes.QueryClient, userAddr sdk.AccAddress, app *integration.App) math.Int { +func queryUserBalance(t *testing.T, queryClient banktypes.QueryClient, userAddr sdk.AccAddress, denom string, app *integration.App) math.Int { resp, err := queryClient.Balance(app.Context(), &banktypes.QueryBalanceRequest{ Address: userAddr.String(), - Denom: rolluptypes.ETH, + Denom: denom, }) require.NoError(t, err) return resp.Balance.Amount } + +func queryUserETHBalance(t *testing.T, queryClient banktypes.QueryClient, userAddr sdk.AccAddress, app *integration.App) math.Int { + return queryUserBalance(t, queryClient, userAddr, rolluptypes.ETH, app) +} + +func queryUserERC20Balance(t *testing.T, queryClient banktypes.QueryClient, userAddr sdk.AccAddress, erc20addr common.Address, app *integration.App) math.Int { + return queryUserBalance(t, queryClient, userAddr, "erc20/"+erc20addr.String()[2:], app) +} diff --git a/x/rollup/types/events.go b/x/rollup/types/events.go index 6704f31b..f005327c 100644 --- a/x/rollup/types/events.go +++ b/x/rollup/types/events.go @@ -11,10 +11,12 @@ const ( AttributeKeyGasLimit = "gas_limit" AttributeKeyData = "data" AttributeKeyNonce = "nonce" + AttributeKeyERC20Address = "erc20_address" L1UserDepositTxType = "l1_user_deposit" EventTypeMintETH = "mint_eth" + EventTypeMintERC20 = "mint_erc20" EventTypeBurnETH = "burn_eth" EventTypeWithdrawalInitiated = "withdrawal_initiated" )