mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-24 15:25:18 +00:00
feat(evmutil): add CosmosCoinsFullyBackedInvariant (#1610)
* add IterateAllDeployedCosmosCoinContracts method * refactor unpacking big int from erc20 query * add QueryERC20TotalSupply method * feat(evmutil): add CosmosCoinsFullyBackedInvariant * update changelog
This commit is contained in:
parent
f4b8bf8f07
commit
528be6350e
@ -44,6 +44,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||
- (evmutil) [#1604] Emit events for MsgConvertCosmosCoinToERC20: `message` & `convert_cosmos_coin_to_erc20`
|
||||
- (evmutil) [#1605] Add query for deployed ERC20 contracts representing Cosmos coins in the EVM
|
||||
- (evmutil) [#1609] Add MsgConvertCosmosCoinFromERC20 for converting the ERC20 back to an sdk.Coin
|
||||
- (evmutil) [#1610] Add new invariant checking that ERC20s are fully backed by sdk.Coins
|
||||
|
||||
### Client Breaking
|
||||
- (evmutil) [#1603] Renamed error `ErrConversionNotEnabled` to `ErrEVMConversionNotEnabled`
|
||||
@ -252,6 +253,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
|
||||
|
||||
[#1610]: https://github.com/Kava-Labs/kava/pull/1610
|
||||
[#1609]: https://github.com/Kava-Labs/kava/pull/1609
|
||||
[#1605]: https://github.com/Kava-Labs/kava/pull/1605
|
||||
[#1604]: https://github.com/Kava-Labs/kava/pull/1604
|
||||
|
@ -1138,6 +1138,7 @@ func (app *App) loadBlockedMaccAddrs() map[string]bool {
|
||||
app.accountKeeper.GetModuleAddress(kavadisttypes.FundModuleAccount).String(): true,
|
||||
// community
|
||||
app.accountKeeper.GetModuleAddress(communitytypes.ModuleAccountName).String(): true,
|
||||
// NOTE: if adding evmutil, adjust the cosmos-coins-fully-backed-invariant accordingly.
|
||||
}
|
||||
|
||||
for addr := range modAccAddrs {
|
||||
|
@ -17,7 +17,10 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
erc20BalanceOfMethod = "balanceOf"
|
||||
erc20BalanceOfMethod = "balanceOf"
|
||||
erc20BurnMethod = "burn"
|
||||
erc20MintMethod = "mint"
|
||||
erc20TotalSupplyMethod = "totalSupply"
|
||||
)
|
||||
|
||||
// DeployTestMintableERC20Contract deploys an ERC20 contract on the EVM as the
|
||||
@ -149,7 +152,7 @@ func (k Keeper) MintERC20(
|
||||
types.ERC20MintableBurnableContract.ABI,
|
||||
types.ModuleEVMAddress,
|
||||
contractAddr,
|
||||
"mint",
|
||||
erc20MintMethod,
|
||||
// Mint ERC20 args
|
||||
receiver.Address,
|
||||
amount,
|
||||
@ -170,7 +173,7 @@ func (k Keeper) BurnERC20(
|
||||
types.ERC20KavaWrappedCosmosCoinContract.ABI,
|
||||
types.ModuleEVMAddress,
|
||||
contractAddr,
|
||||
"burn",
|
||||
erc20BurnMethod,
|
||||
// Burn ERC20 args
|
||||
initiator.Address,
|
||||
amount,
|
||||
@ -179,6 +182,8 @@ func (k Keeper) BurnERC20(
|
||||
return err
|
||||
}
|
||||
|
||||
// QueryERC20BalanceOf makes a contract call to the balanceOf method of the ERC20 contract to get
|
||||
// the ERC20 balance of the given account.
|
||||
func (k Keeper) QueryERC20BalanceOf(
|
||||
ctx sdk.Context,
|
||||
contractAddr types.InternalEVMAddress,
|
||||
@ -197,6 +202,31 @@ func (k Keeper) QueryERC20BalanceOf(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unpackERC20ResToBigInt(res, erc20BalanceOfMethod)
|
||||
}
|
||||
|
||||
// QueryERC20TotalSupply makes a contract call to the totalSupply method of the ERC20 contract to
|
||||
// get the total supply of the token.
|
||||
func (k Keeper) QueryERC20TotalSupply(
|
||||
ctx sdk.Context,
|
||||
contractAddr types.InternalEVMAddress,
|
||||
) (*big.Int, error) {
|
||||
res, err := k.CallEVM(
|
||||
ctx,
|
||||
types.ERC20KavaWrappedCosmosCoinContract.ABI,
|
||||
types.ModuleEVMAddress,
|
||||
contractAddr,
|
||||
erc20TotalSupplyMethod,
|
||||
// totalSupply takes no args
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return unpackERC20ResToBigInt(res, erc20TotalSupplyMethod)
|
||||
}
|
||||
|
||||
func unpackERC20ResToBigInt(res *evmtypes.MsgEthereumTxResponse, methodName string) (*big.Int, error) {
|
||||
if res.Failed() {
|
||||
if res.VmError == vm.ErrExecutionReverted.Error() {
|
||||
// Unpacks revert
|
||||
@ -206,11 +236,11 @@ func (k Keeper) QueryERC20BalanceOf(
|
||||
return nil, status.Error(codes.Internal, res.VmError)
|
||||
}
|
||||
|
||||
anyOutput, err := types.ERC20MintableBurnableContract.ABI.Unpack(erc20BalanceOfMethod, res.Ret)
|
||||
anyOutput, err := types.ERC20MintableBurnableContract.ABI.Unpack(methodName, res.Ret)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(
|
||||
"failed to unpack method %v response: %w",
|
||||
erc20BalanceOfMethod,
|
||||
methodName,
|
||||
err,
|
||||
)
|
||||
}
|
||||
@ -218,7 +248,7 @@ func (k Keeper) QueryERC20BalanceOf(
|
||||
if len(anyOutput) != 1 {
|
||||
return nil, fmt.Errorf(
|
||||
"invalid ERC20 %v call return outputs %v, expected %v",
|
||||
erc20BalanceOfMethod,
|
||||
methodName,
|
||||
len(anyOutput),
|
||||
1,
|
||||
)
|
||||
|
@ -86,6 +86,27 @@ func (suite *ERC20TestSuite) TestERC20Mint() {
|
||||
suite.Require().Equal(big.NewInt(1234), balance)
|
||||
}
|
||||
|
||||
func (suite *ERC20TestSuite) TestQueryERC20TotalSupply() {
|
||||
suite.Run("with no balance", func() {
|
||||
bal, err := suite.Keeper.QueryERC20TotalSupply(suite.Ctx, suite.contractAddr)
|
||||
suite.NoError(err)
|
||||
suite.BigIntsEqual(big.NewInt(0), bal, "expected total supply of 0")
|
||||
})
|
||||
|
||||
suite.Run("with balance", func() {
|
||||
amount := big.NewInt(1e10)
|
||||
expectedTotal := big.NewInt(3e10)
|
||||
// mint 1e10 to three random accounts
|
||||
suite.NoError(suite.Keeper.MintERC20(suite.Ctx, suite.contractAddr, testutil.RandomInternalEVMAddress(), amount))
|
||||
suite.NoError(suite.Keeper.MintERC20(suite.Ctx, suite.contractAddr, testutil.RandomInternalEVMAddress(), amount))
|
||||
suite.NoError(suite.Keeper.MintERC20(suite.Ctx, suite.contractAddr, testutil.RandomInternalEVMAddress(), amount))
|
||||
|
||||
bal, err := suite.Keeper.QueryERC20TotalSupply(suite.Ctx, suite.contractAddr)
|
||||
suite.NoError(err)
|
||||
suite.BigIntsEqual(expectedTotal, bal, "unexpected total supply after minting")
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *ERC20TestSuite) TestDeployKavaWrappedCosmosCoinERC20Contract() {
|
||||
suite.Run("fails to deploy invalid contract", func() {
|
||||
// empty other fields means this token is invalid.
|
||||
|
@ -1,6 +1,8 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
|
||||
@ -11,6 +13,7 @@ import (
|
||||
func RegisterInvariants(ir sdk.InvariantRegistry, bankK types.BankKeeper, k Keeper) {
|
||||
ir.RegisterRoute(types.ModuleName, "fully-backed", FullyBackedInvariant(bankK, k))
|
||||
ir.RegisterRoute(types.ModuleName, "small-balances", SmallBalancesInvariant(bankK, k))
|
||||
ir.RegisterRoute(types.ModuleName, "cosmos-coins-fully-backed", CosmosCoinsFullyBackedInvariant(bankK, k))
|
||||
// Disable this invariant due to some issues with it requiring some staking params to be set in genesis.
|
||||
// ir.RegisterRoute(types.ModuleName, "backed-conversion-coins", BackedCoinsInvariant(bankK, k))
|
||||
}
|
||||
@ -24,6 +27,9 @@ func AllInvariants(bankK types.BankKeeper, k Keeper) sdk.Invariant {
|
||||
if res, stop := BackedCoinsInvariant(bankK, k)(ctx); stop {
|
||||
return res, stop
|
||||
}
|
||||
if res, stop := CosmosCoinsFullyBackedInvariant(bankK, k)(ctx); stop {
|
||||
return res, stop
|
||||
}
|
||||
return SmallBalancesInvariant(bankK, k)(ctx)
|
||||
}
|
||||
}
|
||||
@ -105,3 +111,38 @@ func BackedCoinsInvariant(_ types.BankKeeper, k Keeper) sdk.Invariant {
|
||||
return message, broken
|
||||
}
|
||||
}
|
||||
|
||||
// CosmosCoinsFullyBackedInvariant ensures the total supply of ERC20 representations of sdk.Coins
|
||||
// match the balances in the module account.
|
||||
//
|
||||
// This invariant depends on the fact that coins can only become part of the balance through
|
||||
// conversion to ERC20s.
|
||||
// If in the future sdk.Coins can be sent directly to the module account,
|
||||
// or the module account balance can be increased in any other way,
|
||||
// this invariant should be changed from checking that the balance equals the total supply,
|
||||
// to check that the balance is greater than or equal to the total supply.
|
||||
func CosmosCoinsFullyBackedInvariant(bankK types.BankKeeper, k Keeper) sdk.Invariant {
|
||||
broken := false
|
||||
message := sdk.FormatInvariant(
|
||||
types.ModuleName,
|
||||
"cosmos coins fully-backed broken",
|
||||
"ERC20 total supply is not equal to module account balance",
|
||||
)
|
||||
maccAddress := authtypes.NewModuleAddress(types.ModuleName)
|
||||
|
||||
return func(ctx sdk.Context) (string, bool) {
|
||||
k.IterateAllDeployedCosmosCoinContracts(ctx, func(c types.DeployedCosmosCoinContract) bool {
|
||||
moduleBalance := bankK.GetBalance(ctx, maccAddress, c.CosmosDenom).Amount
|
||||
totalSupply, err := k.QueryERC20TotalSupply(ctx, *c.Address)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("failed to query total supply for %+v", c))
|
||||
}
|
||||
// expect total supply to equal balance in the module
|
||||
if totalSupply.Cmp(moduleBalance.BigInt()) != 0 {
|
||||
broken = true
|
||||
}
|
||||
return broken
|
||||
})
|
||||
return message, broken
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,12 @@ import (
|
||||
|
||||
sdkmath "cosmossdk.io/math"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
|
||||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/evmutil/keeper"
|
||||
"github.com/kava-labs/kava/x/evmutil/testutil"
|
||||
"github.com/kava-labs/kava/x/evmutil/types"
|
||||
@ -17,6 +21,9 @@ type invariantTestSuite struct {
|
||||
testutil.Suite
|
||||
invariants map[string]map[string]sdk.Invariant
|
||||
contractAddr types.InternalEVMAddress
|
||||
|
||||
cosmosCoin types.AllowedCosmosCoinERC20Token
|
||||
cosmosCoinContractAddr types.InternalEVMAddress
|
||||
}
|
||||
|
||||
func TestInvariantTestSuite(t *testing.T) {
|
||||
@ -62,6 +69,31 @@ func (suite *invariantTestSuite) SetupValidState() {
|
||||
big.NewInt(1000),
|
||||
)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// setup a cosmos coin erc20 with supply
|
||||
tokenInfo := types.NewAllowedCosmosCoinERC20Token("magic", "Magic coin", "MAGIC", 6)
|
||||
suite.cosmosCoin = tokenInfo
|
||||
params := suite.Keeper.GetParams(suite.Ctx)
|
||||
params.AllowedCosmosDenoms = append(params.AllowedCosmosDenoms, tokenInfo)
|
||||
suite.Keeper.SetParams(suite.Ctx, params)
|
||||
|
||||
suite.cosmosCoinContractAddr, err = suite.Keeper.GetOrDeployCosmosCoinERC20Contract(suite.Ctx, tokenInfo)
|
||||
suite.NoError(err)
|
||||
|
||||
// setup converted coin position
|
||||
err = suite.Keeper.MintERC20(
|
||||
suite.Ctx,
|
||||
suite.cosmosCoinContractAddr,
|
||||
testutil.RandomInternalEVMAddress(),
|
||||
big.NewInt(1e12),
|
||||
)
|
||||
suite.NoError(err)
|
||||
err = suite.App.FundModuleAccount(
|
||||
suite.Ctx,
|
||||
types.ModuleName,
|
||||
sdk.NewCoins(sdk.NewInt64Coin(tokenInfo.CosmosDenom, 1e12)),
|
||||
)
|
||||
suite.NoError(err)
|
||||
}
|
||||
|
||||
// RegisterRoutes implements sdk.InvariantRegistry
|
||||
@ -137,3 +169,49 @@ func (suite *invariantTestSuite) TestSmallBalances() {
|
||||
suite.Equal("evmutil: small balances broken invariant\nminor balances not all less than overflow\n", message)
|
||||
suite.Equal(true, broken)
|
||||
}
|
||||
|
||||
// the cosmos-coins-fully-backed invariant depends on 1-to-1 mapping of module balance to erc20s
|
||||
// if coins can be sent directly to the module account, this assumption is broken.
|
||||
// this test verifies that coins cannot be directly sent to the module account.
|
||||
func (suite *invariantTestSuite) TestSendToModuleAccountNotAllowed() {
|
||||
bKeeper := suite.App.GetBankKeeper()
|
||||
maccAddress := authtypes.NewModuleAddress(types.ModuleName)
|
||||
suite.True(bKeeper.BlockedAddr(maccAddress))
|
||||
|
||||
coins := sdk.NewCoins(sdk.NewInt64Coin(suite.cosmosCoin.CosmosDenom, 1e7))
|
||||
addr := app.RandomAddress()
|
||||
|
||||
err := suite.App.FundAccount(suite.Ctx, addr, coins)
|
||||
suite.NoError(err)
|
||||
|
||||
bankMsgServer := bankkeeper.NewMsgServerImpl(bKeeper)
|
||||
_, err = bankMsgServer.Send(suite.Ctx, &banktypes.MsgSend{
|
||||
FromAddress: addr.String(),
|
||||
ToAddress: maccAddress.String(),
|
||||
Amount: coins,
|
||||
})
|
||||
suite.ErrorContains(err, "kava1w9vxuke5dz6hyza2j932qgmxltnfxwl78u920k is not allowed to receive funds: unauthorized")
|
||||
}
|
||||
|
||||
func (suite *invariantTestSuite) TestCosmosCoinsFullyBackedInvariant() {
|
||||
invariantName := "cosmos-coins-fully-backed"
|
||||
// default state is valid
|
||||
_, broken := suite.runInvariant(invariantName, keeper.CosmosCoinsFullyBackedInvariant)
|
||||
suite.False(broken)
|
||||
|
||||
suite.SetupValidState()
|
||||
_, broken = suite.runInvariant(invariantName, keeper.CosmosCoinsFullyBackedInvariant)
|
||||
suite.False(broken)
|
||||
|
||||
// break the invariant by removing module account balance without adjusting token supply
|
||||
err := suite.BankKeeper.SendCoinsFromModuleToAccount(
|
||||
suite.Ctx,
|
||||
types.ModuleName,
|
||||
app.RandomAddress(),
|
||||
sdk.NewCoins(sdk.NewInt64Coin(suite.cosmosCoin.CosmosDenom, 1e5)),
|
||||
)
|
||||
suite.NoError(err)
|
||||
|
||||
_, broken = suite.runInvariant(invariantName, keeper.CosmosCoinsFullyBackedInvariant)
|
||||
suite.True(broken)
|
||||
}
|
||||
|
@ -212,3 +212,21 @@ func (k *Keeper) GetDeployedCosmosCoinContract(ctx sdk.Context, cosmosDenom stri
|
||||
found := len(bz) != 0
|
||||
return types.BytesToInternalEVMAddress(bz), found
|
||||
}
|
||||
|
||||
// IterateAllDeployedCosmosCoinContracts iterates through all the deployed ERC20 contracts representing
|
||||
// cosmos-sdk coins. If true is returned from the callback, iteration is halted.
|
||||
func (k Keeper) IterateAllDeployedCosmosCoinContracts(ctx sdk.Context, cb func(types.DeployedCosmosCoinContract) bool) {
|
||||
store := ctx.KVStore(k.storeKey)
|
||||
iterator := sdk.KVStorePrefixIterator(store, types.DeployedCosmosCoinContractKeyPrefix)
|
||||
|
||||
defer iterator.Close()
|
||||
for ; iterator.Valid(); iterator.Next() {
|
||||
contract := types.NewDeployedCosmosCoinContract(
|
||||
types.DenomFromDeployedCosmosCoinContractKey(iterator.Key()),
|
||||
types.BytesToInternalEVMAddress(iterator.Value()),
|
||||
)
|
||||
if cb(contract) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -391,6 +391,49 @@ func (suite *keeperTestSuite) TestDeployedCosmosCoinContractStoreState() {
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *keeperTestSuite) TestIterateAllDeployedCosmosCoinContracts() {
|
||||
suite.SetupTest()
|
||||
address := testutil.RandomInternalEVMAddress()
|
||||
denoms := []string{}
|
||||
register := func(denom string) {
|
||||
addr := testutil.RandomInternalEVMAddress()
|
||||
if denom == "waldo" {
|
||||
addr = address
|
||||
}
|
||||
err := suite.Keeper.SetDeployedCosmosCoinContract(suite.Ctx, denom, addr)
|
||||
suite.NoError(err)
|
||||
denoms = append(denoms, denom)
|
||||
}
|
||||
|
||||
// register some contracts
|
||||
register("magic")
|
||||
register("popcorn")
|
||||
register("waldo")
|
||||
register("zzz")
|
||||
register("ibc/27394FB092D2ECCD56123C74F36E4C1F926001CEADA9CA97EA622B25F41E5EB2")
|
||||
|
||||
suite.Run("stops when told", func() {
|
||||
// test out stopping the iteration
|
||||
// NOTE: don't actually look for a single contract this way. the keys are deterministic by denom.
|
||||
var contract types.DeployedCosmosCoinContract
|
||||
suite.Keeper.IterateAllDeployedCosmosCoinContracts(suite.Ctx, func(c types.DeployedCosmosCoinContract) bool {
|
||||
contract = c
|
||||
return c.CosmosDenom == "waldo"
|
||||
})
|
||||
suite.Equal(types.NewDeployedCosmosCoinContract("waldo", address), contract)
|
||||
})
|
||||
|
||||
suite.Run("iterates all contracts", func() {
|
||||
foundDenoms := make([]string, 0, len(denoms))
|
||||
suite.Keeper.IterateAllDeployedCosmosCoinContracts(suite.Ctx, func(c types.DeployedCosmosCoinContract) bool {
|
||||
foundDenoms = append(foundDenoms, c.CosmosDenom)
|
||||
return false
|
||||
})
|
||||
suite.Len(foundDenoms, len(denoms))
|
||||
suite.ElementsMatch(denoms, foundDenoms)
|
||||
})
|
||||
}
|
||||
|
||||
func TestKeeperTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(keeperTestSuite))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user