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) [#1591] & [#1596] Configure module to support deploying ERC20KavaWrappedCosmosCoin contracts
 | 
			
		||||
- (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]
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
  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
 | 
			
		||||
[#1596]: https://github.com/Kava-Labs/kava/pull/1596
 | 
			
		||||
[#1591]: https://github.com/Kava-Labs/kava/pull/1591
 | 
			
		||||
 | 
			
		||||
@ -30,6 +30,7 @@ func GetTxCmd() *cobra.Command {
 | 
			
		||||
	cmds := []*cobra.Command{
 | 
			
		||||
		getCmdMsgConvertCoinToERC20(),
 | 
			
		||||
		getCmdConvertERC20ToCoin(),
 | 
			
		||||
		getCmdMsgConvertCosmosCoinToERC20(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, cmd := range cmds {
 | 
			
		||||
@ -43,7 +44,7 @@ func GetTxCmd() *cobra.Command {
 | 
			
		||||
 | 
			
		||||
func getCmdMsgConvertCoinToERC20() *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",
 | 
			
		||||
		Example: fmt.Sprintf(
 | 
			
		||||
			`%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,
 | 
			
		||||
	msg *types.MsgConvertCosmosCoinToERC20,
 | 
			
		||||
) (*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"
 | 
			
		||||
	sdk "github.com/cosmos/cosmos-sdk/types"
 | 
			
		||||
 | 
			
		||||
	"github.com/ethereum/go-ethereum/common"
 | 
			
		||||
	"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/testutil"
 | 
			
		||||
	"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.
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
`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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -6,9 +6,39 @@ order: 3
 | 
			
		||||
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
`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
 | 
			
		||||
service Msg {
 | 
			
		||||
@ -39,7 +69,7 @@ message MsgConvertERC20ToCoin {
 | 
			
		||||
 | 
			
		||||
## 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
 | 
			
		||||
service Msg {
 | 
			
		||||
 | 
			
		||||
@ -21,11 +21,11 @@ Example parameters for `ConversionPair`:
 | 
			
		||||
Example parameters for `AllowedCosmosCoinERC20Token`:
 | 
			
		||||
 | 
			
		||||
| Key          | Type   | Example                                                                | Description                                         |
 | 
			
		||||
| ------------ | ------ | ---------------------------------------------------------------------- | -------------------------------------------------- |
 | 
			
		||||
| ------------ | ------ | ---------------------------------------------------------------------- | --------------------------------------------------- |
 | 
			
		||||
| cosmos_denom | string | "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" | denom of the sdk.Coin                               |
 | 
			
		||||
| name         | string | "Kava-wrapped Atom"                                                    | name 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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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 {
 | 
			
		||||
	out := []sdk.Attribute{}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
 | 
			
		||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertCoinToERC20{}, "evmutil/MsgConvertCoinToERC20")
 | 
			
		||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertERC20ToCoin{}, "evmutil/MsgConvertERC20ToCoin")
 | 
			
		||||
	legacy.RegisterAminoMsg(cdc, &MsgConvertCosmosCoinToERC20{}, "evmutil/MsgConvertCosmosCoinToERC20")
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RegisterInterfaces(registry cdctypes.InterfaceRegistry) {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import errorsmod "cosmossdk.io/errors"
 | 
			
		||||
var (
 | 
			
		||||
	ErrABIPack                 = errorsmod.Register(ModuleName, 2, "contract ABI pack failed")
 | 
			
		||||
	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")
 | 
			
		||||
	ErrUnexpectedContractEvent = errorsmod.Register(ModuleName, 6, "unexpected contract event")
 | 
			
		||||
	ErrInvalidCosmosDenom      = errorsmod.Register(ModuleName, 7, "invalid cosmos denom")
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user