diff --git a/wormchain/app/apptesting/test_suite.go b/wormchain/app/apptesting/test_suite.go index 7080838eac..37fdd42b60 100644 --- a/wormchain/app/apptesting/test_suite.go +++ b/wormchain/app/apptesting/test_suite.go @@ -51,7 +51,7 @@ var ( // Setup sets up basic environment for suite (App, Ctx, and test accounts) func (s *KeeperTestHelper) Setup() { s.App = Setup(s.T(), true, 0) - s.Ctx = s.App.BaseApp.NewContext(false, tmtypes.Header{Height: 1, ChainID: "osmosis-1", Time: time.Now().UTC()}) + s.Ctx = s.App.BaseApp.NewContext(false, tmtypes.Header{Height: 1, ChainID: "wormchain", Time: time.Now().UTC()}) s.QueryHelper = &baseapp.QueryServiceTestHelper{ GRPCQueryRouter: s.App.GRPCQueryRouter(), Ctx: s.Ctx, diff --git a/wormchain/x/tokenfactory/bindings/helpers_test.go b/wormchain/x/tokenfactory/bindings/helpers_test.go index 2105d65271..2fa7025337 100644 --- a/wormchain/x/tokenfactory/bindings/helpers_test.go +++ b/wormchain/x/tokenfactory/bindings/helpers_test.go @@ -21,7 +21,7 @@ import ( func CreateTestInput(t *testing.T) (*app.App, sdk.Context) { osmosis := apptesting.Setup(t, true, 0) - ctx := osmosis.BaseApp.NewContext(false, tmproto.Header{Height: 1, ChainID: "osmosis-1", Time: time.Now().UTC()}) + ctx := osmosis.BaseApp.NewContext(false, tmproto.Header{Height: 1, ChainID: "wormchain", Time: time.Now().UTC()}) return osmosis, ctx } diff --git a/wormchain/x/tokenfactory/keeper/bankactions.go b/wormchain/x/tokenfactory/keeper/bankactions.go index 303bb16625..1d2cd9d557 100644 --- a/wormchain/x/tokenfactory/keeper/bankactions.go +++ b/wormchain/x/tokenfactory/keeper/bankactions.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/wormhole-foundation/wormchain/x/tokenfactory/types" + denoms "github.com/wormhole-foundation/wormchain/x/tokenfactory/types" ) func (k Keeper) mintTo(ctx sdk.Context, amount sdk.Coin, mintTo string) error { @@ -15,6 +16,25 @@ func (k Keeper) mintTo(ctx sdk.Context, amount sdk.Coin, mintTo string) error { return err } + // We enable the new conditional approximately two weeks after block 12,066,314 for mainnet and 13,361,706 on testnet, which is + // calculated by dividing the number of seconds in a week by the average block time (~6s). + // On testnet, the block height is different (and so is the block time) with a block time of ~6s. + // On mainnet, the average block time is 5.77 seconds according to the PR https://github.com/wormhole-foundation/wormhole/pull/3946/files. + // At 5.77 seconds/block, this is ~209,636 blocks for mainnet. On testnet at 6 seconds/block, this is ~201,600 blocks for testnet. + // Therefore, mainnet cutover height is 12,066,314 + 209,636 = 12,275,950 and testnet cutover height is 13,361,706 + 201,600 = 13,563,306. + // The target is about ~7:30pm UTC January 28th, 2025. + isMainnet := ctx.ChainID() == "wormchain" + isTestnet := ctx.ChainID() == "wormchain-testnet-0" + + if (isMainnet && ctx.BlockHeight() >= 12275950) || (isTestnet && ctx.BlockHeight() >= 13563306) { + // Cutover is required because the call to GetSupply() will use more gas, which would result in a consensus failure. + totalSupplyCurrent := k.bankKeeper.GetSupply(ctx, amount.Denom) + TotalSupplyAfter := totalSupplyCurrent.Add(amount) // Can't integer overflow because of a ValidateBasic() check on this amount + if TotalSupplyAfter.Amount.GTE(denoms.MintAmountLimit) { + return fmt.Errorf("failed to mint - surpassed maximum mint amount") + } + } + err = k.bankKeeper.MintCoins(ctx, types.ModuleName, sdk.NewCoins(amount)) if err != nil { return err diff --git a/wormchain/x/tokenfactory/keeper/msg_server_test.go b/wormchain/x/tokenfactory/keeper/msg_server_test.go index 484d60ec68..f0634750eb 100644 --- a/wormchain/x/tokenfactory/keeper/msg_server_test.go +++ b/wormchain/x/tokenfactory/keeper/msg_server_test.go @@ -2,13 +2,20 @@ package keeper_test import ( "fmt" + "math/big" + "time" "github.com/wormhole-foundation/wormchain/x/tokenfactory/types" sdk "github.com/cosmos/cosmos-sdk/types" + denoms "github.com/wormhole-foundation/wormchain/x/tokenfactory/types" + //banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + tmtypes "github.com/tendermint/tendermint/proto/tendermint/types" ) +var mainnetUseConditionalHeight = int64(11445039) + // TestMintDenomMsg tests TypeMsgMint message is emitted on a successful mint func (suite *KeeperTestSuite) TestMintDenomMsg() { // Create a denom @@ -49,6 +56,177 @@ func (suite *KeeperTestSuite) TestMintDenomMsg() { } } +func (suite *KeeperTestSuite) TestMintHuge() { + // Create a denom + suite.CreateDefaultDenom() + + suite.Ctx = suite.App.BaseApp.NewContext(false, tmtypes.Header{Height: mainnetUseConditionalHeight, ChainID: "wormchain", Time: time.Now().UTC()}) + + largeAmount := big.NewInt(0).Sub(big.NewInt(0).Exp(big.NewInt(2), big.NewInt(256), big.NewInt(0)), big.NewInt(1)) // (2 ** 256)-1 + belowLargeAmount := big.NewInt(0).Exp(big.NewInt(2), big.NewInt(191), big.NewInt(0)) // 2 ** 191 + for _, tc := range []struct { + desc string + amount sdk.Int + mintDenom string + admin string + valid bool + expectedMessageEvents int + }{ + { + desc: "failure case - too many", + amount: sdk.NewIntFromBigInt(largeAmount), + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: false, + expectedMessageEvents: 0, + }, + { + desc: "success case with 191", + amount: sdk.NewIntFromBigInt(belowLargeAmount), + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: true, + expectedMessageEvents: 1, + }, + { + desc: "failure case - too many accumulated tokens", + amount: sdk.NewIntFromBigInt(belowLargeAmount), + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: false, + expectedMessageEvents: 0, + }, + } { + suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { + ctx := suite.Ctx.WithEventManager(sdk.NewEventManager()) + suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test mint message + suite.msgServer.Mint(sdk.WrapSDKContext(ctx), types.NewMsgMint(tc.admin, sdk.NewCoin(tc.mintDenom, tc.amount))) //nolint:errcheck + // Ensure current number and type of event is emitted + suite.AssertEventEmitted(ctx, types.TypeMsgMint, tc.expectedMessageEvents) + }) + } +} + +func (suite *KeeperTestSuite) TestMintOffByOne() { + // Create a denom + suite.CreateDefaultDenom() + suite.Ctx = suite.App.BaseApp.NewContext(false, tmtypes.Header{Height: mainnetUseConditionalHeight, ChainID: "wormchain", Time: time.Now().UTC()}) + + for _, tc := range []struct { + desc string + amount sdk.Int + mintDenom string + admin string + valid bool + expectedMessageEvents int + }{ + { + desc: "failure case - too many plus 1", + amount: denoms.MintAmountLimit.Add(sdk.NewIntFromUint64(1)), // 2 ** 192 + 1 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: false, + expectedMessageEvents: 0, + }, + { + desc: "failure case - too many exactly", + amount: denoms.MintAmountLimit, // 2 ** 192 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: true, + expectedMessageEvents: 0, + }, + { + desc: "success case - one less than limit", + amount: denoms.MintAmountLimit.Sub(sdk.NewIntFromUint64(1)), // 2 ** 192 -1 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: true, + expectedMessageEvents: 1, + }, + } { + suite.Run(fmt.Sprintf("Case %s", tc.desc), func() { + ctx := suite.Ctx.WithEventManager(sdk.NewEventManager()) + suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test mint message + suite.msgServer.Mint(sdk.WrapSDKContext(ctx), types.NewMsgMint(tc.admin, sdk.NewCoin(tc.mintDenom, tc.amount))) //nolint:errcheck + // Ensure current number and type of event is emitted + suite.AssertEventEmitted(ctx, types.TypeMsgMint, tc.expectedMessageEvents) + }) + } +} + +func (suite *KeeperTestSuite) TestMintFixBlockHeightChecks() { + // Create a denom + suite.CreateDefaultDenom() + + test_cases := []struct { + desc string + amount sdk.Int + mintDenom string + admin string + valid bool + expectedMessageEvents int + }{ + { + desc: "success case - check not implemented before block height", + amount: denoms.MintAmountLimit, // 2 ** 192 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: true, + expectedMessageEvents: 1, + }, + { + desc: "failure case - check implemented on specific block height", + amount: denoms.MintAmountLimit, // 2 ** 192 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: false, + expectedMessageEvents: 0, + }, + { + desc: "failure case - check implemented after specific block height", + amount: denoms.MintAmountLimit, // 2 ** 192 + mintDenom: suite.defaultDenom, + admin: suite.TestAccs[0].String(), + valid: false, + expectedMessageEvents: 0, + }, + } + // Before the block has been reached. Should succeed with the call. + suite.Ctx = suite.App.BaseApp.NewContext(false, tmtypes.Header{Height: mainnetUseConditionalHeight - 1, ChainID: "wormchain", Time: time.Now().UTC()}) + + ctx := suite.Ctx.WithEventManager(sdk.NewEventManager()) + suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test mint message + suite.msgServer.Mint(sdk.WrapSDKContext(ctx), types.NewMsgMint(test_cases[0].admin, sdk.NewCoin(test_cases[0].mintDenom, test_cases[0].amount))) //nolint:errcheck + + // Ensure current number and type of event is emitted + suite.AssertEventEmitted(ctx, types.TypeMsgMint, test_cases[0].expectedMessageEvents) + + // On the block has been reached + suite.Ctx = suite.App.BaseApp.NewContext(false, tmtypes.Header{Height: mainnetUseConditionalHeight, ChainID: "wormchain", Time: time.Now().UTC()}) + ctx = suite.Ctx.WithEventManager(sdk.NewEventManager()) + suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test mint message + suite.msgServer.Mint(sdk.WrapSDKContext(ctx), types.NewMsgMint(test_cases[1].admin, sdk.NewCoin(test_cases[1].mintDenom, test_cases[1].amount))) //nolint:errcheck + + // Ensure current number and type of event is emitted + suite.AssertEventEmitted(ctx, types.TypeMsgMint, test_cases[1].expectedMessageEvents) + + // After the block has been reached + suite.Ctx = suite.App.BaseApp.NewContext(false, tmtypes.Header{Height: mainnetUseConditionalHeight + 1, ChainID: "wormchain", Time: time.Now().UTC()}) + ctx = suite.Ctx.WithEventManager(sdk.NewEventManager()) + suite.Require().Equal(0, len(ctx.EventManager().Events())) + // Test mint message + suite.msgServer.Mint(sdk.WrapSDKContext(ctx), types.NewMsgMint(test_cases[2].admin, sdk.NewCoin(test_cases[2].mintDenom, test_cases[2].amount))) //nolint:errcheck + + // Ensure current number and type of event is emitted + suite.AssertEventEmitted(ctx, types.TypeMsgMint, test_cases[2].expectedMessageEvents) + +} + // TestBurnDenomMsg tests TypeMsgBurn message is emitted on a successful burn func (suite *KeeperTestSuite) TestBurnDenomMsg() { // Create a denom. diff --git a/wormchain/x/tokenfactory/types/denoms.go b/wormchain/x/tokenfactory/types/denoms.go index 7a9f2f9e99..5291fab4eb 100644 --- a/wormchain/x/tokenfactory/types/denoms.go +++ b/wormchain/x/tokenfactory/types/denoms.go @@ -1,6 +1,7 @@ package types import ( + "math/big" "strings" sdk "github.com/cosmos/cosmos-sdk/types" @@ -18,6 +19,9 @@ const ( MaxCreatorLength = 59 + MaxHrpLength ) +// 2 ** 192 +var MintAmountLimit = sdk.NewIntFromBigInt(big.NewInt(0).Exp(big.NewInt(2), big.NewInt(192), big.NewInt(0))) + // GetTokenDenom constructs a denom string for tokens created by tokenfactory // based on an input creator address and a subdenom // The denom constructed is factory/{creator}/{subdenom} diff --git a/wormchain/x/tokenfactory/types/errors.go b/wormchain/x/tokenfactory/types/errors.go index d5e09de191..dd7e29ec41 100644 --- a/wormchain/x/tokenfactory/types/errors.go +++ b/wormchain/x/tokenfactory/types/errors.go @@ -20,4 +20,5 @@ var ( ErrCreatorTooLong = sdkerrors.Register(ModuleName, 9, fmt.Sprintf("creator too long, max length is %d bytes", MaxCreatorLength)) ErrDenomDoesNotExist = sdkerrors.Register(ModuleName, 10, "denom does not exist") ErrCapabilityNotEnabled = sdkerrors.Register(ModuleName, 11, "this capability is not enabled on chain") + ErrMintAmountTooLarge = sdkerrors.Register(ModuleName, 12, "mint amount exceeds capacity") ) diff --git a/wormchain/x/tokenfactory/types/expected_keepers.go b/wormchain/x/tokenfactory/types/expected_keepers.go index 5500dab76b..4f3be5e837 100644 --- a/wormchain/x/tokenfactory/types/expected_keepers.go +++ b/wormchain/x/tokenfactory/types/expected_keepers.go @@ -12,6 +12,7 @@ type BankKeeper interface { SetDenomMetaData(ctx sdk.Context, denomMetaData banktypes.Metadata) HasSupply(ctx sdk.Context, denom string) bool + GetSupply(ctx sdk.Context, denom string) sdk.Coin SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error diff --git a/wormchain/x/tokenfactory/types/msgs.go b/wormchain/x/tokenfactory/types/msgs.go index be5e8a7167..3fb8664207 100644 --- a/wormchain/x/tokenfactory/types/msgs.go +++ b/wormchain/x/tokenfactory/types/msgs.go @@ -88,6 +88,10 @@ func (m MsgMint) ValidateBasic() error { return sdkerrors.Wrap(sdkerrors.ErrInvalidCoins, m.Amount.String()) } + if m.Amount.Amount.GTE(MintAmountLimit) { + return ErrMintAmountTooLarge + } + return nil } diff --git a/wormchain/x/tokenfactory/types/msgs_test.go b/wormchain/x/tokenfactory/types/msgs_test.go index 206f16d61d..fb7d27edde 100644 --- a/wormchain/x/tokenfactory/types/msgs_test.go +++ b/wormchain/x/tokenfactory/types/msgs_test.go @@ -2,6 +2,7 @@ package types_test import ( fmt "fmt" + "math/big" "testing" sdk "github.com/cosmos/cosmos-sdk/types" @@ -189,6 +190,14 @@ func TestMsgMint(t *testing.T) { }), expectPass: false, }, + { + name: "too large amount", + msg: createMsg(func(msg types.MsgMint) types.MsgMint { + msg.Amount.Amount = sdk.NewIntFromBigInt(big.NewInt(0).Sub(big.NewInt(0).Exp(big.NewInt(2), big.NewInt(256), big.NewInt(0)), big.NewInt(1))) + return msg + }), + expectPass: false, + }, } for _, test := range tests {