mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-04 09:57:27 +00:00 
			
		
		
		
	feat(evmutil)!: implement MsgConvertCosmosCoinToERC20 (#1603)
* feat(evmutil): implement MsgConvertCosmosCoinToERC20 * docs(evmutil): update module spec * update changelog * rename conversion -> conversion_evm_native * refactor ConvertCosmosCoinToERC20 to keeper method * add CLI cmd for MsgConvertCosmosCoinToERC20 * updates from pr
This commit is contained in:
		
							parent
							
								
									1459170a37
								
							
						
					
					
						commit
						741f1e42ee
					
				@ -40,6 +40,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
 | 
				
			|||||||
- (evmutil) [#1590] & [#1596] Add allow list param of sdk native denoms that can be transferred to evm
 | 
					- (evmutil) [#1590] & [#1596] Add allow list param of sdk native denoms that can be transferred to evm
 | 
				
			||||||
- (evmutil) [#1591] & [#1596] Configure module to support deploying ERC20KavaWrappedCosmosCoin contracts
 | 
					- (evmutil) [#1591] & [#1596] Configure module to support deploying ERC20KavaWrappedCosmosCoin contracts
 | 
				
			||||||
- (evmutil) [#1598] Track deployed ERC20 contract addresses for representing cosmos coins in module state
 | 
					- (evmutil) [#1598] Track deployed ERC20 contract addresses for representing cosmos coins in module state
 | 
				
			||||||
 | 
					- (evmutil) [#1603] Add MsgConvertCosmosCoinToERC20 for converting an sdk.Coin to an ERC20 in the EVM
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Client Breaking
 | 
				
			||||||
 | 
					- (evmutil) [#1603] Renamed error `ErrConversionNotEnabled` to `ErrEVMConversionNotEnabled`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## [v0.23.0]
 | 
					## [v0.23.0]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -242,6 +246,7 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md).
 | 
				
			|||||||
- [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run
 | 
					- [#257](https://github.com/Kava-Labs/kava/pulls/257) Include scripts to run
 | 
				
			||||||
  large-scale simulations remotely using aws-batch
 | 
					  large-scale simulations remotely using aws-batch
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[#1603]: https://github.com/Kava-Labs/kava/pull/1603
 | 
				
			||||||
[#1598]: https://github.com/Kava-Labs/kava/pull/1598
 | 
					[#1598]: https://github.com/Kava-Labs/kava/pull/1598
 | 
				
			||||||
[#1596]: https://github.com/Kava-Labs/kava/pull/1596
 | 
					[#1596]: https://github.com/Kava-Labs/kava/pull/1596
 | 
				
			||||||
[#1591]: https://github.com/Kava-Labs/kava/pull/1591
 | 
					[#1591]: https://github.com/Kava-Labs/kava/pull/1591
 | 
				
			||||||
 | 
				
			|||||||
@ -30,6 +30,7 @@ func GetTxCmd() *cobra.Command {
 | 
				
			|||||||
	cmds := []*cobra.Command{
 | 
						cmds := []*cobra.Command{
 | 
				
			||||||
		getCmdMsgConvertCoinToERC20(),
 | 
							getCmdMsgConvertCoinToERC20(),
 | 
				
			||||||
		getCmdConvertERC20ToCoin(),
 | 
							getCmdConvertERC20ToCoin(),
 | 
				
			||||||
 | 
							getCmdMsgConvertCosmosCoinToERC20(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for _, cmd := range cmds {
 | 
						for _, cmd := range cmds {
 | 
				
			||||||
@ -43,7 +44,7 @@ func GetTxCmd() *cobra.Command {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func getCmdMsgConvertCoinToERC20() *cobra.Command {
 | 
					func getCmdMsgConvertCoinToERC20() *cobra.Command {
 | 
				
			||||||
	return &cobra.Command{
 | 
						return &cobra.Command{
 | 
				
			||||||
		Use:   "convert-coin-to-erc20 [Kava ERC20 address] [coin]",
 | 
							Use:   "convert-coin-to-erc20 [Kava EVM address] [coin]",
 | 
				
			||||||
		Short: "converts sdk.Coin to erc20 tokens on Kava eth co-chain",
 | 
							Short: "converts sdk.Coin to erc20 tokens on Kava eth co-chain",
 | 
				
			||||||
		Example: fmt.Sprintf(
 | 
							Example: fmt.Sprintf(
 | 
				
			||||||
			`%s tx %s convert-coin-to-erc20 0x7Bbf300890857b8c241b219C6a489431669b3aFA 500000000erc20/usdc --from <key> --gas 2000000`,
 | 
								`%s tx %s convert-coin-to-erc20 0x7Bbf300890857b8c241b219C6a489431669b3aFA 500000000erc20/usdc --from <key> --gas 2000000`,
 | 
				
			||||||
@ -121,3 +122,40 @@ func getCmdConvertERC20ToCoin() *cobra.Command {
 | 
				
			|||||||
		},
 | 
							},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getCmdMsgConvertCosmosCoinToERC20() *cobra.Command {
 | 
				
			||||||
 | 
						return &cobra.Command{
 | 
				
			||||||
 | 
							Use:   "convert-cosmos-coin-to-erc20 [receiver_0x_address] [amount] [flags]",
 | 
				
			||||||
 | 
							Short: "converts asset native to Cosmos Co-chain to an ERC20 on the EVM Co-chain",
 | 
				
			||||||
 | 
							Example: fmt.Sprintf(
 | 
				
			||||||
 | 
								`Convert 500 ATOM and send ERC20 to 0x03db6b11F47d074a532b9eb8a98aB7AdA5845087:
 | 
				
			||||||
 | 
					  %s tx %s convert-cosmos-coin-to-erc20 0x03db6b11F47d074a532b9eb8a98aB7AdA5845087 500000000ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2 --from <key> --gas 2000000`,
 | 
				
			||||||
 | 
								version.AppName, types.ModuleName,
 | 
				
			||||||
 | 
							),
 | 
				
			||||||
 | 
							Args: cobra.ExactArgs(2),
 | 
				
			||||||
 | 
							RunE: func(cmd *cobra.Command, args []string) error {
 | 
				
			||||||
 | 
								clientCtx, err := client.GetClientTxContext(cmd)
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								receiver := args[0]
 | 
				
			||||||
 | 
								if !common.IsHexAddress(receiver) {
 | 
				
			||||||
 | 
									return fmt.Errorf("receiver '%s' is an invalid hex address", args[0])
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								amount, err := sdk.ParseCoinNormalized(args[1])
 | 
				
			||||||
 | 
								if err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								signer := clientCtx.GetFromAddress()
 | 
				
			||||||
 | 
								msg := types.NewMsgConvertCosmosCoinToERC20(signer.String(), receiver, amount)
 | 
				
			||||||
 | 
								if err := msg.ValidateBasic(); err != nil {
 | 
				
			||||||
 | 
									return err
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										50
									
								
								x/evmutil/keeper/conversion_cosmos_native.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								x/evmutil/keeper/conversion_cosmos_native.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					package keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						errorsmod "cosmossdk.io/errors"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/evmutil/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConvertCosmosCoinToERC20 locks the initiator's sdk.Coin in the module account
 | 
				
			||||||
 | 
					// and mints the receiver a corresponding amount of an ERC20 representing the Coin.
 | 
				
			||||||
 | 
					// If a conversion has never been made before and no contract exists, one will be deployed.
 | 
				
			||||||
 | 
					// Only denoms registered to the AllowedCosmosDenoms param may be converted.
 | 
				
			||||||
 | 
					func (k *Keeper) ConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
						ctx sdk.Context,
 | 
				
			||||||
 | 
						initiator sdk.AccAddress,
 | 
				
			||||||
 | 
						receiver types.InternalEVMAddress,
 | 
				
			||||||
 | 
						amount sdk.Coin,
 | 
				
			||||||
 | 
					) error {
 | 
				
			||||||
 | 
						// check that the conversion is allowed
 | 
				
			||||||
 | 
						tokenInfo, allowed := k.GetAllowedTokenMetadata(ctx, amount.Denom)
 | 
				
			||||||
 | 
						if !allowed {
 | 
				
			||||||
 | 
							return errorsmod.Wrapf(types.ErrSDKConversionNotEnabled, amount.Denom)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// send coins from initiator to the module account
 | 
				
			||||||
 | 
						// do this before possible contract deploy to prevent unnecessary store interactions
 | 
				
			||||||
 | 
						err := k.bankKeeper.SendCoinsFromAccountToModule(
 | 
				
			||||||
 | 
							ctx, initiator, types.ModuleName, sdk.NewCoins(amount),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// find deployed contract if it exits
 | 
				
			||||||
 | 
						contractAddress, err := k.GetOrDeployCosmosCoinERC20Contract(ctx, tokenInfo)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// mint erc20 tokens for the user
 | 
				
			||||||
 | 
						err = k.MintERC20(ctx, contractAddress, receiver, amount.Amount.BigInt())
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO emit conversion event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										151
									
								
								x/evmutil/keeper/conversion_cosmos_native_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								x/evmutil/keeper/conversion_cosmos_native_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					package keeper_test
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"math/big"
 | 
				
			||||||
 | 
						"testing"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/stretchr/testify/suite"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sdkmath "cosmossdk.io/math"
 | 
				
			||||||
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/evmutil/testutil"
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/x/evmutil/types"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ConversionCosmosNativeSuite struct {
 | 
				
			||||||
 | 
						testutil.Suite
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func TestConversionCosmosNativeSuite(t *testing.T) {
 | 
				
			||||||
 | 
						suite.Run(t, new(ConversionCosmosNativeSuite))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fail test if contract for denom not registered
 | 
				
			||||||
 | 
					func (suite *ConversionCosmosNativeSuite) denomContractRegistered(denom string) types.InternalEVMAddress {
 | 
				
			||||||
 | 
						contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
 | 
				
			||||||
 | 
						suite.True(found)
 | 
				
			||||||
 | 
						return contractAddress
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// fail test if contract for denom IS registered
 | 
				
			||||||
 | 
					func (suite *ConversionCosmosNativeSuite) denomContractNotRegistered(denom string) {
 | 
				
			||||||
 | 
						_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
 | 
				
			||||||
 | 
						suite.False(found)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// more tests of tests of this method are made to the msg handler, see ./msg_server_test.go
 | 
				
			||||||
 | 
					func (suite *ConversionCosmosNativeSuite) TestConvertCosmosCoinToERC20() {
 | 
				
			||||||
 | 
						allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
 | 
				
			||||||
 | 
						initialFunding := sdk.NewInt64Coin(allowedDenom, int64(1e10))
 | 
				
			||||||
 | 
						initiator := app.RandomAddress()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						amount := sdk.NewInt64Coin(allowedDenom, 6e8)
 | 
				
			||||||
 | 
						receiver1 := types.BytesToInternalEVMAddress(app.RandomAddress().Bytes())
 | 
				
			||||||
 | 
						receiver2 := types.BytesToInternalEVMAddress(app.RandomAddress().Bytes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var contractAddress types.InternalEVMAddress
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						caller, key := testutil.RandomEvmAccount()
 | 
				
			||||||
 | 
						query := func(method string, args ...interface{}) ([]interface{}, error) {
 | 
				
			||||||
 | 
							return suite.QueryContract(
 | 
				
			||||||
 | 
								types.ERC20KavaWrappedCosmosCoinContract.ABI,
 | 
				
			||||||
 | 
								caller,
 | 
				
			||||||
 | 
								key,
 | 
				
			||||||
 | 
								contractAddress,
 | 
				
			||||||
 | 
								method,
 | 
				
			||||||
 | 
								args...,
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						checkTotalSupply := func(expectedSupply sdkmath.Int) {
 | 
				
			||||||
 | 
							res, err := query("totalSupply")
 | 
				
			||||||
 | 
							suite.NoError(err)
 | 
				
			||||||
 | 
							suite.Len(res, 1)
 | 
				
			||||||
 | 
							suite.BigIntsEqual(expectedSupply.BigInt(), res[0].(*big.Int), "unexpected total supply")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						checkBalanceOf := func(address types.InternalEVMAddress, expectedBalance sdkmath.Int) {
 | 
				
			||||||
 | 
							res, err := query("balanceOf", address.Address)
 | 
				
			||||||
 | 
							suite.NoError(err)
 | 
				
			||||||
 | 
							suite.Len(res, 1)
 | 
				
			||||||
 | 
							suite.BigIntsEqual(expectedBalance.BigInt(), res[0].(*big.Int), fmt.Sprintf("unexpected balanceOf for %s", address))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.Run("fails when denom not allowed", func() {
 | 
				
			||||||
 | 
							suite.denomContractNotRegistered(allowedDenom)
 | 
				
			||||||
 | 
							err := suite.Keeper.ConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
								suite.Ctx,
 | 
				
			||||||
 | 
								initiator,
 | 
				
			||||||
 | 
								receiver1,
 | 
				
			||||||
 | 
								sdk.NewCoin(allowedDenom, sdkmath.NewInt(6e8)),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							suite.ErrorContains(err, "sdk.Coin not enabled to convert to ERC20 token")
 | 
				
			||||||
 | 
							suite.denomContractNotRegistered(allowedDenom)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.Run("allowed denoms have contract deploys on first conversion", func() {
 | 
				
			||||||
 | 
							// make the denom allowed for conversion
 | 
				
			||||||
 | 
							params := suite.Keeper.GetParams(suite.Ctx)
 | 
				
			||||||
 | 
							params.AllowedCosmosDenoms = types.NewAllowedCosmosCoinERC20Tokens(
 | 
				
			||||||
 | 
								types.NewAllowedCosmosCoinERC20Token(allowedDenom, "Kava EVM Atom", "ATOM", 6),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							suite.Keeper.SetParams(suite.Ctx, params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fund account
 | 
				
			||||||
 | 
							err := suite.App.FundAccount(suite.Ctx, initiator, sdk.NewCoins(initialFunding))
 | 
				
			||||||
 | 
							suite.NoError(err, "failed to initially fund account")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// first conversion
 | 
				
			||||||
 | 
							err = suite.Keeper.ConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
								suite.Ctx,
 | 
				
			||||||
 | 
								initiator,
 | 
				
			||||||
 | 
								receiver1,
 | 
				
			||||||
 | 
								sdk.NewCoin(allowedDenom, sdkmath.NewInt(6e8)),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// contract should be deployed & registered
 | 
				
			||||||
 | 
							contractAddress = suite.denomContractRegistered(allowedDenom)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// sdk coin deducted from initiator
 | 
				
			||||||
 | 
							expectedBalance := initialFunding.Sub(amount)
 | 
				
			||||||
 | 
							balance := suite.BankKeeper.GetBalance(suite.Ctx, initiator, allowedDenom)
 | 
				
			||||||
 | 
							suite.Equal(expectedBalance, balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// erc20 minted to receiver
 | 
				
			||||||
 | 
							checkBalanceOf(receiver1, amount.Amount)
 | 
				
			||||||
 | 
							// total supply of erc20 should have increased
 | 
				
			||||||
 | 
							checkTotalSupply(amount.Amount)
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.Run("2nd deploy uses same contract", func() {
 | 
				
			||||||
 | 
							// expect no initial balance
 | 
				
			||||||
 | 
							checkBalanceOf(receiver2, sdkmath.NewInt(0))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// 2nd conversion
 | 
				
			||||||
 | 
							err := suite.Keeper.ConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
								suite.Ctx,
 | 
				
			||||||
 | 
								initiator,
 | 
				
			||||||
 | 
								receiver2,
 | 
				
			||||||
 | 
								sdk.NewCoin(allowedDenom, sdkmath.NewInt(6e8)),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// contract address should not change
 | 
				
			||||||
 | 
							convertTwiceContractAddress := suite.denomContractRegistered(allowedDenom)
 | 
				
			||||||
 | 
							suite.Equal(contractAddress, convertTwiceContractAddress)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// sdk coin deducted from initiator
 | 
				
			||||||
 | 
							expectedBalance := initialFunding.Sub(amount).Sub(amount)
 | 
				
			||||||
 | 
							balance := suite.BankKeeper.GetBalance(suite.Ctx, initiator, allowedDenom)
 | 
				
			||||||
 | 
							suite.Equal(expectedBalance, balance)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// erc20 minted to receiver
 | 
				
			||||||
 | 
							checkBalanceOf(receiver2, amount.Amount)
 | 
				
			||||||
 | 
							// total supply of erc20 should have increased
 | 
				
			||||||
 | 
							checkTotalSupply(amount.Amount.MulRaw(2))
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -118,5 +118,28 @@ func (s msgServer) ConvertCosmosCoinToERC20(
 | 
				
			|||||||
	goCtx context.Context,
 | 
						goCtx context.Context,
 | 
				
			||||||
	msg *types.MsgConvertCosmosCoinToERC20,
 | 
						msg *types.MsgConvertCosmosCoinToERC20,
 | 
				
			||||||
) (*types.MsgConvertCosmosCoinToERC20Response, error) {
 | 
					) (*types.MsgConvertCosmosCoinToERC20Response, error) {
 | 
				
			||||||
	return nil, fmt.Errorf("unimplemented - coming soon")
 | 
						ctx := sdk.UnwrapSDKContext(goCtx)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						initiator, err := sdk.AccAddressFromBech32(msg.Initiator)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("invalid initiator address: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						receiver, err := types.NewInternalEVMAddressFromString(msg.Receiver)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("invalid receiver address: %w", err)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.keeper.ConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
							ctx,
 | 
				
			||||||
 | 
							initiator,
 | 
				
			||||||
 | 
							receiver,
 | 
				
			||||||
 | 
							*msg.Amount,
 | 
				
			||||||
 | 
						); err != nil {
 | 
				
			||||||
 | 
							return nil, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: emit message event
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &types.MsgConvertCosmosCoinToERC20Response{}, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -9,8 +9,10 @@ import (
 | 
				
			|||||||
	sdkmath "cosmossdk.io/math"
 | 
						sdkmath "cosmossdk.io/math"
 | 
				
			||||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
						sdk "github.com/cosmos/cosmos-sdk/types"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/ethereum/go-ethereum/common"
 | 
				
			||||||
	"github.com/ethereum/go-ethereum/common/math"
 | 
						"github.com/ethereum/go-ethereum/common/math"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/kava-labs/kava/app"
 | 
				
			||||||
	"github.com/kava-labs/kava/x/evmutil/keeper"
 | 
						"github.com/kava-labs/kava/x/evmutil/keeper"
 | 
				
			||||||
	"github.com/kava-labs/kava/x/evmutil/testutil"
 | 
						"github.com/kava-labs/kava/x/evmutil/testutil"
 | 
				
			||||||
	"github.com/kava-labs/kava/x/evmutil/types"
 | 
						"github.com/kava-labs/kava/x/evmutil/types"
 | 
				
			||||||
@ -268,3 +270,225 @@ func (suite *MsgServerSuite) TestConvertERC20ToCoin() {
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *MsgServerSuite) TestConvertCosmosCoinToERC20_InitialContractDeploy() {
 | 
				
			||||||
 | 
						allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
 | 
				
			||||||
 | 
						initialFunding := int64(1e10)
 | 
				
			||||||
 | 
						fundedAccount := app.RandomAddress()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						setup := func() {
 | 
				
			||||||
 | 
							suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// make the denom allowed for conversion
 | 
				
			||||||
 | 
							params := suite.Keeper.GetParams(suite.Ctx)
 | 
				
			||||||
 | 
							params.AllowedCosmosDenoms = types.NewAllowedCosmosCoinERC20Tokens(
 | 
				
			||||||
 | 
								types.NewAllowedCosmosCoinERC20Token(allowedDenom, "Kava EVM Atom", "ATOM", 6),
 | 
				
			||||||
 | 
							)
 | 
				
			||||||
 | 
							suite.Keeper.SetParams(suite.Ctx, params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// fund account
 | 
				
			||||||
 | 
							err := suite.App.FundAccount(suite.Ctx, fundedAccount, sdk.NewCoins(
 | 
				
			||||||
 | 
								sdk.NewInt64Coin(allowedDenom, initialFunding),
 | 
				
			||||||
 | 
							))
 | 
				
			||||||
 | 
							suite.NoError(err, "failed to initially fund account")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						testCases := []struct {
 | 
				
			||||||
 | 
							name            string
 | 
				
			||||||
 | 
							msg             types.MsgConvertCosmosCoinToERC20
 | 
				
			||||||
 | 
							amountConverted sdkmath.Int
 | 
				
			||||||
 | 
							expectedErr     string
 | 
				
			||||||
 | 
						}{
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "valid - first conversion deploys contract, send to self",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									fundedAccount.String(),
 | 
				
			||||||
 | 
									common.BytesToAddress(fundedAccount.Bytes()).Hex(), // it's me!
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, 5e7),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								amountConverted: sdkmath.NewInt(5e7),
 | 
				
			||||||
 | 
								expectedErr:     "",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "valid - first conversion deploys contract, send to other",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									fundedAccount.String(),
 | 
				
			||||||
 | 
									testutil.RandomEvmAddress().Hex(), // someone else!
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, 9993317),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								amountConverted: sdkmath.NewInt(9993317),
 | 
				
			||||||
 | 
								expectedErr:     "",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid - un-allowed denom",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									app.RandomAddress().String(),
 | 
				
			||||||
 | 
									testutil.RandomEvmAddress().Hex(),
 | 
				
			||||||
 | 
									sdk.NewInt64Coin("not-allowed-denom", 1e4),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								expectedErr: "sdk.Coin not enabled to convert to ERC20 token",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid - bad initiator",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									"invalid-kava-address",
 | 
				
			||||||
 | 
									testutil.RandomEvmAddress().Hex(),
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, 1e4),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								expectedErr: "invalid initiator address",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid - bad receiver",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									app.RandomAddress().String(),
 | 
				
			||||||
 | 
									"invalid-0x-address",
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, 1e4),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								expectedErr: "invalid receiver address",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid - bad receiver",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									app.RandomAddress().String(),
 | 
				
			||||||
 | 
									"invalid-0x-address",
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, 1e4),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								expectedErr: "invalid receiver address",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							{
 | 
				
			||||||
 | 
								name: "invalid - insufficient balance",
 | 
				
			||||||
 | 
								msg: types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
									fundedAccount.String(),
 | 
				
			||||||
 | 
									testutil.RandomEvmAddress().Hex(),
 | 
				
			||||||
 | 
									sdk.NewInt64Coin(allowedDenom, initialFunding+1),
 | 
				
			||||||
 | 
								),
 | 
				
			||||||
 | 
								expectedErr: "insufficient funds",
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							// NOTE: a zero amount tx passes in this scope but will fail to pass ValidateBasic()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, tc := range testCases {
 | 
				
			||||||
 | 
							suite.Run(tc.name, func() {
 | 
				
			||||||
 | 
								// initial setup
 | 
				
			||||||
 | 
								setup()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								moduleBalanceBefore := suite.ModuleBalance(allowedDenom)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// submit message
 | 
				
			||||||
 | 
								_, err := suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &tc.msg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// verify error, if expected
 | 
				
			||||||
 | 
								if tc.expectedErr != "" {
 | 
				
			||||||
 | 
									suite.ErrorContains(err, tc.expectedErr)
 | 
				
			||||||
 | 
									// the contract wasn't previously deployed, so still shouldn't be
 | 
				
			||||||
 | 
									_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, allowedDenom)
 | 
				
			||||||
 | 
									suite.False(found)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// verify success
 | 
				
			||||||
 | 
								suite.NoError(err)
 | 
				
			||||||
 | 
								suite.Commit()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								initiator := sdk.MustAccAddressFromBech32(tc.msg.Initiator)
 | 
				
			||||||
 | 
								receiver := testutil.MustNewInternalEVMAddressFromString(tc.msg.Receiver)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// initiator no longer has sdk coins
 | 
				
			||||||
 | 
								cosmosBalanceAfter := suite.BankKeeper.GetBalance(suite.Ctx, initiator, allowedDenom)
 | 
				
			||||||
 | 
								suite.Equal(
 | 
				
			||||||
 | 
									sdkmath.NewInt(initialFunding).Sub(tc.amountConverted),
 | 
				
			||||||
 | 
									cosmosBalanceAfter.Amount,
 | 
				
			||||||
 | 
									"unexpected sdk.Coin balance of initiator",
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// sdk coins are locked into module
 | 
				
			||||||
 | 
								moduleBalanceAfter := suite.ModuleBalance(allowedDenom)
 | 
				
			||||||
 | 
								suite.Equal(
 | 
				
			||||||
 | 
									moduleBalanceBefore.Add(tc.amountConverted),
 | 
				
			||||||
 | 
									moduleBalanceAfter,
 | 
				
			||||||
 | 
									"unexpected module balance",
 | 
				
			||||||
 | 
								)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// deployed contract address is registered in module store
 | 
				
			||||||
 | 
								contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, allowedDenom)
 | 
				
			||||||
 | 
								suite.True(found, "expected deployed contract address to be registered, found none")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// receiver has been minted correct number of tokens
 | 
				
			||||||
 | 
								erc20Balance, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, receiver)
 | 
				
			||||||
 | 
								suite.NoError(err)
 | 
				
			||||||
 | 
								suite.Equal(tc.amountConverted.BigInt(), erc20Balance, "unexpected erc20 balance for receiver")
 | 
				
			||||||
 | 
							})
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (suite *MsgServerSuite) TestConvertCosmosCoinToERC20_AlreadyDeployedContract() {
 | 
				
			||||||
 | 
						allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
 | 
				
			||||||
 | 
						initialFunding := int64(1e10)
 | 
				
			||||||
 | 
						fundedAccount := app.RandomAddress()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						amount := sdkmath.NewInt(6e8)
 | 
				
			||||||
 | 
						receiver1 := types.BytesToInternalEVMAddress(app.RandomAddress().Bytes())
 | 
				
			||||||
 | 
						receiver2 := types.BytesToInternalEVMAddress(app.RandomAddress().Bytes())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						suite.SetupTest()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// make the denom allowed for conversion
 | 
				
			||||||
 | 
						params := suite.Keeper.GetParams(suite.Ctx)
 | 
				
			||||||
 | 
						params.AllowedCosmosDenoms = types.NewAllowedCosmosCoinERC20Tokens(
 | 
				
			||||||
 | 
							types.NewAllowedCosmosCoinERC20Token(allowedDenom, "Kava EVM Atom", "ATOM", 6),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.Keeper.SetParams(suite.Ctx, params)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// fund account
 | 
				
			||||||
 | 
						err := suite.App.FundAccount(suite.Ctx, fundedAccount, sdk.NewCoins(
 | 
				
			||||||
 | 
							sdk.NewInt64Coin(allowedDenom, initialFunding),
 | 
				
			||||||
 | 
						))
 | 
				
			||||||
 | 
						suite.NoError(err, "failed to initially fund account")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// verify contract is not deployed
 | 
				
			||||||
 | 
						_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, allowedDenom)
 | 
				
			||||||
 | 
						suite.False(found)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// initial convert deploys contract
 | 
				
			||||||
 | 
						msg := types.NewMsgConvertCosmosCoinToERC20(
 | 
				
			||||||
 | 
							fundedAccount.String(),
 | 
				
			||||||
 | 
							receiver1.Hex(),
 | 
				
			||||||
 | 
							sdk.NewCoin(allowedDenom, amount),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						_, err = suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &msg)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, allowedDenom)
 | 
				
			||||||
 | 
						suite.True(found)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// second convert uses same contract
 | 
				
			||||||
 | 
						msg.Receiver = receiver2.Hex()
 | 
				
			||||||
 | 
						_, err = suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &msg)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						after2ndUseAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, allowedDenom)
 | 
				
			||||||
 | 
						suite.True(found)
 | 
				
			||||||
 | 
						suite.Equal(contractAddress, after2ndUseAddress, "contract address should remain the same")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check balances
 | 
				
			||||||
 | 
						bal1, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, receiver1)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.Equal(amount.BigInt(), bal1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						bal2, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, receiver2)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.Equal(amount.BigInt(), bal2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check total supply
 | 
				
			||||||
 | 
						caller, key := testutil.RandomEvmAccount()
 | 
				
			||||||
 | 
						totalSupply, err := suite.QueryContract(
 | 
				
			||||||
 | 
							types.ERC20KavaWrappedCosmosCoinContract.ABI,
 | 
				
			||||||
 | 
							caller,
 | 
				
			||||||
 | 
							key,
 | 
				
			||||||
 | 
							contractAddress,
 | 
				
			||||||
 | 
							"totalSupply",
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						suite.NoError(err)
 | 
				
			||||||
 | 
						suite.Len(totalSupply, 1)
 | 
				
			||||||
 | 
						suite.Equal(amount.MulRaw(2).BigInt(), totalSupply[0].(*big.Int))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -44,7 +44,7 @@ func (k Keeper) GetEnabledConversionPairFromERC20Address(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return types.ConversionPair{}, errorsmod.Wrap(types.ErrConversionNotEnabled, address.String())
 | 
						return types.ConversionPair{}, errorsmod.Wrap(types.ErrEVMConversionNotEnabled, address.String())
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetEnabledConversionPairFromDenom returns an ConversionPair from the sdk.Coin denom.
 | 
					// GetEnabledConversionPairFromDenom returns an ConversionPair from the sdk.Coin denom.
 | 
				
			||||||
@ -59,5 +59,5 @@ func (k Keeper) GetEnabledConversionPairFromDenom(
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return types.ConversionPair{}, errorsmod.Wrap(types.ErrConversionNotEnabled, denom)
 | 
						return types.ConversionPair{}, errorsmod.Wrap(types.ErrEVMConversionNotEnabled, denom)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -34,9 +34,33 @@ The swap logic ensures that all `akava` is backed by the equivalent `ukava` bala
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## ERC20 token <> sdk.Coin Conversion
 | 
					## ERC20 token <> sdk.Coin Conversion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`x/evmutil` enables the conversion between ERC20 tokens and sdk.Coins. This done through the use of the `MsgConvertERC20ToCoin` & `MsgConvertCoinToERC20` messages (see **[Messages](03_messages.md)**).
 | 
					`x/evmutil` facilitates moving assets between Kava's EVM and Cosmos co-chains. This must be handled differently depending on which co-chain to which the asset it native. The messages controlling these flows involve two accounts:
 | 
				
			||||||
 | 
					1. The _initiator_ who sends coins from their co-chain
 | 
				
			||||||
 | 
					2. The _receiver_ who receives coins on the other co-chain
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Only ERC20 contract address that are whitelist via the `EnabledConversionPairs` param (see **[Params](05_params.md)**) can be converted via these messages.
 | 
					When converting assets from the EVM to the Cosmos co-chain, the initiator is an 0x EVM address and the receiver is a `kava1` Bech32 address.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					When converting assets from the Cosmos co-chain to the EVM, the initiator is a `kava1` Bech32 address and the receiver is an 0x EVM address.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Cosmos-Native Assets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`sdk.Coin`s native to the Cosmos co-chain can be converted to an ERC-20 representing the coin in the EVM. This works by transferring the coin from the initiator to `x/evmutil`'s module account and then minting an ERC-20 token to the receiver. Converting back, the initiator's ERC-20 representation of the coin is burned and the original Cosmos-native asset is transferred to the receiver.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Cosmos-native asset converstion is done through the use of the `MsgConvertCosmosCoinToERC20` & `MsgConvertCosmosCoinFromERC20` messages (see **[Messages](03_messages.md)**).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Only Cosmos co-chain denominations that are in the `AllowedCosmosDenoms` param (see **[Params](05_params.md)**) can be converted via these messages.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`AllowedCosmosDenoms` can be altered through governance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### EVM-Native Assets
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ERC-20 tokens native to the EVM can be converted into an `sdk.Coin` in the Cosmos ecosystem. This works by transferring the tokens to `x/evmutil`'s module account and then minting an `sdk.Coin` to the receiver. Converting back is the inverse: the `sdk.Coin` of the initiator is burned and the original ERC-20 tokens that were locked into the module account are transferred back to the receiver.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					EVM-native asset conversion is done through the use of the `MsgConvertERC20ToCoin` & `MsgConvertCoinToERC20` messages (see **[Messages](03_messages.md)**).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Only ERC20 contract address that are in the `EnabledConversionPairs` param (see **[Params](05_params.md)**) can be converted via these messages.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`EnabledConversionPairs` can be altered through governance.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Module Keeper
 | 
					## Module Keeper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -6,9 +6,39 @@ order: 3
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Users can submit various messages to the evmutil module which trigger state changes detailed below.
 | 
					Users can submit various messages to the evmutil module which trigger state changes detailed below.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## MsgConvertCosmosCoinToERC20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					`MsgConvertCosmosCoinToERC20` converts an sdk.Coin to an ERC20. This message is for moving Cosmos-native assets from the Cosmos ecosystem to the EVM.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Upon first conversion, the message also deploys the ERC20 contract that will represent the cosmos-sdk asset in the EVM. The contract is owned by the `x/evmutil` module.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```proto
 | 
				
			||||||
 | 
					service Msg {
 | 
				
			||||||
 | 
					  // ConvertCosmosCoinToERC20 defines a method for converting a cosmos sdk.Coin to an ERC20.
 | 
				
			||||||
 | 
					  rpc ConvertCosmosCoinToERC20(MsgConvertCosmosCoinToERC20) returns (MsgConvertCosmosCoinToERC20Response);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20.
 | 
				
			||||||
 | 
					message MsgConvertCosmosCoinToERC20 {
 | 
				
			||||||
 | 
					  // Kava bech32 address initiating the conversion.
 | 
				
			||||||
 | 
					  string initiator = 1;
 | 
				
			||||||
 | 
					  // EVM hex address that will receive the ERC20 tokens.
 | 
				
			||||||
 | 
					  string receiver = 2;
 | 
				
			||||||
 | 
					  // Amount is the sdk.Coin amount to convert.
 | 
				
			||||||
 | 
					  cosmos.base.v1beta1.Coin amount = 3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### State Changes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- The `AllowedCosmosDenoms` param from `x/evmutil` is checked to ensure the conversion is allowed.
 | 
				
			||||||
 | 
					- The module's store is checked for the address of the deployed ERC20 contract. If none is found, a new contract is deployed and its address is saved to the module store.
 | 
				
			||||||
 | 
					- The `amount` is deducted from the `initiator`'s balance and transferred to the module account.
 | 
				
			||||||
 | 
					- An equivalent amount of ERC20 tokens are minted by `x/evmutil` to the `receiver`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## MsgConvertERC20ToCoin
 | 
					## MsgConvertERC20ToCoin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`MsgConvertCoinToERC20` converts a Kava ERC20 coin to sdk.Coin.
 | 
					`MsgConvertCoinToERC20` converts a Kava ERC20 coin to sdk.Coin. This message is for moving EVM-native assets from the EVM to the Cosmos ecosystem.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```protobuf
 | 
					```protobuf
 | 
				
			||||||
service Msg {
 | 
					service Msg {
 | 
				
			||||||
@ -39,7 +69,7 @@ message MsgConvertERC20ToCoin {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## MsgConvertCoinToERC20
 | 
					## MsgConvertCoinToERC20
 | 
				
			||||||
 | 
					
 | 
				
			||||||
`MsgConvertCoinToERC20` converts sdk.Coin to Kava ERC20.
 | 
					`MsgConvertCoinToERC20` converts sdk.Coin to Kava ERC20. This message is for moving EVM-native assets from the Cosmos ecosystem back to the EVM.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```protobuf
 | 
					```protobuf
 | 
				
			||||||
service Msg {
 | 
					service Msg {
 | 
				
			||||||
 | 
				
			|||||||
@ -20,12 +20,12 @@ Example parameters for `ConversionPair`:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Example parameters for `AllowedCosmosCoinERC20Token`:
 | 
					Example parameters for `AllowedCosmosCoinERC20Token`:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| Key          | Type   | Example                                                                | Description                                        |
 | 
					| Key          | Type   | Example                                                                | Description                                         |
 | 
				
			||||||
| ------------ | ------ | ---------------------------------------------------------------------- | -------------------------------------------------- |
 | 
					| ------------ | ------ | ---------------------------------------------------------------------- | --------------------------------------------------- |
 | 
				
			||||||
| cosmos_denom | string | "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" | denom of the sdk.Coin                              |
 | 
					| cosmos_denom | string | "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" | denom of the sdk.Coin                               |
 | 
				
			||||||
| name         | string | "Kava-wrapped Atom"                                                    | name field of the erc20 token                      |
 | 
					| name         | string | "Kava-wrapped Atom"                                                    | name field of the erc20 token                       |
 | 
				
			||||||
| symbol       | string | "kATOM"                                                                | symbol field of the erc20 token                    |
 | 
					| symbol       | string | "kATOM"                                                                | symbol field of the erc20 token                     |
 | 
				
			||||||
| decimal      | uint32 | 6                                                                      | decimal field of the erc20 token, for display only |
 | 
					| decimals     | uint32 | 6                                                                      | decimals field of the erc20 token, for display only |
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## EnabledConversionPairs
 | 
					## EnabledConversionPairs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -386,6 +386,11 @@ func (suite *Suite) EventsDoNotContain(events sdk.Events, eventType string) {
 | 
				
			|||||||
	suite.Falsef(foundMatch, "event of type %s should not be found, but was found", eventType)
 | 
						suite.Falsef(foundMatch, "event of type %s should not be found, but was found", eventType)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// BigIntsEqual is a helper method for comparing the equality of two big ints
 | 
				
			||||||
 | 
					func (suite *Suite) BigIntsEqual(expected *big.Int, actual *big.Int, msg string) {
 | 
				
			||||||
 | 
						suite.Truef(expected.Cmp(actual) == 0, "%s (expected: %s, actual: %s)", msg, expected.String(), actual.String())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func attrsToMap(attrs []abci.EventAttribute) []sdk.Attribute {
 | 
					func attrsToMap(attrs []abci.EventAttribute) []sdk.Attribute {
 | 
				
			||||||
	out := []sdk.Attribute{}
 | 
						out := []sdk.Attribute{}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,6 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
 | 
				
			|||||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertCoinToERC20{}, "evmutil/MsgConvertCoinToERC20")
 | 
						legacy.RegisterAminoMsg(cdc, &MsgConvertCoinToERC20{}, "evmutil/MsgConvertCoinToERC20")
 | 
				
			||||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertERC20ToCoin{}, "evmutil/MsgConvertERC20ToCoin")
 | 
						legacy.RegisterAminoMsg(cdc, &MsgConvertERC20ToCoin{}, "evmutil/MsgConvertERC20ToCoin")
 | 
				
			||||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertCosmosCoinToERC20{}, "evmutil/MsgConvertCosmosCoinToERC20")
 | 
						legacy.RegisterAminoMsg(cdc, &MsgConvertCosmosCoinToERC20{}, "evmutil/MsgConvertCosmosCoinToERC20")
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
 | 
					func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,7 @@ import errorsmod "cosmossdk.io/errors"
 | 
				
			|||||||
var (
 | 
					var (
 | 
				
			||||||
	ErrABIPack                 = errorsmod.Register(ModuleName, 2, "contract ABI pack failed")
 | 
						ErrABIPack                 = errorsmod.Register(ModuleName, 2, "contract ABI pack failed")
 | 
				
			||||||
	ErrEVMCall                 = errorsmod.Register(ModuleName, 3, "EVM call unexpected error")
 | 
						ErrEVMCall                 = errorsmod.Register(ModuleName, 3, "EVM call unexpected error")
 | 
				
			||||||
	ErrConversionNotEnabled    = errorsmod.Register(ModuleName, 4, "ERC20 token not enabled to convert to sdk.Coin")
 | 
						ErrEVMConversionNotEnabled = errorsmod.Register(ModuleName, 4, "ERC20 token not enabled to convert to sdk.Coin")
 | 
				
			||||||
	ErrBalanceInvariance       = errorsmod.Register(ModuleName, 5, "post EVM transfer balance invariant failed")
 | 
						ErrBalanceInvariance       = errorsmod.Register(ModuleName, 5, "post EVM transfer balance invariant failed")
 | 
				
			||||||
	ErrUnexpectedContractEvent = errorsmod.Register(ModuleName, 6, "unexpected contract event")
 | 
						ErrUnexpectedContractEvent = errorsmod.Register(ModuleName, 6, "unexpected contract event")
 | 
				
			||||||
	ErrInvalidCosmosDenom      = errorsmod.Register(ModuleName, 7, "invalid cosmos denom")
 | 
						ErrInvalidCosmosDenom      = errorsmod.Register(ModuleName, 7, "invalid cosmos denom")
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user