diff --git a/builder/builder_test.go b/builder/builder_test.go index 11936983..c048a030 100644 --- a/builder/builder_test.go +++ b/builder/builder_test.go @@ -26,7 +26,6 @@ import ( "github.com/polymerdao/monomer/monomerdb/localdb" "github.com/polymerdao/monomer/testapp" "github.com/polymerdao/monomer/testutils" - "github.com/polymerdao/monomer/utils" "github.com/polymerdao/monomer/x/rollup/types" "github.com/stretchr/testify/require" ) @@ -314,12 +313,12 @@ func TestBuildRollupTxs(t *testing.T) { depositTxETH := ethTxs[1] require.NotNil(t, depositTxETH.Mint()) require.NotNil(t, depositTxETH.To(), "Deposit transaction must have a 'to' address") - recipientAddr, err := utils.EvmToCosmosAddress("cosmos", *depositTxETH.To()) + recipientAddr, err := monomer.CosmosETHAddress(*depositTxETH.To()).Encode("cosmos") require.NoError(t, err) from, err := gethtypes.NewCancunSigner(depositTxETH.ChainId()).Sender(depositTxETH) require.NoError(t, err) - mintAddr, err := utils.EvmToCosmosAddress("cosmos", from) + mintAddr, err := monomer.CosmosETHAddress(from).Encode("cosmos") require.NoError(t, err) withdrawalTx := testapp.ToTx(t, &types.MsgInitiateWithdrawal{ diff --git a/e2e/stack_test.go b/e2e/stack_test.go index faab3959..0eb57e83 100644 --- a/e2e/stack_test.go +++ b/e2e/stack_test.go @@ -35,7 +35,6 @@ import ( "github.com/polymerdao/monomer/environment" "github.com/polymerdao/monomer/node" "github.com/polymerdao/monomer/testapp" - "github.com/polymerdao/monomer/utils" rolluptypes "github.com/polymerdao/monomer/x/rollup/types" "github.com/stretchr/testify/require" "golang.org/x/exp/slog" @@ -249,25 +248,27 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { // instantiate L1 user, tx signer. userPrivKey := stack.Users[0] - userAddress := crypto.PubkeyToAddress(userPrivKey.PublicKey) + userETHAddress := 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) + userCosmosETHAddress := monomer.CosmosETHAddress(userETHAddress) + ////////////////////////// ////// ETH DEPOSITS ////// ////////////////////////// // get the user's balance before the deposit has been processed - balanceBeforeDeposit, err := l1Client.BalanceAt(stack.Ctx, userAddress, nil) + balanceBeforeDeposit, err := l1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) // send user Deposit Tx depositAmount := big.NewInt(params.Ether) depositTx, err := stack.OptimismPortal.DepositTransaction( createL1TransactOpts(t, stack, userPrivKey, l1signer, l1GasLimit, depositAmount), - userAddress, + common.Address(userCosmosETHAddress), depositAmount, l2GasLimit, false, // _isCreation @@ -293,8 +294,8 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { End: nil, Context: stack.Ctx, }, - []common.Address{userAddress}, - []common.Address{userAddress}, + []common.Address{userETHAddress}, + []common.Address{common.Address(userCosmosETHAddress)}, []*big.Int{big.NewInt(0)}, ) require.NoError(t, err, "configuring 'TransactionDeposited' event listener") @@ -302,7 +303,7 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { require.NoError(t, depositLogs.Close()) // get the user's balance after the deposit has been processed - balanceAfterDeposit, err := stack.L1Client.BalanceAt(stack.Ctx, userAddress, nil) + balanceAfterDeposit, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) //nolint:gocritic @@ -316,7 +317,7 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { // check that the user's balance has been updated on L1 require.Equal(t, expectedBalance, balanceAfterDeposit) - userCosmosAddr, err := utils.EvmToCosmosAddress("cosmos", userAddress) + userCosmosAddr, err := userCosmosETHAddress.Encode("cosmos") require.NoError(t, err) depositValueHex := hexutil.Encode(depositAmount.Bytes()) requireEthIsMinted(t, stack, userCosmosAddr, depositValueHex) @@ -328,10 +329,10 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { ///////////////////////////// // 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)) + withdrawalTx := e2e.NewWithdrawalTx(0, common.Address(userCosmosETHAddress), userETHAddress, depositAmount, new(big.Int).SetUint64(params.TxGas)) // initiate the withdrawal of the deposited amount on L2 - senderAddr, err := utils.EvmToCosmosAddress("cosmos", *withdrawalTx.Sender) + senderAddr, err := monomer.CosmosETHAddress(*withdrawalTx.Sender).Encode("cosmos") require.NoError(t, err) withdrawalTxResult, err := stack.L2Client.BroadcastTxAsync( stack.Ctx, @@ -400,7 +401,7 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { time.Sleep(time.Duration(finalizationPeriod.Uint64()) * time.Second) // get the user's balance before the withdrawal has been finalized - balanceBeforeFinalization, err := stack.L1Client.BalanceAt(stack.Ctx, userAddress, nil) + balanceBeforeFinalization, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) // send a withdrawal finalizing tx to finalize the withdrawal on L1 @@ -433,7 +434,7 @@ func ethRollupFlow(t *testing.T, stack *e2e.StackConfig) { require.NoError(t, finalizeWithdrawalLogs.Close()) // get the user's balance after the withdrawal has been finalized - balanceAfterFinalization, err := stack.L1Client.BalanceAt(stack.Ctx, userAddress, nil) + balanceAfterFinalization, err := stack.L1Client.BalanceAt(stack.Ctx, userETHAddress, nil) require.NoError(t, err) //nolint:gocritic @@ -533,7 +534,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 := utils.EvmToCosmosAddress("cosmos", userAddress) + userAddr, err := monomer.CosmosETHAddress(userAddress).Encode("cosmos") require.NoError(t, err) requireERC20IsMinted(t, stack, userAddr, weth9Address.String(), hexutil.Encode(wethL2Amount.Bytes())) diff --git a/monomer.go b/monomer.go index 99106db0..e01bb027 100644 --- a/monomer.go +++ b/monomer.go @@ -2,6 +2,7 @@ package monomer import ( "context" + "crypto/ecdsa" "crypto/sha256" "encoding/binary" "errors" @@ -13,14 +14,17 @@ import ( abcitypes "github.com/cometbft/cometbft/abci/types" bfttypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/types/bech32" opeth "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" "github.com/polymerdao/monomer/utils" + "golang.org/x/crypto/ripemd160" //nolint:staticcheck ) type Application interface { @@ -237,3 +241,26 @@ func NewChainConfig(chainID *big.Int) *params.ChainConfig { CanyonTime: utils.Ptr(uint64(0)), } } + +// CosmosETHAddress is a Cosmos address packed into an Ethereum address. +// Only addresses derived from secp256k1 keys can be packed into an Ethereum address. +// See [ADR-28] for more details. +// +//nolint:lll // [ADR-28]: https://github.com/cosmos/cosmos-sdk/blob/8bfcf554275c1efbb42666cc8510d2da139b67fa/docs/architecture/adr-028-public-key-addresses.md?plain=1#L85 +type CosmosETHAddress common.Address + +// PubkeyToCosmosETHAddress converts a secp256k1 public key to a CosmosETHAddress. +// Passing in a non-secp256k1 key results in undefined behavior. +func PubkeyToCosmosETHAddress(pubKey *ecdsa.PublicKey) CosmosETHAddress { + sha := sha256.Sum256(secp256k1.CompressPubkey(pubKey.X, pubKey.Y)) + hasherRIPEMD160 := ripemd160.New() + if _, err := hasherRIPEMD160.Write(sha[:]); err != nil { + // hash.Hash never returns an error on Write. This panic should never execute. + panic(fmt.Errorf("ripemd160: %v", err)) + } + return CosmosETHAddress(hasherRIPEMD160.Sum(nil)) +} + +func (a CosmosETHAddress) Encode(hrp string) (string, error) { + return bech32.ConvertAndEncode(hrp, common.Address(a).Bytes()) +} diff --git a/monomer_test.go b/monomer_test.go index 4b63f977..42002d62 100644 --- a/monomer_test.go +++ b/monomer_test.go @@ -7,6 +7,9 @@ import ( "time" bfttypes "github.com/cometbft/cometbft/types" + cosmossecp256k1 "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/decred/dcrd/dcrec/secp256k1/v4" opeth "github.com/ethereum-optimism/optimism/op-service/eth" "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" @@ -219,3 +222,18 @@ func TestPayloadAttributesValidForkchoiceUpdateResult(t *testing.T) { PayloadID: payloadID, }, result) } + +func TestCosmosETHAddress(t *testing.T) { + privKey, err := secp256k1.GeneratePrivateKey() + require.NoError(t, err) + pubKey := privKey.PubKey().ToECDSA() + got := monomer.PubkeyToCosmosETHAddress(pubKey) + wantBytes := (&cosmossecp256k1.PubKey{ + Key: privKey.PubKey().SerializeCompressed(), // https://github.com/cosmos/cosmos-sdk/blob/346044afd0ecd4738c13993d2ac75da8e242266d/crypto/keys/secp256k1/secp256k1.go#L44-L45 + }).Address().Bytes() + require.Equal(t, wantBytes, common.Address(got).Bytes()) + // We have to use the `cosmos` hrp here because sdk.AccAddress.String() uses the global SDK config variable that uses the `cosmos` hrp. + gotEncoded, err := got.Encode("cosmos") + require.NoError(t, err) + require.Equal(t, sdk.AccAddress(wantBytes).String(), gotEncoded) +} diff --git a/utils/utils.go b/utils/utils.go index a652991d..936195d9 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -4,8 +4,6 @@ import ( "fmt" "io" - "github.com/cosmos/cosmos-sdk/types/bech32" - "github.com/ethereum/go-ethereum/common" "github.com/hashicorp/go-multierror" ) @@ -23,12 +21,3 @@ func WrapCloseErr(err error, closer io.Closer) error { } return nil } - -// EvmToCosmosAddress converts an EVM address to a string -func EvmToCosmosAddress(prefix string, ethAddr common.Address) (string, error) { - addr, err := bech32.ConvertAndEncode(prefix, ethAddr.Bytes()) - if err != nil { - return "", fmt.Errorf("convert and encode: %v", err) - } - return addr, nil -} diff --git a/x/rollup/keeper/deposits.go b/x/rollup/keeper/deposits.go index 4feafb2e..76a4254f 100644 --- a/x/rollup/keeper/deposits.go +++ b/x/rollup/keeper/deposits.go @@ -18,7 +18,6 @@ import ( 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" ) @@ -122,13 +121,13 @@ func (k *Keeper) processL1UserDepositTxs( return nil, types.WrapError(types.ErrInvalidL1Txs, "failed to get sender address: %v", err) } addrPrefix := sdk.GetConfig().GetBech32AccountAddrPrefix() - mintAddr, err := utils.EvmToCosmosAddress(addrPrefix, from) + mintAddr, err := monomer.CosmosETHAddress(from).Encode(addrPrefix) if err != nil { ctx.Logger().Error("Failed to convert EVM to Cosmos address", "err", err) return nil, fmt.Errorf("evm to cosmos address: %v", err) } mintAmount := sdkmath.NewIntFromBigInt(tx.Mint()) - recipientAddr, err := utils.EvmToCosmosAddress(addrPrefix, *tx.To()) + recipientAddr, err := monomer.CosmosETHAddress(*tx.To()).Encode(addrPrefix) if err != nil { ctx.Logger().Error("Failed to convert EVM to Cosmos address", "err", err) return nil, fmt.Errorf("evm to cosmos address: %v", err) @@ -192,7 +191,7 @@ func (k *Keeper) parseAndExecuteCrossDomainMessage(ctx sdk.Context, txData []byt return nil, fmt.Errorf("failed to unpack relay message into finalizeBridgeERC20 interface: %v", err) } - toAddr, err := utils.EvmToCosmosAddress(sdk.GetConfig().GetBech32AccountAddrPrefix(), finalizeBridgeERC20.To) + toAddr, err := monomer.CosmosETHAddress(finalizeBridgeERC20.To).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) if err != nil { return nil, fmt.Errorf("evm to cosmos address: %v", err) } diff --git a/x/rollup/tests/integration/rollup_test.go b/x/rollup/tests/integration/rollup_test.go index 6de52911..e757d4f8 100644 --- a/x/rollup/tests/integration/rollup_test.go +++ b/x/rollup/tests/integration/rollup_test.go @@ -23,8 +23,8 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" "github.com/ethereum/go-ethereum/common" gethtypes "github.com/ethereum/go-ethereum/core/types" + "github.com/polymerdao/monomer" monomertestutils "github.com/polymerdao/monomer/testutils" - "github.com/polymerdao/monomer/utils" "github.com/polymerdao/monomer/x/rollup" rollupkeeper "github.com/polymerdao/monomer/x/rollup/keeper" rolluptypes "github.com/polymerdao/monomer/x/rollup/types" @@ -52,9 +52,9 @@ func TestRollup(t *testing.T) { from, err := gethtypes.NewCancunSigner(depositTx.ChainId()).Sender(depositTx) require.NoError(t, err) - mintAddr, err := utils.EvmToCosmosAddress(sdk.GetConfig().GetBech32AccountAddrPrefix(), from) + mintAddr, err := monomer.CosmosETHAddress(from).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) - recipientAddr, err := utils.EvmToCosmosAddress(sdk.GetConfig().GetBech32AccountAddrPrefix(), *depositTx.To()) + recipientAddr, err := monomer.CosmosETHAddress(*depositTx.To()).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) // query the mint address ETH balance and assert it's zero @@ -64,7 +64,7 @@ func TestRollup(t *testing.T) { require.Equal(t, math.ZeroInt(), queryUserETHBalance(t, queryClient, recipientAddr, integrationApp)) // query the user's ERC20 balance and assert it's zero - erc20userCosmosAddr, err := utils.EvmToCosmosAddress(sdk.GetConfig().GetBech32AccountAddrPrefix(), erc20userAddr) + erc20userCosmosAddr, err := monomer.CosmosETHAddress(erc20userAddr).Encode(sdk.GetConfig().GetBech32AccountAddrPrefix()) require.NoError(t, err) require.Equal(t, math.ZeroInt(), queryUserERC20Balance(t, queryClient, erc20userCosmosAddr, erc20tokenAddr, integrationApp))