Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(evmutil): add ERC20KavaWrappedNativeCoinContract #1591

Merged
merged 3 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions x/evmutil/keeper/erc20.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keeper

import (
"encoding/hex"
"fmt"
"math/big"

Expand Down Expand Up @@ -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(
pirtleshell marked this conversation as resolved.
Show resolved Hide resolved
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(
Expand Down
61 changes: 61 additions & 0 deletions x/evmutil/keeper/erc20_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
17 changes: 11 additions & 6 deletions x/evmutil/testutil/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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() {
Expand Down
20 changes: 18 additions & 2 deletions x/evmutil/types/contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -32,17 +33,32 @@ 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() {
ERC20MintableBurnableAddress = ModuleEVMAddress

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")
}
}
Loading