feat(evmutil): track deployed contracts in state (#1598)

* feat(evmutil): track deployed contracts in state

* docs(evmutil): update state spec

* update changelog
This commit is contained in:
Robert Pirtle 2023-05-24 16:23:33 -07:00 committed by GitHub
parent 8495619130
commit 6585ac24b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 291 additions and 7 deletions

View File

@ -36,9 +36,10 @@ Ref: https://keepachangelog.com/en/1.0.0/
## [Unreleased]
## Features
### Features
- (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) [#1598] Track deployed ERC20 contract addresses for representing cosmos coins in module state
## [v0.23.0]
@ -241,6 +242,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
[#1598]: https://github.com/Kava-Labs/kava/pull/1598
[#1596]: https://github.com/Kava-Labs/kava/pull/1596
[#1591]: https://github.com/Kava-Labs/kava/pull/1591
[#1590]: https://github.com/Kava-Labs/kava/pull/1590

View File

@ -110,6 +110,32 @@ func (k Keeper) DeployKavaWrappedCosmosCoinERC20Contract(
return types.NewInternalEVMAddress(contractAddr), nil
}
// GetOrDeployCosmosCoinERC20Contract checks the module store for a deployed contract for the given
// token info and returns it if preset. Otherwise, it deploys and registers the contract.
func (k *Keeper) GetOrDeployCosmosCoinERC20Contract(
ctx sdk.Context,
tokenInfo types.AllowedCosmosCoinERC20Token,
) (types.InternalEVMAddress, error) {
contractAddress, found := k.GetDeployedCosmosCoinContract(ctx, tokenInfo.CosmosDenom)
if found {
// contract has already been deployed
return contractAddress, nil
}
// deploy a new contract
contractAddress, err := k.DeployKavaWrappedCosmosCoinERC20Contract(ctx, tokenInfo)
if err != nil {
return contractAddress, err
}
// register the contract to the module store
err = k.SetDeployedCosmosCoinContract(ctx, tokenInfo.CosmosDenom, contractAddress)
// TODO: emit event that contract was deployed
return contractAddress, err
}
// 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(

View File

@ -7,6 +7,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/evmutil/testutil"
"github.com/kava-labs/kava/x/evmutil/types"
)
@ -145,3 +146,66 @@ func (suite *ERC20TestSuite) TestDeployKavaWrappedCosmosCoinERC20Contract() {
suite.ErrorContains(err, "Ownable: caller is not the owner")
})
}
func (suite *ERC20TestSuite) TestGetOrDeployCosmosCoinERC20Contract() {
suite.Run("finds existing contract address", func() {
suite.SetupTest()
denom := "magic"
addr := types.BytesToInternalEVMAddress(app.RandomAddress().Bytes())
// pretend like we've registered a contract in a previous life
err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, denom, addr)
suite.NoError(err)
// expect it to find the registered address
tokenInfo := types.AllowedCosmosCoinERC20Token{CosmosDenom: denom}
contractAddress, err := suite.Keeper.GetOrDeployCosmosCoinERC20Contract(suite.Ctx, tokenInfo)
suite.NoError(err)
suite.Equal(addr, contractAddress)
// expect it to still be registered
contractAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(found)
suite.Equal(addr, contractAddress)
})
suite.Run("deploys & registers contract when one does not exist", func() {
suite.SetupTest()
denom := "magic"
tokenInfo := types.NewAllowedCosmosCoinERC20Token(denom, "Magic Coin", "MAGIC", 6)
// expect it to not be registered
_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.False(found)
// deploy the contract
contractAddress, err := suite.Keeper.GetOrDeployCosmosCoinERC20Contract(suite.Ctx, tokenInfo)
suite.NoError(err)
// expect it to be registered now
registeredAddress, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(found)
suite.False(registeredAddress.IsNil())
suite.Equal(contractAddress, registeredAddress)
})
// this can only happen if governance passes a bad allowed token
suite.Run("fails when token can't be deployed", func() {
suite.SetupTest()
denom := "nope"
// empty other fields means this token is invalid.
invalidToken := types.AllowedCosmosCoinERC20Token{CosmosDenom: denom}
// expect it to not be registered
_, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.False(found)
// attempt to deploy the contract
contractAddress, err := suite.Keeper.GetOrDeployCosmosCoinERC20Contract(suite.Ctx, invalidToken)
suite.ErrorContains(err, "failed to deploy erc20")
suite.True(contractAddress.IsNil())
// still expect it to not be registered
_, found = suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.False(found)
})
}

View File

@ -183,3 +183,32 @@ func (k Keeper) RemoveBalance(ctx sdk.Context, addr sdk.AccAddress, amt sdkmath.
}
return k.SetBalance(ctx, addr, finalBal)
}
// SetDeployedCosmosCoinContract stores a single deployed ERC20KavaWrappedCosmosCoin contract address
func (k *Keeper) SetDeployedCosmosCoinContract(ctx sdk.Context, cosmosDenom string, contractAddress types.InternalEVMAddress) error {
if err := sdk.ValidateDenom(cosmosDenom); err != nil {
return errorsmod.Wrap(types.ErrInvalidCosmosDenom, cosmosDenom)
}
if contractAddress.IsNil() {
return errorsmod.Wrapf(
sdkerrors.ErrInvalidAddress,
"attempting to register empty contract address for denom '%s'",
cosmosDenom,
)
}
store := ctx.KVStore(k.storeKey)
storeKey := types.DeployedCosmosCoinContractKey(cosmosDenom)
store.Set(storeKey, contractAddress.Bytes())
return nil
}
// SetDeployedCosmosCoinContract gets a deployed ERC20KavaWrappedCosmosCoin contract address by cosmos denom
// Returns the stored address and a bool indicating if it was found or not
func (k *Keeper) GetDeployedCosmosCoinContract(ctx sdk.Context, cosmosDenom string) (types.InternalEVMAddress, bool) {
store := ctx.KVStore(k.storeKey)
storeKey := types.DeployedCosmosCoinContractKey(cosmosDenom)
bz := store.Get(storeKey)
found := len(bz) != 0
return types.BytesToInternalEVMAddress(bz), found
}

View File

@ -355,6 +355,42 @@ func (suite *keeperTestSuite) TestGetBalance() {
}
}
func (suite *keeperTestSuite) TestDeployedCosmosCoinContractStoreState() {
suite.Run("returns nil for nonexistent denom", func() {
suite.SetupTest()
addr, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, "undeployed-denom")
suite.False(found)
suite.Equal(addr, types.InternalEVMAddress{})
})
suite.Run("handles setting & getting a contract address", func() {
suite.SetupTest()
denom := "ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2"
address := testutil.RandomInternalEVMAddress()
err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, denom, address)
suite.NoError(err)
stored, found := suite.Keeper.GetDeployedCosmosCoinContract(suite.Ctx, denom)
suite.True(found)
suite.Equal(address, stored)
})
suite.Run("fails when setting an invalid denom", func() {
suite.SetupTest()
invalidDenom := ""
err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, invalidDenom, testutil.RandomInternalEVMAddress())
suite.ErrorContains(err, "invalid cosmos denom")
})
suite.Run("fails when setting 0 address", func() {
suite.SetupTest()
invalidAddr := types.InternalEVMAddress{}
err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, "denom", invalidAddr)
suite.ErrorContains(err, "attempting to register empty contract address")
})
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(keeperTestSuite))
}

View File

@ -114,8 +114,8 @@ func (s msgServer) ConvertERC20ToCoin(
// ConvertCosmosCoinToERC20 converts a native sdk.Coin to an ERC20.
// If no ERC20 contract has been deployed for the given denom, a new
// contract will be deployed and registered to the module.
func (msgServer) ConvertCosmosCoinToERC20(
ctx context.Context,
func (s msgServer) ConvertCosmosCoinToERC20(
goCtx context.Context,
msg *types.MsgConvertCosmosCoinToERC20,
) (*types.MsgConvertCosmosCoinToERC20Response, error) {
return nil, fmt.Errorf("unimplemented - coming soon")

View File

@ -4,13 +4,16 @@ import (
"math/big"
"testing"
"github.com/stretchr/testify/suite"
sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/ethereum/go-ethereum/common/math"
"github.com/kava-labs/kava/x/evmutil/keeper"
"github.com/kava-labs/kava/x/evmutil/testutil"
"github.com/kava-labs/kava/x/evmutil/types"
"github.com/stretchr/testify/suite"
)
type MsgServerSuite struct {

View File

@ -20,6 +20,18 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetAllowedTokenMetadata gets the token metadata for the given cosmosDenom if it is allowed.
// Returns the metadata if allowed, and a bool indicating if the denom was in the allow list or not.
func (k Keeper) GetAllowedTokenMetadata(ctx sdk.Context, cosmosDenom string) (types.AllowedCosmosCoinERC20Token, bool) {
params := k.GetParams(ctx)
for _, token := range params.AllowedCosmosDenoms {
if token.CosmosDenom == cosmosDenom {
return token, true
}
}
return types.AllowedCosmosCoinERC20Token{}, false
}
// GetEnabledConversionPairFromERC20Address returns an ConversionPair from the internal contract address.
func (k Keeper) GetEnabledConversionPairFromERC20Address(
ctx sdk.Context,

View File

@ -60,3 +60,30 @@ func (suite *ParamsTestSuite) TestHistoricParamsQuery() {
_ = oldStateKeeper.GetParams(suite.Ctx)
})
}
func (suite *keeperTestSuite) TestGetAllowedTokenMetadata() {
suite.SetupTest()
atom := types.NewAllowedCosmosCoinERC20Token(
"ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2",
"Kava EVM ATOM", "ATOM", 6,
)
hard := types.NewAllowedCosmosCoinERC20Token("hard", "Kava EVM Hard", "HARD", 6)
// init state with some allowed tokens
params := suite.Keeper.GetParams(suite.Ctx)
params.AllowedCosmosDenoms = types.NewAllowedCosmosCoinERC20Tokens(atom, hard)
suite.Keeper.SetParams(suite.Ctx, params)
// finds allowed tokens by denom
storedAtom, allowed := suite.Keeper.GetAllowedTokenMetadata(suite.Ctx, atom.CosmosDenom)
suite.True(allowed)
suite.Equal(atom, storedAtom)
storedHard, allowed := suite.Keeper.GetAllowedTokenMetadata(suite.Ctx, hard.CosmosDenom)
suite.True(allowed)
suite.Equal(hard, storedHard)
// returns not-allowed when token not allowed
_, allowed = suite.Keeper.GetAllowedTokenMetadata(suite.Ctx, "not-in-list")
suite.False(allowed)
}

View File

@ -45,7 +45,6 @@ message AllowedCosmosCoinERC20Token {
// Number of decimals ERC20 contract is deployed with.
uint32 decimals = 4;
}
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the evmutil module to resume.
@ -70,6 +69,17 @@ message Account {
}
```
## Deployed Cosmos Coin Contract Addresses
Addresses for the ERC20 contracts representing cosmos-sdk `Coin`s are kept in the module store. They are stored as bytes by the cosmos-sdk denom they represent.
Example:
If a contract for representing the cosmos-sdk denom `cow` as an ERC20 in the EVM is deployed by the module to the address `0xbeef00000000000000000000000000000000beef`, the module store will contain:
`0x01 | bytes("cow") => bytes(0xbeef00000000000000000000000000000000beef)`
Where `0x01` is the `DeployedCosmosCoinContractKeyPrefix` defined in [keys.go](../types/keys.go).
## Store
For complete implementation details for how items are stored, see [keys.go](../types/keys.go). `x/evmutil` store state consists of accounts.
For complete implementation details for how items are stored, see [keys.go](../types/keys.go). `x/evmutil` store state consists of accounts and deployed contract addresses.

View File

@ -180,6 +180,10 @@ func (suite *Suite) Commit() {
suite.Ctx = suite.App.NewContext(false, header)
}
func (suite *Suite) ModuleBalance(denom string) sdk.Int {
return suite.App.GetModuleAccountBalance(suite.Ctx, types.ModuleName, denom)
}
func (suite *Suite) FundAccountWithKava(addr sdk.AccAddress, coins sdk.Coins) {
ukava := coins.AmountOf("ukava")
if ukava.IsPositive() {
@ -411,3 +415,12 @@ func RandomEvmAccount() (common.Address, *ethsecp256k1.PrivKey) {
addr := common.BytesToAddress(privKey.PubKey().Address())
return addr, privKey
}
func RandomEvmAddress() common.Address {
addr, _ := RandomEvmAccount()
return addr
}
func RandomInternalEVMAddress() types.InternalEVMAddress {
return types.NewInternalEVMAddress(RandomEvmAddress())
}

View File

@ -12,6 +12,11 @@ type InternalEVMAddress struct {
common.Address
}
// IsNil returns true when the address is the 0 address
func (a InternalEVMAddress) IsNil() bool {
return a.Address == common.Address{}
}
// NewInternalEVMAddress returns a new InternalEVMAddress from a common.Address.
func NewInternalEVMAddress(addr common.Address) InternalEVMAddress {
return InternalEVMAddress{
@ -19,6 +24,11 @@ func NewInternalEVMAddress(addr common.Address) InternalEVMAddress {
}
}
// BytesToInternalEVMAddress creates an InternalEVMAddress from a slice of bytes
func BytesToInternalEVMAddress(bz []byte) InternalEVMAddress {
return NewInternalEVMAddress(common.BytesToAddress(bz))
}
// NewInternalEVMAddressFromString returns a new InternalEVMAddress from a hex
// string. Returns an error if hex string is invalid.
func NewInternalEVMAddressFromString(addrStr string) (InternalEVMAddress, error) {

View File

@ -0,0 +1,38 @@
package types_test
import (
"testing"
"github.com/kava-labs/kava/x/evmutil/testutil"
"github.com/kava-labs/kava/x/evmutil/types"
"github.com/stretchr/testify/require"
)
func TestInternalEVMAddress_BytesToInternalEVMAddress(t *testing.T) {
addr := testutil.RandomEvmAddress()
require.Equal(t,
types.NewInternalEVMAddress(addr),
types.BytesToInternalEVMAddress(addr.Bytes()),
)
}
func TestInternalEVMAddress_IsNil(t *testing.T) {
addr := types.InternalEVMAddress{}
require.True(t, addr.IsNil())
addr.Address = testutil.RandomEvmAddress()
require.False(t, addr.IsNil())
}
func TestInternalEVMAddress_NewInternalEVMAddressFromString(t *testing.T) {
t.Run("works with valid address string", func(t *testing.T) {
validAddr := testutil.RandomEvmAddress()
addr, err := types.NewInternalEVMAddressFromString(validAddr.Hex())
require.NoError(t, err)
require.Equal(t, types.NewInternalEVMAddress(validAddr), addr)
})
t.Run("fails with invalid hex string", func(t *testing.T) {
_, err := types.NewInternalEVMAddressFromString("0xinvalid-address")
require.ErrorContains(t, err, "string is not a hex address")
})
}

View File

@ -9,4 +9,6 @@ var (
ErrConversionNotEnabled = errorsmod.Register(ModuleName, 4, "ERC20 token not enabled to convert to sdk.Coin")
ErrBalanceInvariance = errorsmod.Register(ModuleName, 5, "post EVM transfer balance invariant failed")
ErrUnexpectedContractEvent = errorsmod.Register(ModuleName, 6, "unexpected contract event")
ErrInvalidCosmosDenom = errorsmod.Register(ModuleName, 7, "invalid cosmos denom")
ErrSDKConversionNotEnabled = errorsmod.Register(ModuleName, 8, "sdk.Coin not enabled to convert to ERC20 token")
)

View File

@ -17,13 +17,25 @@ const (
RouterKey = ModuleName
)
var AccountStoreKeyPrefix = []byte{0x00} // prefix for keys that store accounts
// KVStore keys
var (
// AccountStoreKeyPrefix is the prefix for keys that store accounts
AccountStoreKeyPrefix = []byte{0x00}
// DeployedCosmosCoinContractKeyPrefix is the key for storing deployed KavaWrappedCosmosCoinERC20s contract addresses
DeployedCosmosCoinContractKeyPrefix = []byte{0x01}
)
// AccountStoreKey turns an address to a key used to get the account from the store
func AccountStoreKey(addr sdk.AccAddress) []byte {
return append(AccountStoreKeyPrefix, address.MustLengthPrefix(addr)...)
}
// DeployedCosmosCoinContractKey gives the store key that holds the address of the deployed ERC20
// that wraps the given cosmosDenom sdk.Coin
func DeployedCosmosCoinContractKey(cosmosDenom string) []byte {
return append(DeployedCosmosCoinContractKeyPrefix, []byte(cosmosDenom)...)
}
// ModuleAddress is the native module address for EVM
var ModuleEVMAddress common.Address