mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 23:35:19 +00:00
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:
parent
8495619130
commit
6585ac24b0
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -20,6 +20,18 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
|
||||
k.paramSubspace.SetParamSet(ctx, ¶ms)
|
||||
}
|
||||
|
||||
// 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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -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) {
|
||||
|
38
x/evmutil/types/address_test.go
Normal file
38
x/evmutil/types/address_test.go
Normal 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")
|
||||
})
|
||||
}
|
@ -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")
|
||||
)
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user