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