mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +00:00
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:
parent
d988330d7f
commit
f4b8bf8f07
@ -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) [#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) [#1609] Add MsgConvertCosmosCoinFromERC20 for converting the ERC20 back to an sdk.Coin
|
||||
|
||||
### Client Breaking
|
||||
- (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
|
||||
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
|
||||
[#1604]: https://github.com/Kava-Labs/kava/pull/1604
|
||||
[#1603]: https://github.com/Kava-Labs/kava/pull/1603
|
||||
|
@ -3914,7 +3914,7 @@ MsgConvertCoinToERC20Response defines the response value from Msg/ConvertCoinToE
|
||||
<a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinFromERC20"></a>
|
||||
|
||||
### 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 |
|
||||
@ -3941,7 +3941,7 @@ MsgConvertCosmosCoinFromERC20Response defines the response value from Msg/MsgCon
|
||||
<a name="kava.evmutil.v1beta1.MsgConvertCosmosCoinToERC20"></a>
|
||||
|
||||
### 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 |
|
||||
|
@ -57,7 +57,7 @@ message MsgConvertERC20ToCoin {
|
||||
// Msg/MsgConvertERC20ToCoin.
|
||||
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 {
|
||||
// Kava bech32 address initiating the conversion.
|
||||
string initiator = 1;
|
||||
@ -70,7 +70,7 @@ message MsgConvertCosmosCoinToERC20 {
|
||||
// MsgConvertCosmosCoinToERC20Response defines the response value from Msg/MsgConvertCosmosCoinToERC20.
|
||||
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 {
|
||||
// EVM hex address initiating the conversion.
|
||||
string initiator = 1;
|
||||
|
@ -31,6 +31,7 @@ func GetTxCmd() *cobra.Command {
|
||||
getCmdMsgConvertCoinToERC20(),
|
||||
getCmdConvertERC20ToCoin(),
|
||||
getCmdMsgConvertCosmosCoinToERC20(),
|
||||
getCmdMsgConvertCosmosCoinFromERC20(),
|
||||
}
|
||||
|
||||
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)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
errorsmod "cosmossdk.io/errors"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/kava-labs/kava/x/evmutil/types"
|
||||
)
|
||||
@ -54,3 +57,51 @@ func (k *Keeper) ConvertCosmosCoinToERC20(
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -15,29 +15,29 @@ import (
|
||||
"github.com/kava-labs/kava/x/evmutil/types"
|
||||
)
|
||||
|
||||
type ConversionCosmosNativeSuite struct {
|
||||
type convertCosmosCoinToERC20Suite struct {
|
||||
testutil.Suite
|
||||
}
|
||||
|
||||
func TestConversionCosmosNativeSuite(t *testing.T) {
|
||||
suite.Run(t, new(ConversionCosmosNativeSuite))
|
||||
func TestConversionCosmosNativeToEvmSuite(t *testing.T) {
|
||||
suite.Run(t, new(convertCosmosCoinToERC20Suite))
|
||||
}
|
||||
|
||||
// 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)
|
||||
suite.True(found)
|
||||
return contractAddress
|
||||
}
|
||||
|
||||
// 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)
|
||||
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() {
|
||||
func (suite *convertCosmosCoinToERC20Suite) TestConvertCosmosCoinToERC20() {
|
||||
allowedDenom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
|
||||
initialFunding := sdk.NewInt64Coin(allowedDenom, int64(1e10))
|
||||
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")
|
||||
})
|
||||
}
|
||||
|
@ -158,6 +158,27 @@ func (k Keeper) MintERC20(
|
||||
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(
|
||||
ctx sdk.Context,
|
||||
contractAddr types.InternalEVMAddress,
|
||||
|
@ -154,7 +154,36 @@ func (s msgServer) ConvertCosmosCoinToERC20(
|
||||
// back into an sdk.Coin.
|
||||
func (s msgServer) ConvertCosmosCoinFromERC20(
|
||||
goCtx context.Context,
|
||||
req *types.MsgConvertCosmosCoinFromERC20,
|
||||
msg *types.MsgConvertCosmosCoinFromERC20,
|
||||
) (*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
|
||||
}
|
||||
|
@ -509,3 +509,222 @@ func (suite *MsgServerSuite) TestConvertCosmosCoinToERC20_AlreadyDeployedContrac
|
||||
suite.Len(totalSupply, 1)
|
||||
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())
|
||||
})
|
||||
}
|
||||
|
@ -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).
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
@ -18,7 +18,7 @@ service Msg {
|
||||
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 {
|
||||
// Kava bech32 address initiating the conversion.
|
||||
string initiator = 1;
|
||||
@ -36,6 +36,33 @@ message MsgConvertCosmosCoinToERC20 {
|
||||
- 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`.
|
||||
|
||||
## 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
|
||||
|
||||
`MsgConvertCoinToERC20` converts a Kava ERC20 coin to sdk.Coin. This message is for moving EVM-native assets from the EVM to the Cosmos ecosystem.
|
||||
|
@ -40,3 +40,14 @@ The evmutil module emits the following events:
|
||||
| convert_cosmos_coin_to_erc20 | amount | `{amount}` |
|
||||
| message | module | evmutil |
|
||||
| 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'} |
|
||||
|
@ -8,7 +8,8 @@ const (
|
||||
EventTypeConvertERC20ToCoin = "convert_evm_erc20_to_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
|
||||
AttributeKeyReceiver = "receiver"
|
||||
|
@ -236,7 +236,7 @@ func (m *MsgConvertERC20ToCoinResponse) XXX_DiscardUnknown() {
|
||||
|
||||
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 {
|
||||
// Kava bech32 address initiating the conversion.
|
||||
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
|
||||
|
||||
// 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 {
|
||||
// EVM hex address initiating the conversion.
|
||||
Initiator string `protobuf:"bytes,1,opt,name=initiator,proto3" json:"initiator,omitempty"`
|
||||
|
Loading…
Reference in New Issue
Block a user