From 8502bcd1de9d99d3c71205bcf0cd47b1150538d7 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Tue, 7 Jul 2026 15:39:16 +0800 Subject: [PATCH 1/3] deprecate vesting module while preserving all store/state Gate the vesting module at the consensus level: MsgCreateVestingAccount (the module's only message) is now rejected by the msg server with a registered ErrVestingDeprecated (codespace "vesting", code 2), covering both the gRPC msg service and the legacy Route() path. All state support is preserved: codec/interface/amino registrations, the vesting account types, and bank/xbank locked-coin logic are untouched, so existing vesting accounts keep decoding and vesting, and historical transactions remain decodable. The module stays wired into all apps for exactly that reason. With the handlers gated, the keeper plumbing became dead code and is removed: msgServer is now an empty struct, NewMsgServerImpl/NewHandler/ NewAppModule take no keepers, and the unused BankKeeper expected-keeper interface, AttributeValueCategory constant, and vesting metrics are deleted. Client tooling is deprecated but functional: the CLI tx command carries a cobra deprecation notice, and the add-genesis-account vesting flags are marked deprecated for legacy genesis tooling. Also repairs the norace-gated vesting CLI integration suite, which had rotted (stale network.DefaultConfig signature, fees below the 2000usei chain minimum); it now runs green end-to-end and asserts the deprecation error code on-chain. Co-Authored-By: Claude Fable 5 --- app/app.go | 2 +- cmd/seid/cmd/genaccounts.go | 4 + sei-cosmos/x/auth/vesting/client/cli/tx.go | 5 +- .../auth/vesting/client/testutil/cli_test.go | 2 +- .../x/auth/vesting/client/testutil/suite.go | 24 ++-- sei-cosmos/x/auth/vesting/handler.go | 8 +- sei-cosmos/x/auth/vesting/handler_test.go | 106 +++++++-------- sei-cosmos/x/auth/vesting/metrics.go | 33 ----- sei-cosmos/x/auth/vesting/module.go | 21 +-- sei-cosmos/x/auth/vesting/msg_server.go | 121 +++--------------- sei-cosmos/x/auth/vesting/types/constants.go | 3 - sei-cosmos/x/auth/vesting/types/errors.go | 11 ++ .../x/auth/vesting/types/expected_keepers.go | 13 -- sei-ibc-go/testing/simapp/app.go | 2 +- sei-wasmd/app/app.go | 2 +- x/evm/module_test.go | 4 +- 16 files changed, 117 insertions(+), 244 deletions(-) delete mode 100644 sei-cosmos/x/auth/vesting/metrics.go create mode 100644 sei-cosmos/x/auth/vesting/types/errors.go delete mode 100644 sei-cosmos/x/auth/vesting/types/expected_keepers.go diff --git a/app/app.go b/app/app.go index caea548446..1daec6afa1 100644 --- a/app/app.go +++ b/app/app.go @@ -890,7 +890,7 @@ func New( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.AccountKeeper, nil), - vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), + vesting.NewAppModule(), bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), diff --git a/cmd/seid/cmd/genaccounts.go b/cmd/seid/cmd/genaccounts.go index 7a55402598..800f0ef1bf 100644 --- a/cmd/seid/cmd/genaccounts.go +++ b/cmd/seid/cmd/genaccounts.go @@ -204,6 +204,10 @@ The association between the sei address and the eth address will also be created cmd.Flags().String(flagVestingAmt, "", "amount of coins for vesting accounts") cmd.Flags().Int64(flagVestingStart, 0, "schedule start time (unix epoch) for vesting accounts") cmd.Flags().Int64(flagVestingEnd, 0, "schedule end time (unix epoch) for vesting accounts") + deprecationNotice := "the vesting module is deprecated; vesting parameters are kept only for legacy genesis tooling" + _ = cmd.Flags().MarkDeprecated(flagVestingAmt, deprecationNotice) + _ = cmd.Flags().MarkDeprecated(flagVestingStart, deprecationNotice) + _ = cmd.Flags().MarkDeprecated(flagVestingEnd, deprecationNotice) flags.AddQueryFlagsToCmd(cmd) return cmd diff --git a/sei-cosmos/x/auth/vesting/client/cli/tx.go b/sei-cosmos/x/auth/vesting/client/cli/tx.go index c55422715c..9ec31a5185 100644 --- a/sei-cosmos/x/auth/vesting/client/cli/tx.go +++ b/sei-cosmos/x/auth/vesting/client/cli/tx.go @@ -22,7 +22,7 @@ const ( func GetTxCmd() *cobra.Command { txCmd := &cobra.Command{ Use: types.ModuleName, - Short: "Vesting transaction subcommands", + Short: "Vesting transaction subcommands (deprecated)", DisableFlagParsing: true, SuggestionsMinimumDistance: 2, RunE: client.ValidateCmd, @@ -46,7 +46,8 @@ account can either be a delayed or continuous vesting account, which is determin by the '--delayed' flag. All vesting accouts created will have their start time set by the committed block's time. The end_time must be provided as a UNIX epoch timestamp. You can also optionally configure the 'admin' field using the flag '--admin {addr}. This admin will be able to perform some administrative actions on the vesting account if set.`, - Args: cobra.ExactArgs(3), + Deprecated: "the vesting module is deprecated; the chain rejects this message, so new vesting accounts can no longer be created.", + Args: cobra.ExactArgs(3), RunE: func(cmd *cobra.Command, args []string) error { clientCtx, err := client.GetClientTxContext(cmd) if err != nil { diff --git a/sei-cosmos/x/auth/vesting/client/testutil/cli_test.go b/sei-cosmos/x/auth/vesting/client/testutil/cli_test.go index 9126c139f8..ac86f359c6 100644 --- a/sei-cosmos/x/auth/vesting/client/testutil/cli_test.go +++ b/sei-cosmos/x/auth/vesting/client/testutil/cli_test.go @@ -12,7 +12,7 @@ import ( ) func TestIntegrationTestSuite(t *testing.T) { - cfg := network.DefaultConfig() + cfg := network.DefaultConfig(t) cfg.NumValidators = 1 suite.Run(t, NewIntegrationTestSuite(cfg)) } diff --git a/sei-cosmos/x/auth/vesting/client/testutil/suite.go b/sei-cosmos/x/auth/vesting/client/testutil/suite.go index 08d2d26e96..963a329eba 100644 --- a/sei-cosmos/x/auth/vesting/client/testutil/suite.go +++ b/sei-cosmos/x/auth/vesting/client/testutil/suite.go @@ -1,6 +1,7 @@ package testutil import ( + "bytes" "fmt" "github.com/gogo/protobuf/proto" @@ -11,6 +12,7 @@ import ( "github.com/sei-protocol/sei-chain/sei-cosmos/testutil/network" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/client/cli" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) type IntegrationTestSuite struct { @@ -47,7 +49,7 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() { expectedCode uint32 respType proto.Message }{ - "create a continuous vesting account": { + "create a continuous vesting account is rejected as deprecated": { args: []string{ sdk.AccAddress("addr2_______________").String(), sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String(), @@ -55,13 +57,13 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() { fmt.Sprintf("--%s=%s", flags.FlagFrom, val.Address), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(2000))).String()), }, expectErr: false, - expectedCode: 0, + expectedCode: types.ErrVestingDeprecated.ABCICode(), respType: &sdk.TxResponse{}, }, - "create a delayed vesting account": { + "create a delayed vesting account is rejected as deprecated": { args: []string{ sdk.AccAddress("addr3_______________").String(), sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String(), @@ -70,10 +72,10 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() { fmt.Sprintf("--%s=true", cli.FlagDelayed), fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastBlock), - fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(10))).String()), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(s.cfg.BondDenom, sdk.NewInt(2000))).String()), }, expectErr: false, - expectedCode: 0, + expectedCode: types.ErrVestingDeprecated.ABCICode(), respType: &sdk.TxResponse{}, }, "invalid address": { @@ -112,8 +114,6 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() { } for name, tc := range testCases { - tc := tc - s.Run(name, func() { clientCtx := val.ClientCtx @@ -122,7 +122,13 @@ func (s *IntegrationTestSuite) TestNewMsgCreateVestingAccountCmd() { s.Require().Error(err) } else { s.Require().NoError(err) - s.Require().NoError(clientCtx.Codec.UnmarshalAsJSON(bw.Bytes(), tc.respType), bw.String()) + // the mock IO buffer captures cobra's deprecation notice ahead + // of the JSON response; skip past it before unmarshalling + out := bw.Bytes() + if i := bytes.IndexByte(out, '{'); i > 0 { + out = out[i:] + } + s.Require().NoError(clientCtx.Codec.UnmarshalAsJSON(out, tc.respType), bw.String()) txResp := tc.respType.(*sdk.TxResponse) s.Require().Equal(tc.expectedCode, txResp.Code) diff --git a/sei-cosmos/x/auth/vesting/handler.go b/sei-cosmos/x/auth/vesting/handler.go index 18033be66a..5989395eb2 100644 --- a/sei-cosmos/x/auth/vesting/handler.go +++ b/sei-cosmos/x/auth/vesting/handler.go @@ -3,13 +3,13 @@ package vesting import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" - "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) -// NewHandler returns a handler for x/auth message types. -func NewHandler(ak keeper.AccountKeeper, bk types.BankKeeper) sdk.Handler { - msgServer := NewMsgServerImpl(ak, bk) +// NewHandler returns a handler for x/auth message types. The vesting module is +// deprecated, so every message is rejected with types.ErrVestingDeprecated. +func NewHandler() sdk.Handler { + msgServer := NewMsgServerImpl() return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { ctx = ctx.WithEventManager(sdk.NewEventManager()) diff --git a/sei-cosmos/x/auth/vesting/handler_test.go b/sei-cosmos/x/auth/vesting/handler_test.go index f0694c8a0f..bf7a8df3af 100644 --- a/sei-cosmos/x/auth/vesting/handler_test.go +++ b/sei-cosmos/x/auth/vesting/handler_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/suite" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) @@ -24,97 +25,80 @@ func (suite *HandlerTestSuite) SetupTest() { checkTx := false app := app.Setup(suite.T(), checkTx, false, false) - suite.handler = vesting.NewHandler(app.AccountKeeper, app.BankKeeper) + suite.handler = vesting.NewHandler() suite.app = app } -func (suite *HandlerTestSuite) TestMsgCreateVestingAccount() { +// TestMsgCreateVestingAccountDeprecated verifies that the deprecated vesting +// module rejects account creation without mutating any state. +func (suite *HandlerTestSuite) TestMsgCreateVestingAccountDeprecated() { ctx := suite.app.BaseApp.NewContext(false, tmproto.Header{Height: suite.app.LastBlockHeight() + 1}) balances := sdk.NewCoins(sdk.NewInt64Coin("test", 1000)) addr1 := sdk.AccAddress([]byte("addr1_______________")) addr2 := sdk.AccAddress([]byte("addr2_______________")) - addr3 := sdk.AccAddress([]byte("addr3_______________")) - addr4 := sdk.AccAddress([]byte("addr4_______________")) - addr5 := sdk.AccAddress([]byte("addr5_______________")) - addr6 := sdk.AccAddress([]byte("addr6_______________")) - addr7 := sdk.AccAddress([]byte("addr7_______________")) - addr8 := sdk.AccAddress([]byte("addr8_______________")) acc1 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr1) suite.app.AccountKeeper.SetAccount(ctx, acc1) suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, addr1, balances)) - acc4 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr4) - suite.app.AccountKeeper.SetAccount(ctx, acc4) - suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, addr4, balances)) testCases := []struct { - name string - msg *types.MsgCreateVestingAccount - expectErr bool + name string + msg *types.MsgCreateVestingAccount }{ { - name: "create delayed vesting account", - msg: types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, nil), - expectErr: false, + name: "delayed vesting account", + msg: types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, nil), }, { - name: "create continuous vesting account", - msg: types.NewMsgCreateVestingAccount(addr1, addr3, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, nil), - expectErr: false, + name: "continuous vesting account", + msg: types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, nil), }, { - name: "create delayed vesting account with admin", - msg: types.NewMsgCreateVestingAccount(addr1, addr5, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, addr4), - expectErr: false, - }, - { - name: "create continuous vesting account with admin", - msg: types.NewMsgCreateVestingAccount(addr1, addr6, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, addr4), - expectErr: false, - }, - { - name: "create continuous vesting account with non-existing admin", - msg: types.NewMsgCreateVestingAccount(addr1, addr7, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, addr8), - expectErr: true, - }, - { - name: "continuous vesting account already exists", - msg: types.NewMsgCreateVestingAccount(addr1, addr3, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, nil), - expectErr: true, + name: "delayed vesting account with admin", + msg: types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, addr1), }, } for _, tc := range testCases { - tc := tc - suite.Run(tc.name, func() { res, err := suite.handler(ctx, tc.msg) - if tc.expectErr { - suite.Require().Error(err) - } else { - suite.Require().NoError(err) - suite.Require().NotNil(res) - - toAddr, err := sdk.AccAddressFromBech32(tc.msg.ToAddress) - suite.Require().NoError(err) - accI := suite.app.AccountKeeper.GetAccount(ctx, toAddr) - suite.Require().NotNil(accI) - - if tc.msg.Delayed { - acc, ok := accI.(*types.DelayedVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.msg.Amount, acc.GetVestingCoins(ctx.BlockTime())) - } else { - acc, ok := accI.(*types.ContinuousVestingAccount) - suite.Require().True(ok) - suite.Require().Equal(tc.msg.Amount, acc.GetVestingCoins(ctx.BlockTime())) - } - } + suite.Require().ErrorIs(err, types.ErrVestingDeprecated) + suite.Require().Nil(res) + + // no account is created and no funds move + suite.Require().Nil(suite.app.AccountKeeper.GetAccount(ctx, addr2)) + suite.Require().Equal(balances, suite.app.BankKeeper.GetAllBalances(ctx, addr1)) }) } } +// TestExistingVestingAccountsStillSupported verifies that vesting accounts +// already in state remain decodable and keep vesting after the module's +// deprecation. +func (suite *HandlerTestSuite) TestExistingVestingAccountsStillSupported() { + ctx := suite.app.BaseApp.NewContext(false, tmproto.Header{Height: suite.app.LastBlockHeight() + 1}) + + addr := sdk.AccAddress([]byte("vestacct____________")) + origVesting := sdk.NewCoins(sdk.NewInt64Coin("test", 100)) + endTime := ctx.BlockTime().Unix() + 10000 + + baseAcc, ok := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr).(*authtypes.BaseAccount) + suite.Require().True(ok) + + vestingAcc := types.NewDelayedVestingAccountRaw(types.NewBaseVestingAccount(baseAcc, origVesting, endTime, nil)) + suite.app.AccountKeeper.SetAccount(ctx, vestingAcc) + suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, addr, origVesting)) + + accI := suite.app.AccountKeeper.GetAccount(ctx, addr) + suite.Require().NotNil(accI) + + acc, ok := accI.(*types.DelayedVestingAccount) + suite.Require().True(ok) + suite.Require().Equal(origVesting, acc.GetVestingCoins(ctx.BlockTime())) + suite.Require().True(suite.app.BankKeeper.SpendableCoins(ctx, addr).IsZero()) +} + func TestHandlerTestSuite(t *testing.T) { suite.Run(t, new(HandlerTestSuite)) } diff --git a/sei-cosmos/x/auth/vesting/metrics.go b/sei-cosmos/x/auth/vesting/metrics.go deleted file mode 100644 index 6e7bc0c748..0000000000 --- a/sei-cosmos/x/auth/vesting/metrics.go +++ /dev/null @@ -1,33 +0,0 @@ -package vesting - -import ( - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/metric" -) - -var ( - meter = otel.Meter("seicosmos_x_auth_vesting") - - vestingMetrics = struct { - newAccount metric.Int64Counter - accountAmount metric.Int64Gauge - }{ - newAccount: must(meter.Int64Counter( - "new_account", - metric.WithDescription("Number of new vesting accounts created"), - metric.WithUnit("{count}"), - )), - accountAmount: must(meter.Int64Gauge( - "account_amount", - metric.WithDescription("Amount funded into the last new vesting account by denomination"), - metric.WithUnit("{utoken}"), - )), - } -) - -func must[V any](v V, err error) V { - if err != nil { - panic(err) - } - return v -} diff --git a/sei-cosmos/x/auth/vesting/module.go b/sei-cosmos/x/auth/vesting/module.go index a9e8dea87f..da7629add8 100644 --- a/sei-cosmos/x/auth/vesting/module.go +++ b/sei-cosmos/x/auth/vesting/module.go @@ -13,7 +13,6 @@ import ( codectypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/types/module" - "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/client/cli" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) @@ -26,6 +25,12 @@ var ( // AppModuleBasic defines the basic application module used by the sub-vesting // module. The module itself contain no special logic or state other than message // handling. +// +// The vesting module is deprecated: its message handlers reject all messages, +// so new vesting accounts can no longer be created. The module must remain +// wired into the app so its codec and interface registrations stay in place: +// they are required to decode existing vesting accounts in the auth store and +// historical transactions. type AppModuleBasic struct{} // Name returns the module's name. @@ -83,18 +88,16 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { // AppModule extends the AppModuleBasic implementation by implementing the // AppModule interface. +// +// The vesting module is deprecated; see AppModuleBasic. All message handlers +// return types.ErrVestingDeprecated. type AppModule struct { AppModuleBasic - - accountKeeper keeper.AccountKeeper - bankKeeper types.BankKeeper } -func NewAppModule(ak keeper.AccountKeeper, bk types.BankKeeper) AppModule { +func NewAppModule() AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, - accountKeeper: ak, - bankKeeper: bk, } } @@ -103,7 +106,7 @@ func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} // Route returns the module's message router and handler. func (am AppModule) Route() sdk.Route { - return sdk.NewRoute(types.RouterKey, NewHandler(am.accountKeeper, am.bankKeeper)) + return sdk.NewRoute(types.RouterKey, NewHandler()) } // QuerierRoute returns an empty string as the module contains no query @@ -112,7 +115,7 @@ func (AppModule) QuerierRoute() string { return "" } // RegisterServices registers module services. func (am AppModule) RegisterServices(cfg module.Configurator) { - types.RegisterMsgServer(cfg.MsgServer(), NewMsgServerImpl(am.accountKeeper, am.bankKeeper)) + types.RegisterMsgServer(cfg.MsgServer(), NewMsgServerImpl()) } // LegacyQuerierHandler performs a no-op. diff --git a/sei-cosmos/x/auth/vesting/msg_server.go b/sei-cosmos/x/auth/vesting/msg_server.go index 1bd1994d27..939c77ac09 100644 --- a/sei-cosmos/x/auth/vesting/msg_server.go +++ b/sei-cosmos/x/auth/vesting/msg_server.go @@ -3,115 +3,28 @@ package vesting import ( "context" - authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" - - "github.com/armon/go-metrics" - - "github.com/sei-protocol/sei-chain/sei-cosmos/telemetry" - sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" - sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" - "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" - "go.opentelemetry.io/otel/attribute" - otelmetric "go.opentelemetry.io/otel/metric" ) -type msgServer struct { - keeper.AccountKeeper - types.BankKeeper -} - -// NewMsgServerImpl returns an implementation of the vesting MsgServer interface, -// wrapping the corresponding AccountKeeper and BankKeeper. -func NewMsgServerImpl(k keeper.AccountKeeper, bk types.BankKeeper) types.MsgServer { - return &msgServer{AccountKeeper: k, BankKeeper: bk} +type msgServer struct{} + +// NewMsgServerImpl returns an implementation of the vesting MsgServer interface. +// +// The vesting module is deprecated: every handler rejects its message with +// types.ErrVestingDeprecated, so the server needs no keepers. It is kept +// registered so that deprecated messages fail with a clear error instead of an +// unroutable-message error, while existing vesting accounts in state remain +// fully supported. +func NewMsgServerImpl() types.MsgServer { + return msgServer{} } var _ types.MsgServer = msgServer{} -func (s msgServer) CreateVestingAccount(goCtx context.Context, msg *types.MsgCreateVestingAccount) (*types.MsgCreateVestingAccountResponse, error) { - ctx := sdk.UnwrapSDKContext(goCtx) - ak := s.AccountKeeper - bk := s.BankKeeper - - if err := bk.IsSendEnabledCoins(ctx, msg.Amount...); err != nil { - return nil, err - } - - from, err := sdk.AccAddressFromBech32(msg.FromAddress) - if err != nil { - return nil, err - } - to, err := sdk.AccAddressFromBech32(msg.ToAddress) - if err != nil { - return nil, err - } - - if bk.BlockedAddr(to) { - return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress) - } - - if acc := ak.GetAccount(ctx, to); acc != nil { - return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account %s already exists", msg.ToAddress) - } - - baseAccount := ak.NewAccountWithAddress(ctx, to) - if _, ok := baseAccount.(*authtypes.BaseAccount); !ok { - return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid account type; expected: BaseAccount, got: %T", baseAccount) - } - - var admin sdk.AccAddress - if len(msg.Admin) > 0 { - admin, err = sdk.AccAddressFromBech32(msg.Admin) - if err != nil { - return nil, err - } - if acc := ak.GetAccount(ctx, admin); acc == nil { - return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "admin account %s doesn't exist", msg.ToAddress) - } - } - - baseVestingAccount := types.NewBaseVestingAccount(baseAccount.(*authtypes.BaseAccount), msg.Amount.Sort(), msg.EndTime, admin) - - var acc authtypes.AccountI - - if msg.Delayed { - acc = types.NewDelayedVestingAccountRaw(baseVestingAccount) - } else { - acc = types.NewContinuousVestingAccountRaw(baseVestingAccount, ctx.BlockTime().Unix()) - } - - ak.SetAccount(ctx, acc) - - defer func() { - vestingMetrics.newAccount.Add(goCtx, 1) - // TODO(PLT-353): remove once vesting_new_account verified - telemetry.IncrCounter(1, "new", "account") - - for _, a := range msg.Amount { - if a.Amount.IsInt64() { - vestingMetrics.accountAmount.Record(goCtx, a.Amount.Int64(), otelmetric.WithAttributes(attribute.String("denom_class", telemetry.DenomClass(a.Denom)))) - // TODO(PLT-353): remove once vesting_account_amount verified - telemetry.SetGaugeWithLabels( - []string{"tx", "msg", "create_vesting_account"}, - float32(a.Amount.Int64()), - []metrics.Label{telemetry.NewLabel("denom", a.Denom)}, - ) - } - } - }() - - err = bk.SendCoins(ctx, from, to, msg.Amount) - if err != nil { - return nil, err - } - - ctx.EventManager().EmitEvent( - sdk.NewEvent( - sdk.EventTypeMessage, - sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), - ), - ) - - return &types.MsgCreateVestingAccountResponse{}, nil +// CreateVestingAccount is deprecated and always returns +// types.ErrVestingDeprecated. Existing vesting accounts remain in state and +// continue to vest according to their schedules; only the creation of new +// vesting accounts is disabled. +func (s msgServer) CreateVestingAccount(context.Context, *types.MsgCreateVestingAccount) (*types.MsgCreateVestingAccountResponse, error) { + return nil, types.ErrVestingDeprecated } diff --git a/sei-cosmos/x/auth/vesting/types/constants.go b/sei-cosmos/x/auth/vesting/types/constants.go index 1d0b0ebbb3..b385598d6a 100644 --- a/sei-cosmos/x/auth/vesting/types/constants.go +++ b/sei-cosmos/x/auth/vesting/types/constants.go @@ -4,9 +4,6 @@ const ( // ModuleName defines the module's name. ModuleName = "vesting" - // AttributeValueCategory is an alias for the message event value. - AttributeValueCategory = ModuleName - // RouterKey defines the module's message routing key RouterKey = ModuleName ) diff --git a/sei-cosmos/x/auth/vesting/types/errors.go b/sei-cosmos/x/auth/vesting/types/errors.go new file mode 100644 index 0000000000..16635a6ab5 --- /dev/null +++ b/sei-cosmos/x/auth/vesting/types/errors.go @@ -0,0 +1,11 @@ +package types + +import ( + sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" +) + +// ErrVestingDeprecated is returned by every vesting message handler now that +// the module is deprecated. Existing vesting accounts remain in state and +// continue to vest according to their schedules; only the creation of new +// vesting accounts is disabled. +var ErrVestingDeprecated = sdkerrors.Register(ModuleName, 2, "vesting module is deprecated; creating new vesting accounts is disabled") diff --git a/sei-cosmos/x/auth/vesting/types/expected_keepers.go b/sei-cosmos/x/auth/vesting/types/expected_keepers.go deleted file mode 100644 index ee77cd747d..0000000000 --- a/sei-cosmos/x/auth/vesting/types/expected_keepers.go +++ /dev/null @@ -1,13 +0,0 @@ -package types - -import ( - sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" -) - -// BankKeeper defines the expected interface contract the vesting module requires -// for creating vesting accounts with funds. -type BankKeeper interface { - IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error - SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error - BlockedAddr(addr sdk.AccAddress) bool -} diff --git a/sei-ibc-go/testing/simapp/app.go b/sei-ibc-go/testing/simapp/app.go index b0e881ecc9..69880e8ae0 100644 --- a/sei-ibc-go/testing/simapp/app.go +++ b/sei-ibc-go/testing/simapp/app.go @@ -380,7 +380,7 @@ func NewSimApp( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.AccountKeeper, nil), - vesting.NewAppModule(app.AccountKeeper, app.BankKeeper), + vesting.NewAppModule(), bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), diff --git a/sei-wasmd/app/app.go b/sei-wasmd/app/app.go index 3d0b15fb17..90b01c2404 100644 --- a/sei-wasmd/app/app.go +++ b/sei-wasmd/app/app.go @@ -535,7 +535,7 @@ func NewWasmApp( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.accountKeeper, nil), - vesting.NewAppModule(app.accountKeeper, app.bankKeeper), + vesting.NewAppModule(), bank.NewAppModule(appCodec, app.bankKeeper, app.accountKeeper), capability.NewAppModule(appCodec, *app.capabilityKeeper), gov.NewAppModule(appCodec, app.govKeeper, app.accountKeeper, app.bankKeeper), diff --git a/x/evm/module_test.go b/x/evm/module_test.go index 149570f70f..68b12410f7 100644 --- a/x/evm/module_test.go +++ b/x/evm/module_test.go @@ -128,10 +128,10 @@ func TestABCI(t *testing.T) { require.Equal(t, receipt.BlockNumber, uint64(ctx.BlockHeight())) require.Equal(t, receipt.VmError, "test error") - // disallow creating vesting account for coinbase address + // creating vesting accounts (including for coinbase addresses) is rejected: the module is deprecated k.BeginBlock(ctx) coinbase := state.GetCoinbaseAddress(2) - vms := vesting.NewMsgServerImpl(*k.AccountKeeper(), k.BankKeeper()) + vms := vesting.NewMsgServerImpl() _, err = vms.CreateVestingAccount(sdk.WrapSDKContext(ctx), &vestingtypes.MsgCreateVestingAccount{ FromAddress: sdk.AccAddress(evmAddr1[:]).String(), ToAddress: coinbase.String(), From f19732dab0257aaf5f5818fa7da7c6f93f8882ae Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Wed, 8 Jul 2026 22:34:51 +0800 Subject: [PATCH 2/3] gate vesting deprecation on upgrade height to preserve historical replay Address review feedback: the unconditional rejection was app-hash-breaking for from-genesis sync, since replaying a historical block containing a successful MsgCreateVestingAccount would produce different state, gas, and tx results. The deprecation is now gated per chain: - pacific-1 / atlantic-2 / arctic-1 (chains with pre-deprecation history): the original creation logic is preserved below the height at which the deprecation release's upgrade (DeprecationUpgradeName, "v6.6") executes, using UpgradeKeeper.IsUpgradeActiveAtHeight with a gas-free lookup so historical gas consumption is unchanged; from the upgrade height onward the message is rejected with ErrVestingDeprecated. - every other chain (fresh networks, tests): rejected from genesis. This also fixes early-binary-adoption divergence: validators running the new binary before the scheduled upgrade height keep consensus with the network. The msg server, handler, and module take back their keeper wiring (plus the upgrade keeper) to support the preserved legacy path; the telemetry- only vesting metrics remain removed. Tests cover all three gate states. Co-Authored-By: Claude Fable 5 --- app/app.go | 2 +- sei-cosmos/x/auth/vesting/handler.go | 8 +- sei-cosmos/x/auth/vesting/handler_test.go | 130 ++++++++++++++-- sei-cosmos/x/auth/vesting/module.go | 28 ++-- sei-cosmos/x/auth/vesting/msg_server.go | 141 ++++++++++++++++-- sei-cosmos/x/auth/vesting/types/constants.go | 3 + .../x/auth/vesting/types/expected_keepers.go | 19 +++ sei-ibc-go/testing/simapp/app.go | 2 +- sei-wasmd/app/app.go | 2 +- x/evm/module_test.go | 2 +- 10 files changed, 296 insertions(+), 41 deletions(-) create mode 100644 sei-cosmos/x/auth/vesting/types/expected_keepers.go diff --git a/app/app.go b/app/app.go index 1daec6afa1..d48c0c251f 100644 --- a/app/app.go +++ b/app/app.go @@ -890,7 +890,7 @@ func New( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.AccountKeeper, nil), - vesting.NewAppModule(), + vesting.NewAppModule(app.AccountKeeper, app.BankKeeper, app.UpgradeKeeper), bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), diff --git a/sei-cosmos/x/auth/vesting/handler.go b/sei-cosmos/x/auth/vesting/handler.go index 5989395eb2..5b44543b4b 100644 --- a/sei-cosmos/x/auth/vesting/handler.go +++ b/sei-cosmos/x/auth/vesting/handler.go @@ -3,13 +3,15 @@ package vesting import ( sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) // NewHandler returns a handler for x/auth message types. The vesting module is -// deprecated, so every message is rejected with types.ErrVestingDeprecated. -func NewHandler() sdk.Handler { - msgServer := NewMsgServerImpl() +// deprecated: once the deprecation gate is active, every message is rejected +// with types.ErrVestingDeprecated. +func NewHandler(ak keeper.AccountKeeper, bk types.BankKeeper, uk types.UpgradeKeeper) sdk.Handler { + msgServer := NewMsgServerImpl(ak, bk, uk) return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) { ctx = ctx.WithEventManager(sdk.NewEventManager()) diff --git a/sei-cosmos/x/auth/vesting/handler_test.go b/sei-cosmos/x/auth/vesting/handler_test.go index bf7a8df3af..0527da80bd 100644 --- a/sei-cosmos/x/auth/vesting/handler_test.go +++ b/sei-cosmos/x/auth/vesting/handler_test.go @@ -25,22 +25,28 @@ func (suite *HandlerTestSuite) SetupTest() { checkTx := false app := app.Setup(suite.T(), checkTx, false, false) - suite.handler = vesting.NewHandler() + suite.handler = vesting.NewHandler(app.AccountKeeper, app.BankKeeper, app.UpgradeKeeper) suite.app = app } -// TestMsgCreateVestingAccountDeprecated verifies that the deprecated vesting -// module rejects account creation without mutating any state. -func (suite *HandlerTestSuite) TestMsgCreateVestingAccountDeprecated() { - ctx := suite.app.BaseApp.NewContext(false, tmproto.Header{Height: suite.app.LastBlockHeight() + 1}) +func (suite *HandlerTestSuite) fundedContext(chainID string) (sdk.Context, sdk.AccAddress, sdk.Coins) { + ctx := suite.app.BaseApp.NewContext(false, tmproto.Header{Height: suite.app.LastBlockHeight() + 1, ChainID: chainID}) balances := sdk.NewCoins(sdk.NewInt64Coin("test", 1000)) - addr1 := sdk.AccAddress([]byte("addr1_______________")) - addr2 := sdk.AccAddress([]byte("addr2_______________")) + funder := sdk.AccAddress([]byte("addr1_______________")) + acc := suite.app.AccountKeeper.NewAccountWithAddress(ctx, funder) + suite.app.AccountKeeper.SetAccount(ctx, acc) + suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, funder, balances)) - acc1 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr1) - suite.app.AccountKeeper.SetAccount(ctx, acc1) - suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, addr1, balances)) + return ctx, funder, balances +} + +// TestMsgCreateVestingAccountDeprecated verifies that on chains without +// pre-deprecation history (any chain-id outside the allowlist) the deprecated +// vesting module rejects account creation without mutating any state. +func (suite *HandlerTestSuite) TestMsgCreateVestingAccountDeprecated() { + ctx, addr1, balances := suite.fundedContext("") + addr2 := sdk.AccAddress([]byte("addr2_______________")) testCases := []struct { name string @@ -73,6 +79,110 @@ func (suite *HandlerTestSuite) TestMsgCreateVestingAccountDeprecated() { } } +// TestMsgCreateVestingAccountPostUpgrade verifies that on chains with +// pre-deprecation history the gate activates once the deprecation upgrade has +// executed. +func (suite *HandlerTestSuite) TestMsgCreateVestingAccountPostUpgrade() { + ctx, addr1, balances := suite.fundedContext("pacific-1") + addr2 := sdk.AccAddress([]byte("addr2_______________")) + + suite.app.UpgradeKeeper.SetDone(ctx, vesting.DeprecationUpgradeName) + + msg := types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, nil) + res, err := suite.handler(ctx, msg) + suite.Require().ErrorIs(err, types.ErrVestingDeprecated) + suite.Require().Nil(res) + + // no account is created and no funds move + suite.Require().Nil(suite.app.AccountKeeper.GetAccount(ctx, addr2)) + suite.Require().Equal(balances, suite.app.BankKeeper.GetAllBalances(ctx, addr1)) +} + +// TestMsgCreateVestingAccountPreUpgradeHistory verifies that on chains with +// pre-deprecation history the original behavior is preserved below the +// deprecation upgrade height, so replaying historical blocks yields identical +// state. +func (suite *HandlerTestSuite) TestMsgCreateVestingAccountPreUpgradeHistory() { + ctx, addr1, _ := suite.fundedContext("pacific-1") + + addr2 := sdk.AccAddress([]byte("addr2_______________")) + addr3 := sdk.AccAddress([]byte("addr3_______________")) + addr4 := sdk.AccAddress([]byte("addr4_______________")) + addr5 := sdk.AccAddress([]byte("addr5_______________")) + addr6 := sdk.AccAddress([]byte("addr6_______________")) + addr7 := sdk.AccAddress([]byte("addr7_______________")) + addr8 := sdk.AccAddress([]byte("addr8_______________")) + + balances := sdk.NewCoins(sdk.NewInt64Coin("test", 1000)) + acc4 := suite.app.AccountKeeper.NewAccountWithAddress(ctx, addr4) + suite.app.AccountKeeper.SetAccount(ctx, acc4) + suite.Require().NoError(apptesting.FundAccount(suite.app.BankKeeper, ctx, addr4, balances)) + + testCases := []struct { + name string + msg *types.MsgCreateVestingAccount + expectErr bool + }{ + { + name: "create delayed vesting account", + msg: types.NewMsgCreateVestingAccount(addr1, addr2, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, nil), + expectErr: false, + }, + { + name: "create continuous vesting account", + msg: types.NewMsgCreateVestingAccount(addr1, addr3, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, nil), + expectErr: false, + }, + { + name: "create delayed vesting account with admin", + msg: types.NewMsgCreateVestingAccount(addr1, addr5, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, true, addr4), + expectErr: false, + }, + { + name: "create continuous vesting account with admin", + msg: types.NewMsgCreateVestingAccount(addr1, addr6, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, addr4), + expectErr: false, + }, + { + name: "create continuous vesting account with non-existing admin", + msg: types.NewMsgCreateVestingAccount(addr1, addr7, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, addr8), + expectErr: true, + }, + { + name: "continuous vesting account already exists", + msg: types.NewMsgCreateVestingAccount(addr1, addr3, sdk.NewCoins(sdk.NewInt64Coin("test", 100)), ctx.BlockTime().Unix()+10000, false, nil), + expectErr: true, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + res, err := suite.handler(ctx, tc.msg) + if tc.expectErr { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + suite.Require().NotNil(res) + + toAddr, err := sdk.AccAddressFromBech32(tc.msg.ToAddress) + suite.Require().NoError(err) + accI := suite.app.AccountKeeper.GetAccount(ctx, toAddr) + suite.Require().NotNil(accI) + + if tc.msg.Delayed { + acc, ok := accI.(*types.DelayedVestingAccount) + suite.Require().True(ok) + suite.Require().Equal(tc.msg.Amount, acc.GetVestingCoins(ctx.BlockTime())) + } else { + acc, ok := accI.(*types.ContinuousVestingAccount) + suite.Require().True(ok) + suite.Require().Equal(tc.msg.Amount, acc.GetVestingCoins(ctx.BlockTime())) + } + } + }) + } +} + // TestExistingVestingAccountsStillSupported verifies that vesting accounts // already in state remain decodable and keep vesting after the module's // deprecation. diff --git a/sei-cosmos/x/auth/vesting/module.go b/sei-cosmos/x/auth/vesting/module.go index da7629add8..f0af1bca1b 100644 --- a/sei-cosmos/x/auth/vesting/module.go +++ b/sei-cosmos/x/auth/vesting/module.go @@ -13,6 +13,7 @@ import ( codectypes "github.com/sei-protocol/sei-chain/sei-cosmos/codec/types" sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" "github.com/sei-protocol/sei-chain/sei-cosmos/types/module" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/client/cli" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) @@ -26,10 +27,12 @@ var ( // module. The module itself contain no special logic or state other than message // handling. // -// The vesting module is deprecated: its message handlers reject all messages, -// so new vesting accounts can no longer be created. The module must remain -// wired into the app so its codec and interface registrations stay in place: -// they are required to decode existing vesting accounts in the auth store and +// The vesting module is deprecated: once the deprecation gate is active (the +// DeprecationUpgradeName upgrade on chains with pre-deprecation history, +// genesis everywhere else), its message handlers reject all messages, so new +// vesting accounts can no longer be created. The module must remain wired into +// the app so its codec and interface registrations stay in place: they are +// required to decode existing vesting accounts in the auth store and // historical transactions. type AppModuleBasic struct{} @@ -89,15 +92,22 @@ func (AppModuleBasic) GetQueryCmd() *cobra.Command { // AppModule extends the AppModuleBasic implementation by implementing the // AppModule interface. // -// The vesting module is deprecated; see AppModuleBasic. All message handlers -// return types.ErrVestingDeprecated. +// The vesting module is deprecated; see AppModuleBasic. Once the deprecation +// gate is active, all message handlers return types.ErrVestingDeprecated. type AppModule struct { AppModuleBasic + + accountKeeper keeper.AccountKeeper + bankKeeper types.BankKeeper + upgradeKeeper types.UpgradeKeeper } -func NewAppModule() AppModule { +func NewAppModule(ak keeper.AccountKeeper, bk types.BankKeeper, uk types.UpgradeKeeper) AppModule { return AppModule{ AppModuleBasic: AppModuleBasic{}, + accountKeeper: ak, + bankKeeper: bk, + upgradeKeeper: uk, } } @@ -106,7 +116,7 @@ func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {} // Route returns the module's message router and handler. func (am AppModule) Route() sdk.Route { - return sdk.NewRoute(types.RouterKey, NewHandler()) + return sdk.NewRoute(types.RouterKey, NewHandler(am.accountKeeper, am.bankKeeper, am.upgradeKeeper)) } // QuerierRoute returns an empty string as the module contains no query @@ -115,7 +125,7 @@ func (AppModule) QuerierRoute() string { return "" } // RegisterServices registers module services. func (am AppModule) RegisterServices(cfg module.Configurator) { - types.RegisterMsgServer(cfg.MsgServer(), NewMsgServerImpl()) + types.RegisterMsgServer(cfg.MsgServer(), NewMsgServerImpl(am.accountKeeper, am.bankKeeper, am.upgradeKeeper)) } // LegacyQuerierHandler performs a no-op. diff --git a/sei-cosmos/x/auth/vesting/msg_server.go b/sei-cosmos/x/auth/vesting/msg_server.go index 939c77ac09..9881215e27 100644 --- a/sei-cosmos/x/auth/vesting/msg_server.go +++ b/sei-cosmos/x/auth/vesting/msg_server.go @@ -3,28 +3,139 @@ package vesting import ( "context" + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" + sdkerrors "github.com/sei-protocol/sei-chain/sei-cosmos/types/errors" + "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/keeper" + authtypes "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/types" "github.com/sei-protocol/sei-chain/sei-cosmos/x/auth/vesting/types" ) -type msgServer struct{} +// DeprecationUpgradeName is the upgrade plan that activates the vesting +// module's deprecation on chains with pre-deprecation history. It must match +// the plan name of the release that ships the deprecation; if this change +// slips to a later release, bump this constant to that release's name. +const DeprecationUpgradeName = "v6.6" -// NewMsgServerImpl returns an implementation of the vesting MsgServer interface. +// chainsWithVestingHistory are the public chains whose history may contain +// successful MsgCreateVestingAccount transactions. On these chains the +// deprecation activates at the height the DeprecationUpgradeName upgrade +// executes, so replaying historical blocks produces identical state, gas, and +// app hashes. On every other chain (fresh networks, tests) the deprecation is +// active from genesis. +var chainsWithVestingHistory = map[string]struct{}{ + "pacific-1": {}, // mainnet + "atlantic-2": {}, // testnet + "arctic-1": {}, // devnet +} + +type msgServer struct { + keeper.AccountKeeper + types.BankKeeper + upgradeKeeper types.UpgradeKeeper +} + +// NewMsgServerImpl returns an implementation of the vesting MsgServer +// interface, wrapping the corresponding AccountKeeper and BankKeeper. // -// The vesting module is deprecated: every handler rejects its message with -// types.ErrVestingDeprecated, so the server needs no keepers. It is kept -// registered so that deprecated messages fail with a clear error instead of an -// unroutable-message error, while existing vesting accounts in state remain -// fully supported. -func NewMsgServerImpl() types.MsgServer { - return msgServer{} +// The vesting module is deprecated: once the deprecation gate is active (see +// creationDeprecated), every handler rejects its message with +// types.ErrVestingDeprecated. Existing vesting accounts in state remain fully +// supported. +func NewMsgServerImpl(k keeper.AccountKeeper, bk types.BankKeeper, uk types.UpgradeKeeper) types.MsgServer { + return &msgServer{AccountKeeper: k, BankKeeper: bk, upgradeKeeper: uk} } var _ types.MsgServer = msgServer{} -// CreateVestingAccount is deprecated and always returns -// types.ErrVestingDeprecated. Existing vesting accounts remain in state and -// continue to vest according to their schedules; only the creation of new -// vesting accounts is disabled. -func (s msgServer) CreateVestingAccount(context.Context, *types.MsgCreateVestingAccount) (*types.MsgCreateVestingAccountResponse, error) { - return nil, types.ErrVestingDeprecated +// creationDeprecated reports whether vesting account creation is disabled at +// the current block. Chains with pre-deprecation history keep the original +// behavior below the deprecation upgrade height so historical replay is +// unchanged; all other chains reject immediately. +func (s msgServer) creationDeprecated(ctx sdk.Context) bool { + if _, ok := chainsWithVestingHistory[ctx.ChainID()]; !ok { + return true + } + // The done-height lookup must not consume gas: the pre-deprecation handler + // performed no store reads before its first bank check, so charging gas + // here would alter gas usage of historical transactions during replay. + gasFreeCtx := ctx.WithGasMeter(sdk.NewInfiniteGasMeter(1, 1)) + return s.upgradeKeeper.IsUpgradeActiveAtHeight(gasFreeCtx, DeprecationUpgradeName, ctx.BlockHeight()) +} + +// CreateVestingAccount is deprecated and returns types.ErrVestingDeprecated +// once the deprecation gate is active; the original behavior is preserved +// below the gate so chains with pre-deprecation history replay identically. +// Existing vesting accounts remain in state and continue to vest according to +// their schedules regardless of the gate. +func (s msgServer) CreateVestingAccount(goCtx context.Context, msg *types.MsgCreateVestingAccount) (*types.MsgCreateVestingAccountResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + if s.creationDeprecated(ctx) { + return nil, types.ErrVestingDeprecated + } + + ak := s.AccountKeeper + bk := s.BankKeeper + + if err := bk.IsSendEnabledCoins(ctx, msg.Amount...); err != nil { + return nil, err + } + + from, err := sdk.AccAddressFromBech32(msg.FromAddress) + if err != nil { + return nil, err + } + to, err := sdk.AccAddressFromBech32(msg.ToAddress) + if err != nil { + return nil, err + } + + if bk.BlockedAddr(to) { + return nil, sdkerrors.Wrapf(sdkerrors.ErrUnauthorized, "%s is not allowed to receive funds", msg.ToAddress) + } + + if acc := ak.GetAccount(ctx, to); acc != nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "account %s already exists", msg.ToAddress) + } + + baseAccount := ak.NewAccountWithAddress(ctx, to) + if _, ok := baseAccount.(*authtypes.BaseAccount); !ok { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "invalid account type; expected: BaseAccount, got: %T", baseAccount) + } + + var admin sdk.AccAddress + if len(msg.Admin) > 0 { + admin, err = sdk.AccAddressFromBech32(msg.Admin) + if err != nil { + return nil, err + } + if acc := ak.GetAccount(ctx, admin); acc == nil { + return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "admin account %s doesn't exist", msg.ToAddress) + } + } + + baseVestingAccount := types.NewBaseVestingAccount(baseAccount.(*authtypes.BaseAccount), msg.Amount.Sort(), msg.EndTime, admin) + + var acc authtypes.AccountI + + if msg.Delayed { + acc = types.NewDelayedVestingAccountRaw(baseVestingAccount) + } else { + acc = types.NewContinuousVestingAccountRaw(baseVestingAccount, ctx.BlockTime().Unix()) + } + + ak.SetAccount(ctx, acc) + + err = bk.SendCoins(ctx, from, to, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + ), + ) + + return &types.MsgCreateVestingAccountResponse{}, nil } diff --git a/sei-cosmos/x/auth/vesting/types/constants.go b/sei-cosmos/x/auth/vesting/types/constants.go index b385598d6a..1d0b0ebbb3 100644 --- a/sei-cosmos/x/auth/vesting/types/constants.go +++ b/sei-cosmos/x/auth/vesting/types/constants.go @@ -4,6 +4,9 @@ const ( // ModuleName defines the module's name. ModuleName = "vesting" + // AttributeValueCategory is an alias for the message event value. + AttributeValueCategory = ModuleName + // RouterKey defines the module's message routing key RouterKey = ModuleName ) diff --git a/sei-cosmos/x/auth/vesting/types/expected_keepers.go b/sei-cosmos/x/auth/vesting/types/expected_keepers.go new file mode 100644 index 0000000000..c3bfe37ace --- /dev/null +++ b/sei-cosmos/x/auth/vesting/types/expected_keepers.go @@ -0,0 +1,19 @@ +package types + +import ( + sdk "github.com/sei-protocol/sei-chain/sei-cosmos/types" +) + +// BankKeeper defines the expected interface contract the vesting module requires +// for creating vesting accounts with funds. +type BankKeeper interface { + IsSendEnabledCoins(ctx sdk.Context, coins ...sdk.Coin) error + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + BlockedAddr(addr sdk.AccAddress) bool +} + +// UpgradeKeeper defines the expected interface contract the vesting module +// requires for checking whether the deprecation upgrade has executed. +type UpgradeKeeper interface { + IsUpgradeActiveAtHeight(ctx sdk.Context, name string, height int64) bool +} diff --git a/sei-ibc-go/testing/simapp/app.go b/sei-ibc-go/testing/simapp/app.go index 69880e8ae0..ee98f1b8e0 100644 --- a/sei-ibc-go/testing/simapp/app.go +++ b/sei-ibc-go/testing/simapp/app.go @@ -380,7 +380,7 @@ func NewSimApp( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.AccountKeeper, nil), - vesting.NewAppModule(), + vesting.NewAppModule(app.AccountKeeper, app.BankKeeper, app.UpgradeKeeper), bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper), capability.NewAppModule(appCodec, *app.CapabilityKeeper), feegrantmodule.NewAppModule(appCodec, app.AccountKeeper, app.BankKeeper, app.FeeGrantKeeper, app.interfaceRegistry), diff --git a/sei-wasmd/app/app.go b/sei-wasmd/app/app.go index 90b01c2404..505c5a0ca8 100644 --- a/sei-wasmd/app/app.go +++ b/sei-wasmd/app/app.go @@ -535,7 +535,7 @@ func NewWasmApp( encodingConfig.TxConfig, ), auth.NewAppModule(appCodec, app.accountKeeper, nil), - vesting.NewAppModule(), + vesting.NewAppModule(app.accountKeeper, app.bankKeeper, app.upgradeKeeper), bank.NewAppModule(appCodec, app.bankKeeper, app.accountKeeper), capability.NewAppModule(appCodec, *app.capabilityKeeper), gov.NewAppModule(appCodec, app.govKeeper, app.accountKeeper, app.bankKeeper), diff --git a/x/evm/module_test.go b/x/evm/module_test.go index 68b12410f7..868771a2f1 100644 --- a/x/evm/module_test.go +++ b/x/evm/module_test.go @@ -131,7 +131,7 @@ func TestABCI(t *testing.T) { // creating vesting accounts (including for coinbase addresses) is rejected: the module is deprecated k.BeginBlock(ctx) coinbase := state.GetCoinbaseAddress(2) - vms := vesting.NewMsgServerImpl() + vms := vesting.NewMsgServerImpl(*k.AccountKeeper(), k.BankKeeper(), k.UpgradeKeeper()) _, err = vms.CreateVestingAccount(sdk.WrapSDKContext(ctx), &vestingtypes.MsgCreateVestingAccount{ FromAddress: sdk.AccAddress(evmAddr1[:]).String(), ToAddress: coinbase.String(), From c5eff688b1a2fca8010fc2adcd9477eaf1133a19 Mon Sep 17 00:00:00 2001 From: Tony Chen Date: Thu, 9 Jul 2026 12:54:14 +0800 Subject: [PATCH 3/3] bump vesting deprecation upgrade name to v6.7 Co-Authored-By: Claude Fable 5 --- sei-cosmos/x/auth/vesting/msg_server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sei-cosmos/x/auth/vesting/msg_server.go b/sei-cosmos/x/auth/vesting/msg_server.go index 9881215e27..09393d1cb5 100644 --- a/sei-cosmos/x/auth/vesting/msg_server.go +++ b/sei-cosmos/x/auth/vesting/msg_server.go @@ -14,7 +14,7 @@ import ( // module's deprecation on chains with pre-deprecation history. It must match // the plan name of the release that ships the deprecation; if this change // slips to a later release, bump this constant to that release's name. -const DeprecationUpgradeName = "v6.6" +const DeprecationUpgradeName = "v6.7" // chainsWithVestingHistory are the public chains whose history may contain // successful MsgCreateVestingAccount transactions. On these chains the