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:
Robert Pirtle 2023-05-30 12:00:01 -07:00 committed by GitHub
parent 1459170a37
commit 741f1e42ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 565 additions and 16 deletions

View File

@ -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

View File

@ -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)
},
}
}

View 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
}

View 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))
})
}

View File

@ -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
} }

View File

@ -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))
}

View File

@ -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)
} }

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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{}

View File

@ -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) {

View File

@ -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")