feat(evmutil): add ERC20KavaWrappedNativeCoinContract (#1591)

* feat(evmutil): add ERC20KavaWrappedNativeCoinContract

* adds the contract ABI & bytecode for an Ownable erc20 with the following:
  * customizable decimals on deploy -> requires overriding decimals() view
  * mint() exposed for the contract owner which will be the evmutil module
  * burn() exposed for the contract owner which will be the evmutil module
* sets up keeper to deploy above token based on details from an
  AllowedNativeCoinERC20Token
* tests basic queries and permissions of deployed contract

* update changelog

* improve error messages & comments for erc20 deploy
This commit is contained in:
Robert Pirtle 2023-05-23 10:16:00 -07:00 committed by GitHub
parent 6da31bd662
commit 278f7854dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 8 deletions

View File

@ -38,6 +38,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
## Features ## Features
- (evmutil) [#1590] Add allow list param of sdk native denoms that can be transferred to evm - (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] ## [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 - [#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
[#1591]: https://github.com/Kava-Labs/kava/pull/1591
[#1590]: https://github.com/Kava-Labs/kava/pull/1590 [#1590]: https://github.com/Kava-Labs/kava/pull/1590
[#1568]: https://github.com/Kava-Labs/kava/pull/1568 [#1568]: https://github.com/Kava-Labs/kava/pull/1568
[#1567]: https://github.com/Kava-Labs/kava/pull/1567 [#1567]: https://github.com/Kava-Labs/kava/pull/1567

View File

@ -1,6 +1,7 @@
package keeper package keeper
import ( import (
"encoding/hex"
"fmt" "fmt"
"math/big" "math/big"
@ -64,6 +65,51 @@ func (k Keeper) DeployTestMintableERC20Contract(
return types.NewInternalEVMAddress(contractAddr), nil 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 // 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. // unchecked and should only be called after permission and enabled ERC20 checks.
func (k Keeper) MintERC20( func (k Keeper) MintERC20(

View File

@ -84,3 +84,64 @@ func (suite *ERC20TestSuite) TestERC20Mint() {
suite.Require().True(ok, "balanceOf should respond with *big.Int") suite.Require().True(ok, "balanceOf should respond with *big.Int")
suite.Require().Equal(big.NewInt(1234), balance) 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")
})
}

View File

@ -72,12 +72,10 @@ func (suite *Suite) SetupTest() {
suite.EvmModuleAddr = suite.AccountKeeper.GetModuleAddress(evmtypes.ModuleName) suite.EvmModuleAddr = suite.AccountKeeper.GetModuleAddress(evmtypes.ModuleName)
// test evm user keys that have no minting permissions // test evm user keys that have no minting permissions
key1, err := ethsecp256k1.GenerateKey() addr, privKey := suite.RandomAccount()
suite.Require().NoError(err) suite.Key1 = privKey
suite.Key1 = key1 suite.Key1Addr = types.NewInternalEVMAddress(addr)
suite.Key1Addr = types.NewInternalEVMAddress(common.BytesToAddress(suite.Key1.PubKey().Address())) _, suite.Key2 = suite.RandomAccount()
suite.Key2, err = ethsecp256k1.GenerateKey()
suite.Require().NoError(err)
_, addrs := app.GeneratePrivKeyAddressPairs(4) _, addrs := app.GeneratePrivKeyAddressPairs(4)
suite.Addrs = addrs suite.Addrs = addrs
@ -182,6 +180,13 @@ func (suite *Suite) Commit() {
suite.Ctx = suite.App.NewContext(false, header) 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) { func (suite *Suite) FundAccountWithKava(addr sdk.AccAddress, coins sdk.Coins) {
ukava := coins.AmountOf("ukava") ukava := coins.AmountOf("ukava")
if ukava.IsPositive() { if ukava.IsPositive() {

View File

@ -18,6 +18,7 @@ import (
// Embed ERC20 JSON files // Embed ERC20 JSON files
_ "embed" _ "embed"
"encoding/json" "encoding/json"
"fmt"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
evmtypes "github.com/evmos/ethermint/x/evm/types" evmtypes "github.com/evmos/ethermint/x/evm/types"
@ -32,6 +33,12 @@ var (
// ERC20MintableBurnableAddress is the erc20 module address // ERC20MintableBurnableAddress is the erc20 module address
ERC20MintableBurnableAddress common.Address ERC20MintableBurnableAddress common.Address
//go:embed ethermint_json/ERC20KavaWrappedNativeCoin.json
ERC20KavaWrappedNativeCoinJSON []byte
// ERC20KavaWrappedNativeCoinContract is the compiled erc20 contract
ERC20KavaWrappedNativeCoinContract evmtypes.CompiledContract
) )
func init() { func init() {
@ -39,10 +46,19 @@ func init() {
err := json.Unmarshal(ERC20MintableBurnableJSON, &ERC20MintableBurnableContract) err := json.Unmarshal(ERC20MintableBurnableJSON, &ERC20MintableBurnableContract)
if err != nil { if err != nil {
panic(err) panic(fmt.Sprintf("failed to unmarshal ERC20MintableBurnableJSON: %s. %s", err, string(ERC20MintableBurnableJSON)))
} }
if len(ERC20MintableBurnableContract.Bin) == 0 { 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")
} }
} }