diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df4be05..98309c46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## Features - (evmutil) [#1590] Add allow list param of sdk native denoms that can be transferred to evm +- (evmutil) [#1591] Configure module to support deploying ERC20KavaWrappedNativeCoin contracts ## [v0.23.0] @@ -240,6 +241,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 +[#1591]: https://github.com/Kava-Labs/kava/pull/1591 [#1590]: https://github.com/Kava-Labs/kava/pull/1590 [#1568]: https://github.com/Kava-Labs/kava/pull/1568 [#1567]: https://github.com/Kava-Labs/kava/pull/1567 diff --git a/x/evmutil/keeper/erc20.go b/x/evmutil/keeper/erc20.go index db0e6d30..65db7f3b 100644 --- a/x/evmutil/keeper/erc20.go +++ b/x/evmutil/keeper/erc20.go @@ -1,6 +1,7 @@ package keeper import ( + "encoding/hex" "fmt" "math/big" @@ -64,6 +65,51 @@ func (k Keeper) DeployTestMintableERC20Contract( return types.NewInternalEVMAddress(contractAddr), nil } +// DeployKavaWrappedNativeCoinERC20Contract validates token details and then deploys an ERC20 +// contract with the token metadata. +// This method does NOT check if a token for the provided SdkDenom has already been deployed. +func (k Keeper) DeployKavaWrappedNativeCoinERC20Contract( + ctx sdk.Context, + token types.AllowedNativeCoinERC20Token, +) (types.InternalEVMAddress, error) { + if err := token.Validate(); err != nil { + return types.InternalEVMAddress{}, errorsmod.Wrapf(err, "failed to deploy erc20 for sdk denom %s", token.SdkDenom) + } + + packedAbi, err := types.ERC20KavaWrappedNativeCoinContract.ABI.Pack( + "", // Empty string for contract constructor + token.Name, + token.Symbol, + uint8(token.Decimals), // cast to uint8 is safe because of Validate() + ) + if err != nil { + return types.InternalEVMAddress{}, errorsmod.Wrapf(err, "failed to pack token with details %+v", token) + } + + data := make([]byte, len(types.ERC20KavaWrappedNativeCoinContract.Bin)+len(packedAbi)) + copy( + data[:len(types.ERC20KavaWrappedNativeCoinContract.Bin)], + types.ERC20KavaWrappedNativeCoinContract.Bin, + ) + copy( + data[len(types.ERC20KavaWrappedNativeCoinContract.Bin):], + packedAbi, + ) + + nonce, err := k.accountKeeper.GetSequence(ctx, types.ModuleEVMAddress.Bytes()) + if err != nil { + return types.InternalEVMAddress{}, err + } + + contractAddr := crypto.CreateAddress(types.ModuleEVMAddress, nonce) + _, err = k.CallEVMWithData(ctx, types.ModuleEVMAddress, nil, data) + if err != nil { + return types.InternalEVMAddress{}, fmt.Errorf("failed to deploy ERC20 %s (nonce=%d, data=%s): %s", token.Name, nonce, hex.EncodeToString(data), err) + } + + return types.NewInternalEVMAddress(contractAddr), nil +} + // MintERC20 mints the given amount of an ERC20 token to an address. This is // unchecked and should only be called after permission and enabled ERC20 checks. func (k Keeper) MintERC20( diff --git a/x/evmutil/keeper/erc20_test.go b/x/evmutil/keeper/erc20_test.go index b50dca24..1f9e8c08 100644 --- a/x/evmutil/keeper/erc20_test.go +++ b/x/evmutil/keeper/erc20_test.go @@ -84,3 +84,64 @@ func (suite *ERC20TestSuite) TestERC20Mint() { suite.Require().True(ok, "balanceOf should respond with *big.Int") suite.Require().Equal(big.NewInt(1234), balance) } + +func (suite *ERC20TestSuite) TestDeployKavaWrappedNativeCoinERC20Contract() { + suite.Run("fails to deploy invalid contract", func() { + // empty other fields means this token is invalid. + invalidToken := types.AllowedNativeCoinERC20Token{SdkDenom: "nope"} + _, err := suite.Keeper.DeployKavaWrappedNativeCoinERC20Contract(suite.Ctx, invalidToken) + suite.ErrorContains(err, "token's name cannot be empty") + }) + + suite.Run("deploys contract with expected metadata & permissions", func() { + caller, privKey := suite.RandomAccount() + + token := types.NewAllowedNativeCoinERC20Token("hard", "EVM HARD", "HARD", 6) + addr, err := suite.Keeper.DeployKavaWrappedNativeCoinERC20Contract(suite.Ctx, token) + suite.NoError(err) + suite.NotNil(addr) + + callContract := func(method string, args ...interface{}) ([]interface{}, error) { + return suite.QueryContract( + types.ERC20KavaWrappedNativeCoinContract.ABI, + caller, + privKey, + addr, + method, + args..., + ) + } + + // owner must be the evmutil module account + data, err := callContract("owner") + suite.NoError(err) + suite.Len(data, 1) + suite.Equal(types.ModuleEVMAddress, data[0].(common.Address)) + + // get name + data, err = callContract("name") + suite.NoError(err) + suite.Len(data, 1) + suite.Equal(token.Name, data[0].(string)) + + // get symbol + data, err = callContract("symbol") + suite.NoError(err) + suite.Len(data, 1) + suite.Equal(token.Symbol, data[0].(string)) + + // get decimals + data, err = callContract("decimals") + suite.NoError(err) + suite.Len(data, 1) + suite.Equal(token.Decimals, uint32(data[0].(uint8))) + + // should not be able to call mint + _, err = callContract("mint", caller, big.NewInt(1)) + suite.ErrorContains(err, "Ownable: caller is not the owner") + + // should not be able to call burn + _, err = callContract("burn", caller, big.NewInt(1)) + suite.ErrorContains(err, "Ownable: caller is not the owner") + }) +} diff --git a/x/evmutil/testutil/suite.go b/x/evmutil/testutil/suite.go index c36882bd..08abbbea 100644 --- a/x/evmutil/testutil/suite.go +++ b/x/evmutil/testutil/suite.go @@ -72,12 +72,10 @@ func (suite *Suite) SetupTest() { suite.EvmModuleAddr = suite.AccountKeeper.GetModuleAddress(evmtypes.ModuleName) // test evm user keys that have no minting permissions - key1, err := ethsecp256k1.GenerateKey() - suite.Require().NoError(err) - suite.Key1 = key1 - suite.Key1Addr = types.NewInternalEVMAddress(common.BytesToAddress(suite.Key1.PubKey().Address())) - suite.Key2, err = ethsecp256k1.GenerateKey() - suite.Require().NoError(err) + addr, privKey := suite.RandomAccount() + suite.Key1 = privKey + suite.Key1Addr = types.NewInternalEVMAddress(addr) + _, suite.Key2 = suite.RandomAccount() _, addrs := app.GeneratePrivKeyAddressPairs(4) suite.Addrs = addrs @@ -182,6 +180,13 @@ func (suite *Suite) Commit() { suite.Ctx = suite.App.NewContext(false, header) } +func (suite *Suite) RandomAccount() (common.Address, *ethsecp256k1.PrivKey) { + privKey, err := ethsecp256k1.GenerateKey() + suite.NoError(err) + addr := common.BytesToAddress(privKey.PubKey().Address()) + return addr, privKey +} + func (suite *Suite) FundAccountWithKava(addr sdk.AccAddress, coins sdk.Coins) { ukava := coins.AmountOf("ukava") if ukava.IsPositive() { diff --git a/x/evmutil/types/contract.go b/x/evmutil/types/contract.go index bf8dc229..38ae08bd 100644 --- a/x/evmutil/types/contract.go +++ b/x/evmutil/types/contract.go @@ -18,6 +18,7 @@ import ( // Embed ERC20 JSON files _ "embed" "encoding/json" + "fmt" "github.com/ethereum/go-ethereum/common" evmtypes "github.com/evmos/ethermint/x/evm/types" @@ -32,6 +33,12 @@ var ( // ERC20MintableBurnableAddress is the erc20 module address ERC20MintableBurnableAddress common.Address + + //go:embed ethermint_json/ERC20KavaWrappedNativeCoin.json + ERC20KavaWrappedNativeCoinJSON []byte + + // ERC20KavaWrappedNativeCoinContract is the compiled erc20 contract + ERC20KavaWrappedNativeCoinContract evmtypes.CompiledContract ) func init() { @@ -39,10 +46,19 @@ func init() { err := json.Unmarshal(ERC20MintableBurnableJSON, &ERC20MintableBurnableContract) if err != nil { - panic(err) + panic(fmt.Sprintf("failed to unmarshal ERC20MintableBurnableJSON: %s. %s", err, string(ERC20MintableBurnableJSON))) } if len(ERC20MintableBurnableContract.Bin) == 0 { - panic("load contract failed") + panic("loading ERC20MintableBurnable contract failed") + } + + err = json.Unmarshal(ERC20KavaWrappedNativeCoinJSON, &ERC20KavaWrappedNativeCoinContract) + if err != nil { + panic(fmt.Sprintf("failed to unmarshal ERC20KavaWrappedNativeCoinJSON: %s. %s", err, string(ERC20KavaWrappedNativeCoinJSON))) + } + + if len(ERC20KavaWrappedNativeCoinContract.Bin) == 0 { + panic("loading ERC20KavaWrappedNativeCoin contract failed") } }