feat(evmutil): implement MsgConvertCosmosCoinFromERC20 (#1609)

* first pass at convert cosmos coin -> evm msg

* test ConvertCosmosCoinFromERC20 method

* test message server for MsgConvertCosmosCoinFromERC20

* update spec to include MsgConvertCosmosCoinFromERC20

* update changelog

* add CLI command for convert-cosmos-coin-from-erc20

* add test of removed/re-enable denom for convert
This commit is contained in:
Robert Pirtle 2023-06-02 14:26:51 -07:00 committed by GitHub
parent d988330d7f
commit f4b8bf8f07
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 545 additions and 16 deletions

View File

@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (evmutil) [#1603] Add MsgConvertCosmosCoinToERC20 for converting an sdk.Coin to an ERC20 in the EVM - (evmutil) [#1603] Add MsgConvertCosmosCoinToERC20 for converting an sdk.Coin to an ERC20 in the EVM
- (evmutil) [#1604] Emit events for MsgConvertCosmosCoinToERC20: `message` & `convert_cosmos_coin_to_erc20` - (evmutil) [#1604] Emit events for MsgConvertCosmosCoinToERC20: `message` & `convert_cosmos_coin_to_erc20`
- (evmutil) [#1605] Add query for deployed ERC20 contracts representing Cosmos coins in the EVM - (evmutil) [#1605] Add query for deployed ERC20 contracts representing Cosmos coins in the EVM
- (evmutil) [#1609] Add MsgConvertCosmosCoinFromERC20 for converting the ERC20 back to an sdk.Coin
### Client Breaking ### Client Breaking
- (evmutil) [#1603] Renamed error `ErrConversionNotEnabled` to `ErrEVMConversionNotEnabled` - (evmutil) [#1603] Renamed error `ErrConversionNotEnabled` to `ErrEVMConversionNotEnabled`
@ -251,6 +252,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
[#1609]: https://github.com/Kava-Labs/kava/pull/1609
[#1605]: https://github.com/Kava-Labs/kava/pull/1605 [#1605]: https://github.com/Kava-Labs/kava/pull/1605
[#1604]: https://github.com/Kava-Labs/kava/pull/1604 [#1604]: https://github.com/Kava-Labs/kava/pull/1604
[#1603]: https://github.com/Kava-Labs/kava/pull/1603 [#1603]: https://github.com/Kava-Labs/kava/pull/1603

View File

@ -3914,7 +3914,7 @@ MsgConvertCoinToERC20Response defines the response value from Msg/ConvertCoinToE
<a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinFromERC20"></a> <a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinFromERC20"></a>
### MsgConvertCosmosCoinFromERC20 ### MsgConvertCosmosCoinFromERC20
ConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets. MsgConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets.
| Field | Type | Label | Description | | Field | Type | Label | Description |
@ -3941,7 +3941,7 @@ MsgConvertCosmosCoinFromERC20Response defines the response value from Msg/MsgCon
<a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinToERC20"></a> <a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinToERC20"></a>
### MsgConvertCosmosCoinToERC20 ### MsgConvertCosmosCoinToERC20
ConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets. MsgConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets.
| Field | Type | Label | Description | | Field | Type | Label | Description |

View File

@ -57,7 +57,7 @@ message MsgConvertERC20ToCoin {
// Msg/MsgConvertERC20ToCoin. // Msg/MsgConvertERC20ToCoin.
message MsgConvertERC20ToCoinResponse {} message MsgConvertERC20ToCoinResponse {}
// ConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets. // MsgConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets.
message MsgConvertCosmosCoinToERC20 { message MsgConvertCosmosCoinToERC20 {
// Kava bech32 address initiating the conversion. // Kava bech32 address initiating the conversion.
string initiator = 1; string initiator = 1;
@ -70,7 +70,7 @@ message MsgConvertCosmosCoinToERC20 {
// MsgConvertCosmosCoinToERC20Response defines the response value from Msg/MsgConvertCosmosCoinToERC20. // MsgConvertCosmosCoinToERC20Response defines the response value from Msg/MsgConvertCosmosCoinToERC20.
message MsgConvertCosmosCoinToERC20Response {} message MsgConvertCosmosCoinToERC20Response {}
// ConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets. // MsgConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets.
message MsgConvertCosmosCoinFromERC20 { message MsgConvertCosmosCoinFromERC20 {
// EVM hex address initiating the conversion. // EVM hex address initiating the conversion.
string initiator = 1; string initiator = 1;

View File

@ -31,6 +31,7 @@ func GetTxCmd() *cobra.Command {
getCmdMsgConvertCoinToERC20(), getCmdMsgConvertCoinToERC20(),
getCmdConvertERC20ToCoin(), getCmdConvertERC20ToCoin(),
getCmdMsgConvertCosmosCoinToERC20(), getCmdMsgConvertCosmosCoinToERC20(),
getCmdMsgConvertCosmosCoinFromERC20(),
} }
for _, cmd := range cmds { for _, cmd := range cmds {
@ -159,3 +160,42 @@ func getCmdMsgConvertCosmosCoinToERC20() *cobra.Command {
}, },
} }
} }
func getCmdMsgConvertCosmosCoinFromERC20() *cobra.Command {
return &cobra.Command{
Use: "convert-cosmos-coin-from-erc20 [receiver_kava_address] [amount] [flags]",
Short: "converts asset native to Cosmos Co-chain back from an ERC20 on the EVM co-chain",
Example: fmt.Sprintf(
`Convert ERC20 representation of 500 ATOM back to a Cosmos coin, sending to kava1q0dkky0505r555etn6u2nz4h4kjcg5y8dg863a:
%s tx %s convert-cosmos-coin-from-erc20 kava1q0dkky0505r555etn6u2nz4h4kjcg5y8dg863a 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, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return fmt.Errorf("receiver '%s' is an invalid kava address", args[0])
}
amount, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return err
}
signer := clientCtx.GetFromAddress()
initiator := common.BytesToAddress(signer.Bytes())
msg := types.NewMsgConvertCosmosCoinFromERC20(initiator.String(), receiver.String(), amount)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg)
},
}
}

View File

@ -1,8 +1,11 @@
package keeper package keeper
import ( import (
"fmt"
errorsmod "cosmossdk.io/errors" errorsmod "cosmossdk.io/errors"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/evmutil/types" "github.com/kava-labs/kava/x/evmutil/types"
) )
@ -54,3 +57,51 @@ func (k *Keeper) ConvertCosmosCoinToERC20(
return nil return nil
} }
// ConvertCosmosCoinFromERC20 burns the ERC20 wrapper of the cosmos coin and
// sends the underlying sdk coin form the module account to the receiver.
func (k *Keeper) ConvertCosmosCoinFromERC20(
ctx sdk.Context,
initiator types.InternalEVMAddress,
receiver sdk.AccAddress,
coin sdk.Coin,
) error {
amount := coin.Amount.BigInt()
// get deployed contract
contractAddress, found := k.GetDeployedCosmosCoinContract(ctx, coin.Denom)
if !found {
// no contract deployed
return errorsmod.Wrapf(types.ErrInvalidCosmosDenom, fmt.Sprintf("no erc20 contract found for %s", coin.Denom))
}
// verify sufficient balance
balance, err := k.QueryERC20BalanceOf(ctx, contractAddress, initiator)
if err != nil {
return errorsmod.Wrapf(types.ErrEVMCall, "failed to retrieve balance %s", err.Error())
}
if balance.Cmp(amount) == -1 {
return errorsmod.Wrapf(sdkerrors.ErrInsufficientFunds, "failed to convert to cosmos coins")
}
// burn initiator's ERC20 tokens
err = k.BurnERC20(ctx, contractAddress, initiator, amount)
if err != nil {
return err
}
// send sdk coins to receiver, unlocking them from the module account
err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, receiver, sdk.NewCoins(coin))
if err != nil {
return err
}
ctx.EventManager().EmitEvent(sdk.NewEvent(
types.EventTypeConvertCosmosCoinFromERC20,
sdk.NewAttribute(types.AttributeKeyInitiator, initiator.String()),
sdk.NewAttribute(types.AttributeKeyReceiver, receiver.String()),
sdk.NewAttribute(types.AttributeKeyERC20Address, contractAddress.Hex()),
sdk.NewAttribute(types.AttributeKeyAmount, coin.String()),
))
return nil
}

View File

@ -15,29 +15,29 @@ import (
"github.com/kava-labs/kava/x/evmutil/types" "github.com/kava-labs/kava/x/evmutil/types"
) )
type ConversionCosmosNativeSuite struct { type convertCosmosCoinToERC20Suite struct {
testutil.Suite testutil.Suite
} }
func TestConversionCosmosNativeSuite(t *testing.T) { func TestConversionCosmosNativeToEvmSuite(t *testing.T) {
suite.Run(t, new(ConversionCosmosNativeSuite)) suite.Run(t, new(convertCosmosCoinToERC20Suite))
} }
// fail test if contract for denom not registered // fail test if contract for denom not registered
func (suite *ConversionCosmosNativeSuite) denomContractRegistered(denom string) types.InternalEVMAddress { func (suite *convertCosmosCoinToERC20Suite) denomContractRegistered(denom string) types.InternalEVMAddress {
contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom) contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(found) suite.True(found)
return contractAddress return contractAddress
} }
// fail test if contract for denom IS registered // fail test if contract for denom IS registered
func (suite *ConversionCosmosNativeSuite) denomContractNotRegistered(denom string) { func (suite *convertCosmosCoinToERC20Suite) denomContractNotRegistered(denom string) {
_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom) _, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.False(found) suite.False(found)
} }
// more tests of tests of this method are made to the msg handler, see ./msg_server_test.go // more tests of tests of this method are made to the msg handler, see ./msg_server_test.go
func (suite *ConversionCosmosNativeSuite) TestConvertCosmosCoinToERC20() { func (suite *convertCosmosCoinToERC20Suite) TestConvertCosmosCoinToERC20() {
allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2" allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
initialFunding := sdk.NewInt64Coin(allowedDenom, int64(1e10)) initialFunding := sdk.NewInt64Coin(allowedDenom, int64(1e10))
initiator := app.RandomAddress() initiator := app.RandomAddress()
@ -171,3 +171,129 @@ func (suite *ConversionCosmosNativeSuite) TestConvertCosmosCoinToERC20() {
) )
}) })
} }
type convertCosmosCoinFromERC20Suite struct {
testutil.Suite
denom string
initiator types.InternalEVMAddress
receiver sdk.AccAddress
contractAddress types.InternalEVMAddress
initialPosition sdk.Coin
query func(method string, args ...interface{}) ([]interface{}, error)
}
func (suite *convertCosmosCoinFromERC20Suite) SetupTest() {
var err error
suite.Suite.SetupTest()
suite.denom = "magic"
suite.initiator = testutil.RandomInternalEVMAddress()
suite.receiver = app.RandomAddress()
// manually create an initial position - sdk coin locked in module
suite.initialPosition = sdk.NewInt64Coin(suite.denom, 1e12)
err = suite.App.FundModuleAccount(suite.Ctx, types.ModuleName, sdk.NewCoins(suite.initialPosition))
suite.NoError(err)
// deploy erc20 contract for the denom
tokenInfo := types.AllowedCosmosCoinERC20Token{
CosmosDenom: suite.denom,
Name: "Test Token",
Symbol: "MAGIC",
Decimals: 6,
}
suite.contractAddress, err = suite.Keeper.GetOrDeployCosmosCoinERC20Contract(suite.Ctx, tokenInfo)
suite.NoError(err)
// manually create an initial position - minted tokens
err = suite.Keeper.MintERC20(suite.Ctx, suite.contractAddress, suite.initiator, suite.initialPosition.Amount.BigInt())
suite.NoError(err)
caller, key := testutil.RandomEvmAccount()
suite.query = func(method string, args ...interface{}) ([]interface{}, error) {
return suite.QueryContract(
types.ERC20KavaWrappedCosmosCoinContract.ABI,
caller,
key,
suite.contractAddress,
method,
args...,
)
}
}
func (suite *convertCosmosCoinFromERC20Suite) checkTotalSupply(expectedSupply sdkmath.Int) {
res, err := suite.query("totalSupply")
suite.NoError(err)
suite.Len(res, 1)
suite.BigIntsEqual(expectedSupply.BigInt(), res[0].(*big.Int), "unexpected total supply")
}
func (suite *convertCosmosCoinFromERC20Suite) checkBalanceOf(address types.InternalEVMAddress, expectedBalance sdkmath.Int) {
res, err := suite.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))
}
func TestConversionCosmosNativeFromEVMSuite(t *testing.T) {
suite.Run(t, new(convertCosmosCoinFromERC20Suite))
}
func (suite *convertCosmosCoinFromERC20Suite) TestConvertCosmosCoinFromERC20_NoContractDeployed() {
err := suite.Keeper.ConvertCosmosCoinFromERC20(
suite.Ctx,
suite.initiator,
suite.receiver,
sdk.NewInt64Coin("unsupported-denom", 1e6),
)
suite.ErrorContains(err, "no erc20 contract found for unsupported-denom")
}
func (suite *convertCosmosCoinFromERC20Suite) TestConvertCosmosCoinFromERC20() {
// half the initial position
amount := suite.initialPosition.SubAmount(suite.initialPosition.Amount.QuoRaw(2))
suite.Run("partial withdraw", func() {
err := suite.Keeper.ConvertCosmosCoinFromERC20(
suite.Ctx,
suite.initiator,
suite.receiver,
amount,
)
suite.NoError(err)
suite.checkTotalSupply(amount.Amount)
suite.checkBalanceOf(suite.initiator, amount.Amount)
suite.App.CheckBalance(suite.T(), suite.Ctx, suite.receiver, sdk.NewCoins(amount))
})
suite.Run("full withdraw", func() {
err := suite.Keeper.ConvertCosmosCoinFromERC20(
suite.Ctx,
suite.initiator,
suite.receiver,
amount,
)
suite.NoError(err)
// expect no remaining erc20 balance
suite.checkTotalSupply(sdkmath.ZeroInt())
suite.checkBalanceOf(suite.initiator, sdkmath.ZeroInt())
// expect full amount withdrawn to receiver
suite.App.CheckBalance(suite.T(), suite.Ctx, suite.receiver, sdk.NewCoins(suite.initialPosition))
})
suite.Run("insufficient balance", func() {
err := suite.Keeper.ConvertCosmosCoinFromERC20(
suite.Ctx,
suite.initiator,
suite.receiver,
amount,
)
suite.ErrorContains(err, "failed to convert to cosmos coins: insufficient funds")
})
}

View File

@ -158,6 +158,27 @@ func (k Keeper) MintERC20(
return err return err
} }
// BurnERC20 burns the token amount from the initiator's balance.
func (k Keeper) BurnERC20(
ctx sdk.Context,
contractAddr types.InternalEVMAddress,
initiator types.InternalEVMAddress,
amount *big.Int,
) error {
_, err := k.CallEVM(
ctx,
types.ERC20KavaWrappedCosmosCoinContract.ABI,
types.ModuleEVMAddress,
contractAddr,
"burn",
// Burn ERC20 args
initiator.Address,
amount,
)
return err
}
func (k Keeper) QueryERC20BalanceOf( func (k Keeper) QueryERC20BalanceOf(
ctx sdk.Context, ctx sdk.Context,
contractAddr types.InternalEVMAddress, contractAddr types.InternalEVMAddress,

View File

@ -154,7 +154,36 @@ func (s msgServer) ConvertCosmosCoinToERC20(
// back into an sdk.Coin. // back into an sdk.Coin.
func (s msgServer) ConvertCosmosCoinFromERC20( func (s msgServer) ConvertCosmosCoinFromERC20(
goCtx context.Context, goCtx context.Context,
req *types.MsgConvertCosmosCoinFromERC20, msg *types.MsgConvertCosmosCoinFromERC20,
) (*types.MsgConvertCosmosCoinFromERC20Response, error) { ) (*types.MsgConvertCosmosCoinFromERC20Response, error) {
panic("unimplemented - coming soon!") ctx := sdk.UnwrapSDKContext(goCtx)
initiator, err := types.NewInternalEVMAddressFromString(msg.Initiator)
if err != nil {
return nil, fmt.Errorf("invalid initiator address: %w", err)
}
receiver, err := sdk.AccAddressFromBech32(msg.Receiver)
if err != nil {
return nil, fmt.Errorf("invalid receiver address: %w", err)
}
if err := s.keeper.ConvertCosmosCoinFromERC20(
ctx,
initiator,
receiver,
*msg.Amount,
); err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Initiator),
),
)
return &types.MsgConvertCosmosCoinFromERC20Response{}, nil
} }

View File

@ -509,3 +509,222 @@ func (suite *MsgServerSuite) TestConvertCosmosCoinToERC20_AlreadyDeployedContrac
suite.Len(totalSupply, 1) suite.Len(totalSupply, 1)
suite.Equal(amount.MulRaw(2).BigInt(), totalSupply[0].(*big.Int)) suite.Equal(amount.MulRaw(2).BigInt(), totalSupply[0].(*big.Int))
} }
func (suite *MsgServerSuite) TestConvertCosmosCoinFromERC20() {
denom := "magic"
tokenInfo := types.NewAllowedCosmosCoinERC20Token(denom, "Cosmos Coin", "MAGIC", 6)
initialPosition := sdk.NewInt64Coin(denom, 1e10)
initiator := testutil.RandomInternalEVMAddress()
var contractAddress types.InternalEVMAddress
setup := func() {
suite.SetupTest()
// allow conversion to the denom
params := suite.Keeper.GetParams(suite.Ctx)
params.AllowedCosmosDenoms = append(params.AllowedCosmosDenoms, tokenInfo)
suite.Keeper.SetParams(suite.Ctx, params)
// setup initial position
addr := app.RandomAddress()
err := suite.App.FundAccount(suite.Ctx, addr, sdk.NewCoins(initialPosition))
suite.NoError(err)
err = suite.Keeper.ConvertCosmosCoinToERC20(suite.Ctx, addr, initiator, initialPosition)
suite.NoError(err)
contractAddress, _ = suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
}
testCases := []struct {
name string
msg types.MsgConvertCosmosCoinFromERC20
amountConverted sdkmath.Int
expectedErr string
}{
{
name: "valid - full convert",
msg: types.NewMsgConvertCosmosCoinFromERC20(
initiator.Hex(),
app.RandomAddress().String(),
initialPosition,
),
amountConverted: initialPosition.Amount,
expectedErr: "",
},
{
name: "valid - partial convert",
msg: types.NewMsgConvertCosmosCoinFromERC20(
initiator.Hex(),
app.RandomAddress().String(),
sdk.NewInt64Coin(denom, 123456),
),
amountConverted: sdkmath.NewInt(123456),
expectedErr: "",
},
{
name: "invalid - bad initiator",
msg: types.NewMsgConvertCosmosCoinFromERC20(
"invalid-address",
app.RandomAddress().String(),
sdk.NewInt64Coin(denom, 123456),
),
amountConverted: sdkmath.ZeroInt(),
expectedErr: "invalid initiator address",
},
{
name: "invalid - bad receiver",
msg: types.NewMsgConvertCosmosCoinFromERC20(
testutil.RandomEvmAddress().Hex(),
"invalid-address",
sdk.NewInt64Coin(denom, 123456),
),
amountConverted: sdkmath.ZeroInt(),
expectedErr: "invalid receiver address",
},
{
name: "invalid - unsupported asset",
msg: types.NewMsgConvertCosmosCoinFromERC20(
initiator.Hex(),
app.RandomAddress().String(),
sdk.NewInt64Coin("not-supported", 123456),
),
amountConverted: sdkmath.ZeroInt(),
expectedErr: "no erc20 contract found",
},
{
name: "invalid - insufficient funds",
msg: types.NewMsgConvertCosmosCoinFromERC20(
initiator.Hex(),
app.RandomAddress().String(),
initialPosition.AddAmount(sdkmath.OneInt()),
),
amountConverted: sdkmath.ZeroInt(),
expectedErr: "failed to convert to cosmos coins: insufficient funds",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
setup()
_, err := suite.msgServer.ConvertCosmosCoinFromERC20(suite.Ctx, &tc.msg)
if tc.expectedErr != "" {
suite.ErrorContains(err, tc.expectedErr)
// expect no change in erc20 balance
balance, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, initiator)
suite.NoError(err)
suite.BigIntsEqual(initialPosition.Amount.BigInt(), balance, "expected no change in initiator's erc20 balance")
// expect no change in module balance
suite.Equal(initialPosition.Amount, suite.ModuleBalance(denom), "expected no change in module balance")
return
}
suite.NoError(err)
receiver := sdk.MustAccAddressFromBech32(tc.msg.Receiver)
// expect receiver to have the sdk coins
sdkBalance := suite.BankKeeper.GetBalance(suite.Ctx, receiver, denom)
suite.Equal(tc.amountConverted, sdkBalance.Amount)
newEvmBalance := initialPosition.SubAmount(tc.amountConverted)
// expect initiator to have the balance deducted
evmBalance, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, initiator)
suite.NoError(err)
suite.BigIntsEqual(newEvmBalance.Amount.BigInt(), evmBalance, "unexpected initiator final erc20 balance")
// expect tokens to be deducted from module account
suite.True(newEvmBalance.Amount.Equal(suite.ModuleBalance(denom)), "unexpected module balance")
// expect erc20 total supply to reflect new value
caller, key := testutil.RandomEvmAccount()
totalSupply, err := suite.QueryContract(
types.ERC20KavaWrappedCosmosCoinContract.ABI,
caller,
key,
contractAddress,
"totalSupply",
)
suite.NoError(err)
suite.BigIntsEqual(newEvmBalance.Amount.BigInt(), totalSupply[0].(*big.Int), "unexpected total supply")
})
}
}
// the test verifies the behavior for when a denom is removed from the params list
// after conversions have been made:
// - it should prevent more conversions from sdk -> evm for that denom
// - existing erc20s should be allowed to get converted back to a sdk.Coins
// - allowing the denom again should use existing contract
func (suite *MsgServerSuite) TestConvertCosmosCoinForRemovedDenom() {
denom := "magic"
tokenInfo := types.NewAllowedCosmosCoinERC20Token(denom, "MAGIC COIN", "MAGIC", 6)
account := app.RandomAddress()
evmAddr := types.BytesToInternalEVMAddress(account.Bytes())
coin := func(amt int64) sdk.Coin { return sdk.NewInt64Coin(denom, amt) }
// fund account
suite.NoError(suite.App.FundAccount(suite.Ctx, account, sdk.NewCoins(coin(1e10))))
// setup the token as allowed
params := suite.Keeper.GetParams(suite.Ctx)
params.AllowedCosmosDenoms = append(params.AllowedCosmosDenoms, tokenInfo)
suite.Keeper.SetParams(suite.Ctx, params)
// convert some coins while its allowed
msg := types.NewMsgConvertCosmosCoinToERC20(account.String(), evmAddr.Hex(), coin(5e9))
_, err := suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &msg)
suite.NoError(err)
// expect contract registered
contractAddress, isRegistered := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(isRegistered)
suite.False(contractAddress.IsNil())
// unregister contract
params.AllowedCosmosDenoms = []types.AllowedCosmosCoinERC20Token{}
suite.Keeper.SetParams(suite.Ctx, params)
suite.Run("disallows sdk -> evm when removed", func() {
msg := types.NewMsgConvertCosmosCoinToERC20(account.String(), evmAddr.Hex(), coin(5e9))
_, err := suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &msg)
suite.ErrorContains(err, "sdk.Coin not enabled to convert to ERC20 token")
})
suite.Run("allows conversion of existing ERC20s", func() {
msg := types.NewMsgConvertCosmosCoinFromERC20(evmAddr.Hex(), account.String(), coin(5e9))
_, err := suite.msgServer.ConvertCosmosCoinFromERC20(suite.Ctx, &msg)
suite.NoError(err)
// should be fully withdrawn
erc20Bal, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, evmAddr)
suite.NoError(err)
suite.BigIntsEqual(big.NewInt(0), erc20Bal, "cosmos coins were not converted back")
sdkBal := suite.BankKeeper.GetBalance(suite.Ctx, account, denom)
suite.Equal(coin(1e10), sdkBal)
})
suite.Run("contract stays registered", func() {
postDisableContractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(found)
suite.Equal(contractAddress, postDisableContractAddress)
})
suite.Run("re-enable uses original contract", func() {
// re-enable contract
params.AllowedCosmosDenoms = append(params.AllowedCosmosDenoms, tokenInfo)
suite.Keeper.SetParams(suite.Ctx, params)
// attempt conversion
msg := types.NewMsgConvertCosmosCoinToERC20(account.String(), evmAddr.Hex(), coin(1e10))
_, err := suite.msgServer.ConvertCosmosCoinToERC20(suite.Ctx, &msg)
suite.NoError(err)
// should have balance on original ERC20 contract
erc20Bal, err := suite.Keeper.QueryERC20BalanceOf(suite.Ctx, contractAddress, evmAddr)
suite.NoError(err)
suite.BigIntsEqual(big.NewInt(1e10), erc20Bal, "cosmos coins were not converted")
sdkBal := suite.BankKeeper.GetBalance(suite.Ctx, account, denom)
suite.True(sdkBal.IsZero())
})
}

View File

@ -54,6 +54,8 @@ Only Cosmos co-chain denominations that are in the `AllowedCosmosDenoms` param (
The ERC20 contracts are deployed and managed by x/evmutil. The contract is deployed on first convert of the coin. Once deployed, the addresses of the contracts can be queried via the `DeployedCosmosCoinContracts` query (`deployed_cosmos_coin_contracts` endpoint). The ERC20 contracts are deployed and managed by x/evmutil. The contract is deployed on first convert of the coin. Once deployed, the addresses of the contracts can be queried via the `DeployedCosmosCoinContracts` query (`deployed_cosmos_coin_contracts` endpoint).
If a denom is removed from the `AllowedCosmosDenoms` param, existing ERC20 tokens can be converted back to the underlying sdk.Coin via `MsgConvertCosmosCoinFromERC20`, but no conversions from sdk.Coin -> ERC via `MsgConvertCosmosCoinToERC20` are allowed.
### EVM-Native Assets ### 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. 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.

View File

@ -18,7 +18,7 @@ service Msg {
rpc ConvertCosmosCoinToERC20(MsgConvertCosmosCoinToERC20) returns (MsgConvertCosmosCoinToERC20Response); rpc ConvertCosmosCoinToERC20(MsgConvertCosmosCoinToERC20) returns (MsgConvertCosmosCoinToERC20Response);
} }
// ConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20. // MsgConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20.
message MsgConvertCosmosCoinToERC20 { message MsgConvertCosmosCoinToERC20 {
// Kava bech32 address initiating the conversion. // Kava bech32 address initiating the conversion.
string initiator = 1; string initiator = 1;
@ -36,6 +36,33 @@ message MsgConvertCosmosCoinToERC20 {
- The `amount` is deducted from the `initiator`'s balance and transferred to the module account. - 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`. - An equivalent amount of ERC20 tokens are minted by `x/evmutil` to the `receiver`.
## MsgConvertCosmosCoinFromERC20
`MsgConvertCosmosCoinFromERC20` is the inverse of `MsgConvertCosmosCoinToERC20`. It converts an ERC20 representation of a cosmos-sdk coin back to its underlying sdk.Coin.
```proto
service Msg {
// ConvertCosmosCoinFromERC20 defines a method for converting a cosmos sdk.Coin to an ERC20.
rpc ConvertCosmosCoinFromERC20(MsgConvertCosmosCoinFromERC20) returns (MsgConvertCosmosCoinFromERC20Response);
}
// MsgConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets.
message MsgConvertCosmosCoinFromERC20 {
// EVM hex address initiating the conversion.
string initiator = 1;
// Kava bech32 address that will receive the cosmos coins.
string receiver = 2;
// Amount is the amount to convert, expressed as a Cosmos coin.
cosmos.base.v1beta1.Coin amount = 3;
}
```
### State Changes
- The `amount` is transferred from the `x/evmutil` module account to the `receiver`.
- The same amount of the corresponding ERC20 is burned from the `initiator` account in the EVM.
## MsgConvertERC20ToCoin ## MsgConvertERC20ToCoin
`MsgConvertCoinToERC20` converts a Kava ERC20 coin to sdk.Coin. This message is for moving EVM-native assets from the EVM to the Cosmos ecosystem. `MsgConvertCoinToERC20` converts a Kava ERC20 coin to sdk.Coin. This message is for moving EVM-native assets from the EVM to the Cosmos ecosystem.

View File

@ -40,3 +40,14 @@ The evmutil module emits the following events:
| convert_cosmos_coin_to_erc20 | amount | `{amount}` | | convert_cosmos_coin_to_erc20 | amount | `{amount}` |
| message | module | evmutil | | message | module | evmutil |
| message | sender | {'sender address'} | | message | sender | {'sender address'} |
### MsgConvertCosmosCoinFromERC20
| Type | Attribute Key | Attribute Value |
| ------------------------------ | ------------- | ------------------ |
| convert_cosmos_coin_from_erc20 | initiator | `{initiator}` |
| convert_cosmos_coin_from_erc20 | receiver | `{receiver}` |
| convert_cosmos_coin_from_erc20 | erc20_address | `{erc20_address}` |
| convert_cosmos_coin_from_erc20 | amount | `{amount}` |
| message | module | evmutil |
| message | sender | {'sender address'} |

View File

@ -8,7 +8,8 @@ const (
EventTypeConvertERC20ToCoin = "convert_evm_erc20_to_coin" EventTypeConvertERC20ToCoin = "convert_evm_erc20_to_coin"
EventTypeConvertCoinToERC20 = "convert_evm_erc20_from_coin" EventTypeConvertCoinToERC20 = "convert_evm_erc20_from_coin"
EventTypeConvertCosmosCoinToERC20 = "convert_cosmos_coin_to_erc20" EventTypeConvertCosmosCoinToERC20 = "convert_cosmos_coin_to_erc20"
EventTypeConvertCosmosCoinFromERC20 = "convert_cosmos_coin_from_erc20"
// Event Attributes - Common // Event Attributes - Common
AttributeKeyReceiver = "receiver" AttributeKeyReceiver = "receiver"

View File

@ -236,7 +236,7 @@ func (m *MsgConvertERC20ToCoinResponse) XXX_DiscardUnknown() {
var xxx_messageInfo_MsgConvertERC20ToCoinResponse proto.InternalMessageInfo var xxx_messageInfo_MsgConvertERC20ToCoinResponse proto.InternalMessageInfo
// ConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets. // MsgConvertCosmosCoinToERC20 defines a conversion from cosmos sdk.Coin to ERC20 for cosmos-native assets.
type MsgConvertCosmosCoinToERC20 struct { type MsgConvertCosmosCoinToERC20 struct {
// Kava bech32 address initiating the conversion. // Kava bech32 address initiating the conversion.
Initiator string `protobuf:"bytes,1,opt,name=initiator,proto3" json:"initiator,omitempty"` Initiator string `protobuf:"bytes,1,opt,name=initiator,proto3" json:"initiator,omitempty"`
@ -337,7 +337,7 @@ func (m *MsgConvertCosmosCoinToERC20Response) XXX_DiscardUnknown() {
var xxx_messageInfo_MsgConvertCosmosCoinToERC20Response proto.InternalMessageInfo var xxx_messageInfo_MsgConvertCosmosCoinToERC20Response proto.InternalMessageInfo
// ConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets. // MsgConvertCosmosCoinFromERC20 defines a conversion from ERC20 to cosmos coins for cosmos-native assets.
type MsgConvertCosmosCoinFromERC20 struct { type MsgConvertCosmosCoinFromERC20 struct {
// EVM hex address initiating the conversion. // EVM hex address initiating the conversion.
Initiator string `protobuf:"bytes,1,opt,name=initiator,proto3" json:"initiator,omitempty"` Initiator string `protobuf:"bytes,1,opt,name=initiator,proto3" json:"initiator,omitempty"`