diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b7ac129de..d5f4a78d8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,10 @@ jobs: runs-on: ubuntu-latest steps: - - name: Set up Go 1.x + - name: Set up Go 1.21.x uses: actions/setup-go@v5 with: - go-version: ^1.21 + go-version: "1.21.5" id: go - name: Check out code into the Go module directory diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7e3db16e7..b4086cffb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -16,12 +16,12 @@ jobs: steps: - uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: "1.21.5" - uses: actions/checkout@v4 - name: golangci-lint uses: golangci/golangci-lint-action@v3 with: - version: v1.50.1 + version: "v1.52.2" args: --timeout=5m diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 194738d73..a3afac7ca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -32,7 +32,7 @@ jobs: steps: - uses: actions/setup-go@v5 with: - go-version: 1.21 + go-version: "1.21.5" - uses: actions/checkout@v4 @@ -114,7 +114,7 @@ jobs: - name: Setup Go with cache uses: magnetikonline/action-golang-cache@v4 with: - go-version: 1.21.0 + go-version: "1.21.5" id: go - name: Checkout repository @@ -149,7 +149,7 @@ jobs: - name: Setup Go with cache uses: magnetikonline/action-golang-cache@v4 with: - go-version: 1.21.0 + go-version: "1.21.5" id: go - name: Checkout repository diff --git a/.golangci.yml b/.golangci.yml index f5779c294..2b08da5ec 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,7 +20,6 @@ linters: - gosimple - govet - ineffassign - - interfacer - maligned - misspell - nakedret diff --git a/app/app.go b/app/app.go index 494ef9419..41e828673 100644 --- a/app/app.go +++ b/app/app.go @@ -126,6 +126,9 @@ import ( pstakeante "github.com/persistenceOne/pstake-native/v2/ante" pstakeappparams "github.com/persistenceOne/pstake-native/v2/app/params" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake" + liquidstakekeeper "github.com/persistenceOne/pstake-native/v2/x/liquidstake/keeper" + liquidstaketypes "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" "github.com/persistenceOne/pstake-native/v2/x/liquidstakeibc" liquidstakeibckeeper "github.com/persistenceOne/pstake-native/v2/x/liquidstakeibc/keeper" liquidstakeibctypes "github.com/persistenceOne/pstake-native/v2/x/liquidstakeibc/types" @@ -175,6 +178,7 @@ var ( interchainquery.AppModuleBasic{}, lscosmos.AppModuleBasic{}, liquidstakeibc.AppModuleBasic{}, + liquidstake.AppModuleBasic{}, ratesync.AppModuleBasic{}, consensus.AppModuleBasic{}, ) @@ -191,6 +195,7 @@ var ( ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, ibcfeetypes.ModuleName: nil, liquidstakeibctypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + liquidstaketypes.ModuleName: {authtypes.Minter, authtypes.Burner}, liquidstakeibctypes.DepositModuleAccount: nil, liquidstakeibctypes.UndelegationModuleAccount: {authtypes.Burner}, } @@ -249,6 +254,7 @@ type PstakeApp struct { EpochsKeeper *epochskeeper.Keeper InterchainQueryKeeper interchainquerykeeper.Keeper LiquidStakeIBCKeeper liquidstakeibckeeper.Keeper + LiquidStakeKeeper liquidstakekeeper.Keeper RatesyncKeeper *ratesynckeeper.Keeper // make scoped keepers public for test purposes @@ -302,7 +308,8 @@ func NewpStakeApp( evidencetypes.StoreKey, ibctransfertypes.StoreKey, capabilitytypes.StoreKey, feegrant.StoreKey, authzkeeper.StoreKey, icahosttypes.StoreKey, icacontrollertypes.StoreKey, epochstypes.StoreKey, interchainquerytypes.StoreKey, - ibcfeetypes.StoreKey, liquidstakeibctypes.StoreKey, consensusparamtypes.StoreKey, ratesynctypes.StoreKey, + ibcfeetypes.StoreKey, liquidstakeibctypes.StoreKey, liquidstaketypes.StoreKey, consensusparamtypes.StoreKey, + ratesynctypes.StoreKey, ) tkeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey) memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey) @@ -418,6 +425,17 @@ func NewpStakeApp( stakingtypes.NewMultiStakingHooks(app.DistrKeeper.Hooks(), app.SlashingKeeper.Hooks()), ) + app.LiquidStakeKeeper = liquidstakekeeper.NewKeeper( + appCodec, + keys[liquidstaketypes.StoreKey], + app.AccountKeeper, + app.BankKeeper, + app.StakingKeeper, + app.DistrKeeper, + app.SlashingKeeper, + app.MsgServiceRouter(), + authtypes.NewModuleAddress(govtypes.ModuleName).String(), + ) app.IBCKeeper = ibckeeper.NewKeeper( appCodec, keys[ibcexported.StoreKey], @@ -495,6 +513,7 @@ func NewpStakeApp( app.RatesyncKeeper.LiquidStakeIBCHooks())) _ = app.InterchainQueryKeeper.SetCallbackHandler(liquidstakeibctypes.ModuleName, app.LiquidStakeIBCKeeper.CallbackHandler()) + liquidStakeIBCModule := liquidstakeibc.NewIBCModule(app.LiquidStakeIBCKeeper) ibcTransferHooksKeeper := ibchookerkeeper.NewKeeper() @@ -519,9 +538,9 @@ func NewpStakeApp( ibcRouter. AddRoute(ibctransfertypes.ModuleName, transferStack). AddRoute(icahosttypes.SubModuleName, icaHostStack). - AddRoute(icacontrollertypes.SubModuleName, icaControllerStack) - //AddRoute(liquidstakeibctypes.ModuleName, icaControllerStack). - //AddRoute(ratesynctypes.ModuleName, icaControllerStack) + AddRoute(icacontrollertypes.SubModuleName, icaControllerStack). + AddRoute(liquidstakeibctypes.ModuleName, icaControllerStack). + AddRoute(ratesynctypes.ModuleName, icaControllerStack) app.IBCKeeper.SetRouter(ibcRouter) @@ -605,6 +624,7 @@ func NewpStakeApp( //ibchooker.NewAppModule(), interchainQueryModule, liquidstakeibc.NewAppModule(app.LiquidStakeIBCKeeper), + liquidstake.NewAppModule(app.LiquidStakeKeeper), ratesync.NewAppModule(appCodec, *app.RatesyncKeeper, app.AccountKeeper, app.BankKeeper), consensus.NewAppModule(appCodec, app.ConsensusParamsKeeper), ) @@ -640,6 +660,7 @@ func NewpStakeApp( ibchookertypes.ModuleName, //Noop interchainquerytypes.ModuleName, liquidstakeibctypes.ModuleName, + liquidstaketypes.ModuleName, ratesynctypes.ModuleName, consensusparamtypes.ModuleName, ) @@ -668,6 +689,7 @@ func NewpStakeApp( ibchookertypes.ModuleName, //Noop interchainquerytypes.ModuleName, liquidstakeibctypes.ModuleName, + liquidstaketypes.ModuleName, ratesynctypes.ModuleName, consensusparamtypes.ModuleName, ) @@ -703,6 +725,7 @@ func NewpStakeApp( ibchookertypes.ModuleName, //Noop interchainquerytypes.ModuleName, liquidstakeibctypes.ModuleName, + liquidstaketypes.ModuleName, ratesynctypes.ModuleName, consensusparamtypes.ModuleName, ) @@ -1004,7 +1027,7 @@ func (app *PstakeApp) RegisterUpgradeHandler() { if upgradeInfo.Name == UpgradeName && !app.UpgradeKeeper.IsSkipHeight(upgradeInfo.Height) { storeUpgrades := store.StoreUpgrades{ - Added: []string{ratesynctypes.StoreKey}, + Added: []string{ratesynctypes.StoreKey, liquidstaketypes.StoreKey}, Deleted: []string{}, } diff --git a/app/helpers/test_helpers.go b/app/helpers/test_helpers.go index c8d8f2065..4093be41e 100644 --- a/app/helpers/test_helpers.go +++ b/app/helpers/test_helpers.go @@ -442,3 +442,8 @@ func PP(data interface{}) { } fmt.Printf("%s \n", p) } + +// NoInflationCalculationFn is the function with disabled inflation. +func NoInflationCalculationFn(_ sdk.Context, minter minttypes.Minter, params minttypes.Params, bondedRatio math.LegacyDec) math.LegacyDec { + return math.LegacyNewDec(1) +} diff --git a/proto/pstake/liquidstake/v1beta1/liquidstake.proto b/proto/pstake/liquidstake/v1beta1/liquidstake.proto index 7f90d244e..0cb2ce8ae 100644 --- a/proto/pstake/liquidstake/v1beta1/liquidstake.proto +++ b/proto/pstake/liquidstake/v1beta1/liquidstake.proto @@ -40,10 +40,23 @@ message Params { (gogoproto.nullable) = false ]; - // cw_locked_pool_address defines the bech32-encoded address of + // CwLockedPoolAddress defines the bech32-encoded address of // a CW smart-contract representing a time locked LP (e.g. Superfluid LP). string cw_locked_pool_address = 6 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; + + // FeeAccountAddress defines the bech32-encoded address of + // a an account responsible for accumulating protocol fees. + string fee_account_address = 7 + [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; + + // AutocompoundFeeRate specifies the fee rate for auto redelegating the stake + // rewards. The fee is taken in favour of the fee account (see + // FeeAccountAddress). + string autocompound_fee_rate = 8 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; } // ValidatorStatus enumerates the status of a liquid validator. diff --git a/proto/pstake/liquidstakeibc/v1beta1/liquidstakeibc.proto b/proto/pstake/liquidstakeibc/v1beta1/liquidstakeibc.proto index 55de47844..cf33ce060 100644 --- a/proto/pstake/liquidstakeibc/v1beta1/liquidstakeibc.proto +++ b/proto/pstake/liquidstakeibc/v1beta1/liquidstakeibc.proto @@ -70,7 +70,7 @@ message RewardParams { // rewards denom on the host chain string denom = 1; // entity which will convert rewards to the host denom - string destination = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + string destination = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; } message HostChainLSParams { diff --git a/x/liquidstake/abci.go b/x/liquidstake/abci.go index e6cfce743..33d98aebb 100644 --- a/x/liquidstake/abci.go +++ b/x/liquidstake/abci.go @@ -14,8 +14,6 @@ import ( func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) - k.UpdateLiquidValidatorSet(ctx) - - whitelistedValsMap := types.GetWhitelistedValsMap(k.GetParams(ctx).WhitelistedValidators) - k.AutocompoundStakingRewards(ctx, whitelistedValsMap) + // return value of UpdateLiquidValidatorSet is useful only in testing + _ = k.UpdateLiquidValidatorSet(ctx) } diff --git a/x/liquidstake/handler.go b/x/liquidstake/handler.go index 21c9253e1..432c91587 100644 --- a/x/liquidstake/handler.go +++ b/x/liquidstake/handler.go @@ -1,6 +1,7 @@ package liquidstake import ( + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -29,7 +30,7 @@ func NewHandler(k keeper.Keeper) sdk.Handler { return sdk.WrapServiceResult(ctx, res, err) default: - return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg) + return nil, errors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", types.ModuleName, msg) } } } diff --git a/x/liquidstake/keeper/genesis.go b/x/liquidstake/keeper/genesis.go index 1793ae34a..c3d6e92eb 100644 --- a/x/liquidstake/keeper/genesis.go +++ b/x/liquidstake/keeper/genesis.go @@ -13,11 +13,15 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) { if err := types.ValidateGenesis(genState); err != nil { panic(err) } + // init to prevent nil slice, []types.WhitelistedValidator(nil) if genState.Params.WhitelistedValidators == nil || len(genState.Params.WhitelistedValidators) == 0 { genState.Params.WhitelistedValidators = []types.WhitelistedValidator{} } - k.SetParams(ctx, genState.Params) + + if err := k.SetParams(ctx, genState.Params); err != nil { + panic(err) + } for _, lv := range genState.LiquidValidators { k.SetLiquidValidator(ctx, lv) @@ -32,6 +36,7 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) { // ExportGenesis returns the liquidstake module's genesis state. func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState { params := k.GetParams(ctx) + // init to prevent nil slice, []types.WhitelistedValidator(nil) if params.WhitelistedValidators == nil || len(params.WhitelistedValidators) == 0 { params.WhitelistedValidators = []types.WhitelistedValidator{} diff --git a/x/liquidstake/keeper/genesis_test.go b/x/liquidstake/keeper/genesis_test.go new file mode 100644 index 000000000..70bead2fa --- /dev/null +++ b/x/liquidstake/keeper/genesis_test.go @@ -0,0 +1,68 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + _ "github.com/stretchr/testify/suite" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func (s *KeeperTestSuite) TestInitGenesis() { + genState := *types.DefaultGenesisState() + s.keeper.InitGenesis(s.ctx, genState) + got := s.keeper.ExportGenesis(s.ctx) + s.Require().Equal(genState, *got) +} + +func (s *KeeperTestSuite) TestImportExportGenesis() { + k, ctx := s.keeper, s.ctx + _, valOpers, _ := s.CreateValidators([]int64{1000000, 1000000, 1000000}) + params := k.GetParams(ctx) + + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + } + k.SetParams(ctx, params) + k.UpdateLiquidValidatorSet(ctx) + + stakingAmt := math.NewInt(100000000) + s.Require().NoError(s.liquidStaking(s.delAddrs[0], stakingAmt)) + lvs := k.GetAllLiquidValidators(ctx) + s.Require().Len(lvs, 2) + + lvStates := k.GetAllLiquidValidatorStates(ctx) + genState := k.ExportGenesis(ctx) + + bz := s.app.AppCodec().MustMarshalJSON(genState) + + var genState2 types.GenesisState + s.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + k.InitGenesis(ctx, genState2) + genState3 := k.ExportGenesis(ctx) + + s.Require().Equal(*genState, genState2) + s.Require().Equal(genState2, *genState3) + + lvs = k.GetAllLiquidValidators(ctx) + s.Require().Len(lvs, 2) + + lvStates3 := k.GetAllLiquidValidatorStates(ctx) + s.Require().EqualValues(lvStates, lvStates3) +} + +func (s *KeeperTestSuite) TestImportExportGenesisEmpty() { + k, ctx := s.keeper, s.ctx + k.SetParams(ctx, types.DefaultParams()) + k.UpdateLiquidValidatorSet(ctx) + genState := k.ExportGenesis(ctx) + + var genState2 types.GenesisState + bz := s.app.AppCodec().MustMarshalJSON(genState) + s.app.AppCodec().MustUnmarshalJSON(bz, &genState2) + k.InitGenesis(ctx, genState2) + + genState3 := k.ExportGenesis(ctx) + s.Require().Equal(*genState, genState2) + s.Require().Equal(genState2, *genState3) +} diff --git a/x/liquidstake/keeper/grpc_query.go b/x/liquidstake/keeper/grpc_query.go index 63a242871..221034541 100644 --- a/x/liquidstake/keeper/grpc_query.go +++ b/x/liquidstake/keeper/grpc_query.go @@ -38,7 +38,7 @@ func (k Querier) LiquidValidators(c context.Context, req *types.QueryLiquidValid return &types.QueryLiquidValidatorsResponse{LiquidValidators: k.GetAllLiquidValidatorStates(ctx)}, nil } -// States queries states of liquid staking module. +// States queries states of liquid stake module. func (k Querier) States(c context.Context, req *types.QueryStatesRequest) (*types.QueryStatesResponse, error) { if req == nil { return nil, status.Error(codes.InvalidArgument, "invalid request") diff --git a/x/liquidstake/keeper/grpc_query_test.go b/x/liquidstake/keeper/grpc_query_test.go new file mode 100644 index 000000000..af48220b6 --- /dev/null +++ b/x/liquidstake/keeper/grpc_query_test.go @@ -0,0 +1,64 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + _ "github.com/stretchr/testify/suite" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func (s *KeeperTestSuite) TestGRPCParams() { + resp, err := s.querier.Params(sdk.WrapSDKContext(s.ctx), &types.QueryParamsRequest{}) + s.Require().NoError(err) + s.Require().Equal(s.keeper.GetParams(s.ctx), resp.Params) +} + +func (s *KeeperTestSuite) TestGRPCQueries() { + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + params.MinLiquidStakeAmount = math.NewInt(50000) + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // Test LiquidValidators grpc query + res := s.keeper.GetAllLiquidValidatorStates(s.ctx) + resp, err := s.querier.LiquidValidators(sdk.WrapSDKContext(s.ctx), &types.QueryLiquidValidatorsRequest{}) + s.Require().NoError(err) + s.Require().Equal(resp.LiquidValidators, res) + + resp, err = s.querier.LiquidValidators(sdk.WrapSDKContext(s.ctx), nil) + s.Require().Nil(resp) + s.Require().ErrorIs(err, status.Error(codes.InvalidArgument, "invalid request")) + + // Test States grpc query + respStates, err := s.querier.States(sdk.WrapSDKContext(s.ctx), &types.QueryStatesRequest{}) + resNetAmountState := s.keeper.GetNetAmountState(s.ctx) + s.Require().NoError(err) + s.Require().Equal(respStates.NetAmountState, resNetAmountState) + + respStates, err = s.querier.States(sdk.WrapSDKContext(s.ctx), nil) + s.Require().Nil(respStates) + s.Require().ErrorIs(err, status.Error(codes.InvalidArgument, "invalid request")) + + // Test Params grpc query + respParams, err := s.querier.Params(sdk.WrapSDKContext(s.ctx), &types.QueryParamsRequest{}) + resParams := s.keeper.GetParams(s.ctx) + s.Require().NoError(err) + s.Require().Equal(respParams.Params.LiquidBondDenom, resParams.LiquidBondDenom) + s.Require().Equal(respParams.Params.WhitelistedValidators[0].ValidatorAddress, valOpers[0].String()) + s.Require().Equal(respParams.Params.WhitelistedValidators[1].ValidatorAddress, valOpers[1].String()) + s.Require().Equal(respParams.Params.WhitelistedValidators[2].ValidatorAddress, valOpers[2].String()) +} diff --git a/x/liquidstake/keeper/keeper_test.go b/x/liquidstake/keeper/keeper_test.go new file mode 100644 index 000000000..a641af3e7 --- /dev/null +++ b/x/liquidstake/keeper/keeper_test.go @@ -0,0 +1,465 @@ +package keeper_test + +import ( + "fmt" + "testing" + "time" + + "cosmossdk.io/math" + "github.com/stretchr/testify/suite" + + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/cosmos/cosmos-sdk/x/mint" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + chain "github.com/persistenceOne/pstake-native/v2/app" + testhelpers "github.com/persistenceOne/pstake-native/v2/app/helpers" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/keeper" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +var ( + BlockTime = 10 * time.Second +) + +type KeeperTestSuite struct { + suite.Suite + + app *chain.PstakeApp + ctx sdk.Context + keeper keeper.Keeper + querier keeper.Querier + addrs []sdk.AccAddress + delAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (s *KeeperTestSuite) SetupTest() { + s.app = testhelpers.Setup(s.T(), false, 5) + s.ctx = s.app.BaseApp.NewContext(false, tmproto.Header{}) + stakingParams := stakingtypes.DefaultParams() + stakingParams.MaxEntries = 7 + stakingParams.MaxValidators = 30 + s.Require().NoError(s.app.StakingKeeper.SetParams(s.ctx, stakingParams)) + + s.keeper = s.app.LiquidStakeKeeper + s.querier = keeper.Querier{Keeper: s.keeper} + s.addrs = testhelpers.AddTestAddrs(s.app, s.ctx, 10, math.NewInt(1_000_000_000)) + s.delAddrs = testhelpers.AddTestAddrs(s.app, s.ctx, 10, math.NewInt(1_000_000_000)) + s.valAddrs = testhelpers.ConvertAddrsToValAddrs(s.delAddrs) + + s.ctx = s.ctx.WithBlockHeight(100).WithBlockTime(testhelpers.ParseTime("2022-03-01T00:00:00Z")) + params := s.keeper.GetParams(s.ctx) + params.UnstakeFeeRate = sdk.ZeroDec() + params.AutocompoundFeeRate = types.DefaultAutocompoundFeeRate + s.Require().NoError(s.keeper.SetParams(s.ctx, params)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + // call mint.BeginBlocker for init k.SetLastBlockTime(ctx, ctx.BlockTime()) + mint.BeginBlocker(s.ctx, s.app.MintKeeper, minttypes.DefaultInflationCalculationFn) +} + +func (s *KeeperTestSuite) TearDownTest() { + // invariant check + crisis.EndBlocker(s.ctx, *s.app.CrisisKeeper) +} + +func (s *KeeperTestSuite) CreateValidators(powers []int64) ([]sdk.AccAddress, []sdk.ValAddress, []cryptotypes.PubKey) { + s.app.BeginBlocker(s.ctx, abci.RequestBeginBlock{}) + num := len(powers) + addrs := testhelpers.AddTestAddrsIncremental(s.app, s.ctx, num, math.NewInt(1000000000)) + valAddrs := testhelpers.ConvertAddrsToValAddrs(addrs) + pks := testhelpers.CreateTestPubKeys(num) + + for i, power := range powers { + val, err := stakingtypes.NewValidator(valAddrs[i], pks[i], stakingtypes.Description{}) + s.Require().NoError(err) + s.app.StakingKeeper.SetValidator(s.ctx, val) + err = s.app.StakingKeeper.SetValidatorByConsAddr(s.ctx, val) + s.Require().NoError(err) + s.app.StakingKeeper.SetNewValidatorByPowerIndex(s.ctx, val) + s.app.StakingKeeper.Hooks().AfterValidatorCreated(s.ctx, val.GetOperator()) + newShares, err := s.app.StakingKeeper.Delegate(s.ctx, addrs[i], math.NewInt(power), stakingtypes.Unbonded, val, true) + s.Require().NoError(err) + s.Require().Equal(newShares.TruncateInt(), math.NewInt(power)) + } + + s.app.EndBlocker(s.ctx, abci.RequestEndBlock{}) + return addrs, valAddrs, pks +} + +func (s *KeeperTestSuite) liquidStaking(liquidStaker sdk.AccAddress, stakingAmt math.Int) error { + ctx, writeCache := s.ctx.CacheContext() + params := s.keeper.GetParams(ctx) + + stkxprtBalanceBefore := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, params.LiquidBondDenom, + ).Amount + + newShares, stkXPRTMintAmt, err := s.keeper.LiquidStake( + ctx, + types.LiquidStakeProxyAcc, + liquidStaker, + sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt), + ) + if err != nil { + return err + } + + stkxprtBalanceAfter := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, params.LiquidBondDenom, + ).Amount + + s.Require().NoError(err) + s.NotEqualValues(newShares, sdk.ZeroDec()) + s.Require().EqualValues( + stkXPRTMintAmt, stkxprtBalanceAfter.Sub(stkxprtBalanceBefore), + ) + writeCache() + + return nil +} + +func (s *KeeperTestSuite) liquidUnstaking( + liquidStaker sdk.AccAddress, + ubdStkXPRTAmt math.Int, + ubdComplete bool, +) error { + ctx := s.ctx + params := s.keeper.GetParams(ctx) + + balanceBefore := s.app.BankKeeper.GetBalance( + ctx, + liquidStaker, + sdk.DefaultBondDenom, + ).Amount + + ubdTime, unbondingAmt, dels, unbondedAmt, err := s.liquidUnstakingWithResult( + liquidStaker, + sdk.NewCoin(params.LiquidBondDenom, ubdStkXPRTAmt), + ) + if err != nil { + return err + } + + testhelpers.PP(fmt.Sprintf("GOT UBDS FROM liquidUnstakingWithResult: %d", len(dels))) + + if ubdComplete { + alv := s.keeper.GetActiveLiquidValidators(ctx, params.WhitelistedValsMap()) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 200). + WithBlockTime(ubdTime.Add(1)) + + // EndBlock of staking keeper, mature UBD + s.app.StakingKeeper.BlockValidatorUpdates(ctx) + + balanceCompleteUBD := s.app.BankKeeper.GetBalance( + ctx, + liquidStaker, + sdk.DefaultBondDenom, + ) + for _, v := range alv { + _, found := s.app.StakingKeeper.GetUnbondingDelegation( + ctx, + liquidStaker, + v.GetOperator(), + ) + s.Require().False(found) + } + + s.Require().EqualValues( + balanceCompleteUBD.Amount, + balanceBefore.Add(unbondingAmt).Add(unbondedAmt), + ) + } + + return nil +} + +func (s *KeeperTestSuite) liquidUnstakingWithResult( + liquidStaker sdk.AccAddress, unstakingStkXPRT sdk.Coin, +) (time.Time, math.Int, []stakingtypes.UnbondingDelegation, math.Int, error) { + ctx, writeCache := s.ctx.CacheContext() + params := s.keeper.GetParams(ctx) + alv := s.keeper.GetActiveLiquidValidators(ctx, params.WhitelistedValsMap()) + + balanceBefore := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, sdk.DefaultBondDenom, + ).Amount + stkxprtBalanceBefore := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, params.LiquidBondDenom, + ).Amount + + ubdTime, unbondingAmt, ubds, unbondedAmt, err := s.keeper.LiquidUnstake( + ctx, types.LiquidStakeProxyAcc, liquidStaker, unstakingStkXPRT, + ) + if err != nil { + return ubdTime, unbondingAmt, ubds, unbondedAmt, err + } + + balanceAfter := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, sdk.DefaultBondDenom, + ).Amount + stkxprtBalanceAfter := s.app.BankKeeper.GetBalance( + ctx, liquidStaker, params.LiquidBondDenom, + ).Amount + s.Require().EqualValues( + unstakingStkXPRT.Amount, stkxprtBalanceBefore.Sub(stkxprtBalanceAfter), + ) + + if unbondedAmt.IsPositive() { + s.Require().EqualValues( + unbondedAmt, balanceAfter.Sub(balanceBefore), + ) + } + + for _, v := range alv { + _, found := s.app.StakingKeeper.GetUnbondingDelegation( + ctx, liquidStaker, v.GetOperator(), + ) + s.Require().True(found) + } + + writeCache() + return ubdTime, unbondingAmt, ubds, unbondedAmt, err +} + +func (s *KeeperTestSuite) RequireNetAmountStateZero() { + nas := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(nas.MintRate, sdk.ZeroDec()) + s.Require().EqualValues(nas.StkxprtTotalSupply, sdk.ZeroInt()) + s.Require().EqualValues(nas.NetAmount, sdk.ZeroDec()) + s.Require().EqualValues(nas.TotalDelShares, sdk.ZeroDec()) + s.Require().EqualValues(nas.TotalLiquidTokens, sdk.ZeroInt()) + s.Require().EqualValues(nas.TotalRemainingRewards, sdk.ZeroDec()) + s.Require().EqualValues(nas.TotalUnbondingBalance, sdk.ZeroDec()) + s.Require().EqualValues(nas.ProxyAccBalance, sdk.ZeroInt()) + +} + +// advance block time and height for complete redelegations and unbondings +func (s *KeeperTestSuite) completeRedelegationUnbonding() { + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100). + WithBlockTime(s.ctx.BlockTime().Add(stakingtypes.DefaultUnbondingTime)) + s.app.EndBlocker(s.ctx, abci.RequestEndBlock{}) + reds := s.app.StakingKeeper.GetRedelegations(s.ctx, types.LiquidStakeProxyAcc, 100) + s.Require().Len(reds, 0) + ubds := s.app.StakingKeeper.GetUnbondingDelegations(s.ctx, types.LiquidStakeProxyAcc, 100) + s.Require().Len(ubds, 0) +} + +func (s *KeeperTestSuite) redelegationsErrorCount(redelegations []types.Redelegation) int { + errCnt := 0 + for _, red := range redelegations { + if red.Error != nil { + errCnt++ + } + } + return errCnt +} + +func (s *KeeperTestSuite) printRedelegationsLiquidTokens() { + redsIng := s.app.StakingKeeper.GetRedelegations(s.ctx, types.LiquidStakeProxyAcc, 50) + if len(redsIng) != 0 { + fmt.Println("[Redelegations]") + for i, red := range redsIng { + fmt.Println("\tRedelegation #", i+1) + fmt.Println("\t\tDelegatorAddress: ", red.DelegatorAddress) + fmt.Println("\t\tValidatorSrcAddress : ", red.ValidatorSrcAddress) + fmt.Println("\t\tValidatorDstAddress: ", red.ValidatorDstAddress) + fmt.Println("\t\tEntries: ") + for _, e := range red.Entries { + fmt.Println("\t\t\tCreationHeight: ", e.CreationHeight) + fmt.Println("\t\t\tCompletionTime: ", e.CompletionTime) + fmt.Println("\t\t\tInitialBalance: ", e.InitialBalance) + fmt.Println("\t\t\tSharesDst: ", e.SharesDst) + } + } + fmt.Println("") + } + liquidVals := s.keeper.GetAllLiquidValidators(s.ctx) + if len(liquidVals) != 0 { + fmt.Println("[LiquidValidators]") + for _, v := range s.keeper.GetAllLiquidValidators(s.ctx) { + fmt.Printf(" OperatorAddress %s; LiquidTokens: %s\n", + v.OperatorAddress, v.GetLiquidTokens(s.ctx, s.app.StakingKeeper, false)) + } + } +} + +func (s *KeeperTestSuite) advanceHeight(height int, withBeginBlock bool) { + feeCollector := s.app.AccountKeeper.GetModuleAddress( + authtypes.FeeCollectorName, + ) + + for i := 0; i < height; i++ { + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 1). + WithBlockTime(s.ctx.BlockTime().Add(BlockTime)) + + mint.BeginBlocker(s.ctx, s.app.MintKeeper, minttypes.DefaultInflationCalculationFn) + feeCollectorBalance := s.app.BankKeeper.GetAllBalances( + s.ctx, feeCollector, + ) + rewardsToBeDistributed := feeCollectorBalance.AmountOf( + sdk.DefaultBondDenom, + ) + + // mimic distribution.BeginBlock (AllocateTokens, get rewards from + // feeCollector, AllocateTokensToValidator, add remaining to feePool) + err := s.app.BankKeeper.SendCoinsFromModuleToModule( + s.ctx, authtypes.FeeCollectorName, distrtypes.ModuleName, + feeCollectorBalance, + ) + + s.Require().NoError(err) + totalRewards := sdk.ZeroDec() + totalPower := int64(0) + s.app.StakingKeeper.IterateBondedValidatorsByPower( + s.ctx, + func(index int64, validator stakingtypes.ValidatorI) (stop bool) { + consPower := validator.GetConsensusPower( + s.app.StakingKeeper.PowerReduction(s.ctx), + ) + totalPower = totalPower + consPower + return false + }, + ) + + if totalPower != 0 { + s.app.StakingKeeper.IterateBondedValidatorsByPower( + s.ctx, + func(index int64, validator stakingtypes.ValidatorI) (stop bool) { + consPower := validator.GetConsensusPower( + s.app.StakingKeeper.PowerReduction(s.ctx), + ) + powerFraction := math.LegacyNewDec(consPower).QuoTruncate( + math.LegacyNewDec(totalPower), + ) + reward := rewardsToBeDistributed.ToLegacyDec().MulTruncate( + powerFraction, + ) + + s.app.DistrKeeper.AllocateTokensToValidator( + s.ctx, validator, + sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: reward}}, + ) + + totalRewards = totalRewards.Add(reward) + return false + }, + ) + } + + remaining := rewardsToBeDistributed.ToLegacyDec().Sub(totalRewards) + s.Require().False(remaining.GT(math.LegacyNewDec(1))) + feePool := s.app.DistrKeeper.GetFeePool(s.ctx) + feePool.CommunityPool = feePool.CommunityPool.Add( + sdk.DecCoins{{Denom: sdk.DefaultBondDenom, Amount: remaining}}..., + ) + + s.app.DistrKeeper.SetFeePool(s.ctx, feePool) + if withBeginBlock { + // liquid validator set update, rebalancing, withdraw rewards, + // re-stake + liquidstake.BeginBlocker(s.ctx, s.app.LiquidStakeKeeper) + } + + staking.EndBlocker(s.ctx, s.app.StakingKeeper) + } +} + +// doubleSign, tombstone, slash, jail +func (s *KeeperTestSuite) doubleSign(valOper sdk.ValAddress, consAddr sdk.ConsAddress) { + liquidValidator, found := s.keeper.GetLiquidValidator(s.ctx, valOper) + s.Require().True(found) + val, found := s.app.StakingKeeper.GetValidator(s.ctx, valOper) + s.Require().True(found) + tokens := val.Tokens + liquidTokens := liquidValidator.GetLiquidTokens(s.ctx, s.app.StakingKeeper, false) + + // check sign info + info, found := s.app.SlashingKeeper.GetValidatorSigningInfo(s.ctx, consAddr) + s.Require().True(found) + s.Require().Equal(info.Address, consAddr.String()) + + // make evidence + evidence := &evidencetypes.Equivocation{ + //Height: 0, + //Time: time.Unix(0, 0), + Height: s.ctx.BlockHeight(), + Time: s.ctx.BlockTime(), + Power: s.app.StakingKeeper.TokensToConsensusPower(s.ctx, tokens), + ConsensusAddress: consAddr.String(), + } + + // Double sign + s.app.EvidenceKeeper.HandleEquivocationEvidence(s.ctx, evidence) + // HandleEquivocationEvidence call below functions + //s.app.SlashingKeeper.Slash() + //s.app.SlashingKeeper.Jail(s.ctx, consAddr) + //s.app.SlashingKeeper.JailUntil(s.ctx, consAddr, evidencetypes.DoubleSignJailEndTime) + //s.app.SlashingKeeper.Tombstone(s.ctx, consAddr) + + // should be jailed and tombstoned + s.Require().True(s.app.StakingKeeper.Validator(s.ctx, liquidValidator.GetOperator()).IsJailed()) + s.Require().True(s.app.SlashingKeeper.IsTombstoned(s.ctx, consAddr)) + + // check tombstoned on sign info + info, found = s.app.SlashingKeeper.GetValidatorSigningInfo(s.ctx, consAddr) + s.Require().True(found) + s.Require().True(info.Tombstoned) + val, _ = s.app.StakingKeeper.GetValidator(s.ctx, valOper) + s.Require().True(s.keeper.IsTombstoned(s.ctx, val)) + liquidTokensSlashed := liquidValidator.GetLiquidTokens(s.ctx, s.app.StakingKeeper, false) + tokensSlashed := val.Tokens + s.Require().True(tokensSlashed.LT(tokens)) + s.Require().True(liquidTokensSlashed.LT(liquidTokens)) + + s.app.StakingKeeper.BlockValidatorUpdates(s.ctx) + val, _ = s.app.StakingKeeper.GetValidator(s.ctx, valOper) + + // set unbonding status, no more rewards before return Bonded + s.Require().Equal(val.Status, stakingtypes.Unbonding) +} + +func (s *KeeperTestSuite) createContinuousVestingAccount( + from sdk.AccAddress, to sdk.AccAddress, amt sdk.Coins, + startTime, endTime time.Time, +) vestingtypes.ContinuousVestingAccount { + baseAccount := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, to) + _, ok := baseAccount.(*authtypes.BaseAccount) + s.Require().True(ok) + baseVestingAccount := vestingtypes.NewBaseVestingAccount( + baseAccount.(*authtypes.BaseAccount), amt, endTime.Unix(), + ) + + cVestingAcc := vestingtypes.NewContinuousVestingAccountRaw( + baseVestingAccount, startTime.Unix(), + ) + + s.app.AccountKeeper.SetAccount(s.ctx, cVestingAcc) + err := s.app.BankKeeper.SendCoins(s.ctx, from, to, amt) + s.Require().NoError(err) + + return *cVestingAcc +} + +func (s *KeeperTestSuite) fundAddr(addr sdk.AccAddress, amt sdk.Coins) { + err := s.app.BankKeeper.MintCoins(s.ctx, "mint", amt) + s.Require().NoError(err) + err = s.app.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, "mint", addr, amt) + s.Require().NoError(err) +} diff --git a/x/liquidstake/keeper/liquidstake.go b/x/liquidstake/keeper/liquidstake.go index 5112bbc11..5099c5549 100644 --- a/x/liquidstake/keeper/liquidstake.go +++ b/x/liquidstake/keeper/liquidstake.go @@ -4,6 +4,7 @@ import ( "encoding/json" "time" + "cosmossdk.io/errors" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -48,10 +49,10 @@ func (k Keeper) GetNetAmountState(ctx sdk.Context) (nas types.NetAmountState) { // LiquidStake mints stkXPRT worth of staking coin value according to NetAmount and performs LiquidDelegate. func (k Keeper) LiquidStake( - ctx sdk.Context, proxyAcc, liquidStaker sdk.AccAddress, stakingCoin sdk.Coin) (newShares sdk.Dec, stkXPRTMintAmount math.Int, err error) { + ctx sdk.Context, proxyAcc, liquidStaker sdk.AccAddress, stakingCoin sdk.Coin) (newShares math.LegacyDec, stkXPRTMintAmount math.Int, err error) { params := k.GetParams(ctx) - // check minimum liquid staking amount + // check minimum liquid stake amount if stakingCoin.Amount.LT(params.MinLiquidStakeAmount) { return sdk.ZeroDec(), sdk.ZeroInt(), types.ErrLessThanMinLiquidStakeAmount } @@ -59,7 +60,7 @@ func (k Keeper) LiquidStake( // check bond denomination bondDenom := k.stakingKeeper.BondDenom(ctx) if stakingCoin.Denom != bondDenom { - return sdk.ZeroDec(), sdk.ZeroInt(), sdkerrors.Wrapf( + return sdk.ZeroDec(), sdk.ZeroInt(), errors.Wrapf( types.ErrInvalidBondDenom, "invalid coin denomination: got %s, expected %s", stakingCoin.Denom, bondDenom, ) } @@ -73,7 +74,7 @@ func (k Keeper) LiquidStake( // NetAmount must be calculated before send nas := k.GetNetAmountState(ctx) - // send staking coin to liquid staking proxy account to proxy delegation, need sufficient spendable balances + // send staking coin to liquid stake proxy account to proxy delegation, need sufficient spendable balances err = k.bankKeeper.SendCoins(ctx, liquidStaker, proxyAcc, sdk.NewCoins(stakingCoin)) if err != nil { return sdk.ZeroDec(), sdk.ZeroInt(), err @@ -105,9 +106,9 @@ func (k Keeper) LiquidStake( return newShares, stkXPRTMintAmount, err } -// LockOnLP sends tokens to CW contract (Superfluid LP) with time locking. +// LockOnLP sends tokens to a CW contract (Superfluid LP) with time locking. // It performs a CosmWasm execution through global message handler and may fail. -// Emits events on a successfull call. +// Emits events on a successful call. func (k Keeper) LockOnLP(ctx sdk.Context, delegator sdk.AccAddress, amount sdk.Coin) ([]byte, error) { params := k.GetParams(ctx) @@ -117,7 +118,7 @@ func (k Keeper) LockOnLP(ctx sdk.Context, delegator sdk.AccAddress, amount sdk.C return nil, types.ErrInvalidDenom.Wrapf("cannot lock any denom on LP except liquid bond denom: %s", params.LiquidBondDenom) } - msg := &LockLstAssetForUserMsg{ + msg := &LockLstAssetMsg{ Asset: Asset{ Amount: amount.Amount.String(), Info: AssetInfo{ @@ -126,15 +127,13 @@ func (k Keeper) LockOnLP(ctx sdk.Context, delegator sdk.AccAddress, amount sdk.C }, }, }, - - User: delegator.String(), } callData, err := json.Marshal(&ExecMsg{ - LockLstAssetForUser: msg, + LockLstAsset: msg, }) if err != nil { - panic("failed to marshal CW contract call LockLstAssetForUser") + panic("failed to marshal CW contract call LockLstAsset") } cwMsg := &wasmtypes.MsgExecuteContract{ @@ -168,12 +167,11 @@ func (k Keeper) LockOnLP(ctx sdk.Context, delegator sdk.AccAddress, amount sdk.C } type ExecMsg struct { - LockLstAssetForUser *LockLstAssetForUserMsg `json:"lock_lst_asset_for_user,omitmepty"` + LockLstAsset *LockLstAssetMsg `json:"lock_lst_asset,omitempty"` } -type LockLstAssetForUserMsg struct { - Asset Asset `json:"asset"` - User string `json:"user"` +type LockLstAssetMsg struct { + Asset Asset `json:"asset"` } type Asset struct { @@ -197,14 +195,14 @@ func (k Keeper) LSMDelegate( validator sdk.ValAddress, proxyAcc sdk.AccAddress, stakingCoin sdk.Coin, -) (newShares sdk.Dec, stkXPRTMintAmount math.Int, err error) { +) (newShares math.LegacyDec, stkXPRTMintAmount math.Int, err error) { params := k.GetParams(ctx) if params.LsmDisabled { return sdk.ZeroDec(), sdk.ZeroInt(), types.ErrDisabledLSM } - // check minimum liquid staking amount + // check minimum liquid stake amount if stakingCoin.Amount.LT(params.MinLiquidStakeAmount) { return sdk.ZeroDec(), sdk.ZeroInt(), types.ErrLessThanMinLiquidStakeAmount } @@ -212,7 +210,7 @@ func (k Keeper) LSMDelegate( // check bond denomination bondDenom := k.stakingKeeper.BondDenom(ctx) if stakingCoin.Denom != bondDenom { - return sdk.ZeroDec(), sdk.ZeroInt(), sdkerrors.Wrapf( + return sdk.ZeroDec(), sdk.ZeroInt(), errors.Wrapf( types.ErrInvalidBondDenom, "invalid coin denomination: got %s, expected %s", stakingCoin.Denom, bondDenom, ) } @@ -319,7 +317,7 @@ func (k Keeper) LSMDelegate( } // LiquidDelegate delegates staking amount to active validators by proxy account. -func (k Keeper) LiquidDelegate(ctx sdk.Context, proxyAcc sdk.AccAddress, activeVals types.ActiveLiquidValidators, stakingAmt math.Int, whitelistedValsMap types.WhitelistedValsMap) (newShares sdk.Dec, err error) { +func (k Keeper) LiquidDelegate(ctx sdk.Context, proxyAcc sdk.AccAddress, activeVals types.ActiveLiquidValidators, stakingAmt math.Int, whitelistedValsMap types.WhitelistedValsMap) (newShares math.LegacyDec, err error) { totalNewShares := sdk.ZeroDec() // crumb may occur due to a decimal point error in dividing the staking amount into the weight of liquid validators, It added on first active liquid validator weightedAmt, crumb := types.DivideByWeight(activeVals, stakingAmt, whitelistedValsMap) @@ -350,7 +348,7 @@ func (k Keeper) LiquidUnstake( params := k.GetParams(ctx) liquidBondDenom := k.LiquidBondDenom(ctx) if unstakingStkXPRT.Denom != liquidBondDenom { - return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), sdkerrors.Wrapf( + return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), errors.Wrapf( types.ErrInvalidLiquidBondDenom, "invalid coin denomination: got %s, expected %s", unstakingStkXPRT.Denom, liquidBondDenom, ) } @@ -387,18 +385,26 @@ func (k Keeper) LiquidUnstake( // if no totalLiquidTokens, withdraw directly from balance of proxy acc if !totalLiquidTokens.IsPositive() { if nas.ProxyAccBalance.GTE(unbondingAmountInt) { - err = k.bankKeeper.SendCoins(ctx, types.LiquidStakeProxyAcc, liquidStaker, - sdk.NewCoins(sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), unbondingAmountInt))) + err = k.bankKeeper.SendCoins( + ctx, + types.LiquidStakeProxyAcc, + liquidStaker, + sdk.NewCoins(sdk.NewCoin( + k.stakingKeeper.BondDenom(ctx), + unbondingAmountInt, + )), + ) if err != nil { return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), err - } else { - return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, unbondingAmountInt, nil } - } else { - // error case where there is a quantity that are unbonding balance or remaining rewards that is not re-stake or withdrawn in netAmount. - return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), types.ErrInsufficientProxyAccBalance + + return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, unbondingAmountInt, nil } + + // error case where there is a quantity that are unbonding balance or remaining rewards that is not re-stake or withdrawn in netAmount. + return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), types.ErrInsufficientProxyAccBalance } + // fail when no liquid validators to unbond if liquidVals.Len() == 0 { return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), types.ErrLiquidValidatorsNotExists @@ -409,39 +415,47 @@ func (k Keeper) LiquidUnstake( if !unbondingAmount.Sub(crumb).IsPositive() { return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), types.ErrTooSmallLiquidUnstakingAmount } + totalReturnAmount := sdk.ZeroInt() + var ubdTime time.Time - var ubds []stakingtypes.UnbondingDelegation + ubds := make([]stakingtypes.UnbondingDelegation, 0, len(liquidVals)) for i, val := range liquidVals { // skip zero weight liquid validator if !unbondingAmounts[i].IsPositive() { continue } + var ubd stakingtypes.UnbondingDelegation var returnAmount math.Int - var weightedShare sdk.Dec + var weightedShare math.LegacyDec + // calculate delShares from tokens with validation weightedShare, err = k.stakingKeeper.ValidateUnbondAmount(ctx, proxyAcc, val.GetOperator(), unbondingAmounts[i].TruncateInt()) if err != nil { return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), err } + if !weightedShare.IsPositive() { continue } + // unbond with weightedShare ubdTime, returnAmount, ubd, err = k.LiquidUnbond(ctx, proxyAcc, liquidStaker, val.GetOperator(), weightedShare, true) if err != nil { return time.Time{}, sdk.ZeroInt(), []stakingtypes.UnbondingDelegation{}, sdk.ZeroInt(), err } + ubds = append(ubds, ubd) totalReturnAmount = totalReturnAmount.Add(returnAmount) } + return ubdTime, totalReturnAmount, ubds, sdk.ZeroInt(), nil } // LiquidUnbond unbond delegation shares to active validators by proxy account. func (k Keeper) LiquidUnbond( - ctx sdk.Context, proxyAcc, liquidStaker sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec, checkMaxEntries bool, + ctx sdk.Context, proxyAcc, liquidStaker sdk.AccAddress, valAddr sdk.ValAddress, shares math.LegacyDec, checkMaxEntries bool, ) (time.Time, math.Int, stakingtypes.UnbondingDelegation, error) { validator, found := k.stakingKeeper.GetValidator(ctx, valAddr) if !found { @@ -476,7 +490,7 @@ func (k Keeper) LiquidUnbond( } // CheckDelegationStates returns total remaining rewards, delshares, liquid tokens of delegations by proxy account -func (k Keeper) CheckDelegationStates(ctx sdk.Context, proxyAcc sdk.AccAddress) (sdk.Dec, sdk.Dec, math.Int) { +func (k Keeper) CheckDelegationStates(ctx sdk.Context, proxyAcc sdk.AccAddress) (math.LegacyDec, math.LegacyDec, math.Int) { bondDenom := k.stakingKeeper.BondDenom(ctx) totalRewards := sdk.ZeroDec() totalDelShares := sdk.ZeroDec() diff --git a/x/liquidstake/keeper/liquidstake_test.go b/x/liquidstake/keeper/liquidstake_test.go new file mode 100644 index 000000000..ca66f2201 --- /dev/null +++ b/x/liquidstake/keeper/liquidstake_test.go @@ -0,0 +1,412 @@ +package keeper_test + +import ( + "time" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + testhelpers "github.com/persistenceOne/pstake-native/v2/app/helpers" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +// tests LiquidStake, LiquidUnstake +func (s *KeeperTestSuite) TestLiquidStake() { + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + params.MinLiquidStakeAmount = math.NewInt(50000) + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := params.MinLiquidStakeAmount + + // fail, no active validator + cachedCtx, _ := s.ctx.CacheContext() + newShares, stkXPRTMintAmt, err := s.keeper.LiquidStake( + cachedCtx, types.LiquidStakeProxyAcc, s.delAddrs[0], + sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt), + ) + s.Require().ErrorIs(err, types.ErrActiveLiquidValidatorsNotExists) + s.Require().Equal(newShares, sdk.ZeroDec()) + s.Require().Equal(stkXPRTMintAmt, sdk.ZeroInt()) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + res := s.keeper.GetAllLiquidValidatorStates(s.ctx) + s.Require().Equal(params.WhitelistedValidators[0].ValidatorAddress, + res[0].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[0].TargetWeight, + res[0].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[0].Status) + s.Require().Equal(sdk.ZeroDec(), res[0].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[0].LiquidTokens) + + s.Require().Equal(params.WhitelistedValidators[1].ValidatorAddress, + res[1].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[1].TargetWeight, + res[1].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[1].Status) + s.Require().Equal(sdk.ZeroDec(), res[1].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[1].LiquidTokens) + + s.Require().Equal(params.WhitelistedValidators[2].ValidatorAddress, + res[2].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[2].TargetWeight, + res[2].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[2].Status) + s.Require().Equal(sdk.ZeroDec(), res[2].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[2].LiquidTokens) + + // liquid stake + newShares, stkXPRTMintAmt, err = s.keeper.LiquidStake( + s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], + sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt), + ) + s.Require().NoError(err) + s.Require().Equal(newShares, stakingAmt.ToLegacyDec()) + s.Require().Equal(stkXPRTMintAmt, stakingAmt) + + _, found := s.app.StakingKeeper.GetDelegation( + s.ctx, s.delAddrs[0], valOpers[0], + ) + s.Require().False(found) + _, found = s.app.StakingKeeper.GetDelegation( + s.ctx, s.delAddrs[0], valOpers[1], + ) + s.Require().False(found) + _, found = s.app.StakingKeeper.GetDelegation( + s.ctx, s.delAddrs[0], valOpers[2], + ) + s.Require().False(found) + + proxyAccDel1, found := s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[0], + ) + s.Require().True(found) + proxyAccDel2, found := s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[1], + ) + s.Require().True(found) + proxyAccDel3, found := s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[2], + ) + s.Require().True(found) + s.Require().Equal(proxyAccDel1.Shares, math.LegacyNewDec(16668)) + s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666)) + s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666)) + s.Require().Equal(stakingAmt.ToLegacyDec(), + proxyAccDel1.Shares.Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares)) + + liquidBondDenom := s.keeper.LiquidBondDenom(s.ctx) + balanceBeforeUBD := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], sdk.DefaultBondDenom, + ) + s.Require().Equal(balanceBeforeUBD.Amount, math.NewInt(999950000)) + ubdStkXPRT := sdk.NewCoin(liquidBondDenom, math.NewInt(10000)) + stkXPRTBalance := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], liquidBondDenom, + ) + stkXPRTTotalSupply := s.app.BankKeeper.GetSupply( + s.ctx, liquidBondDenom, + ) + s.Require().Equal(stkXPRTBalance, + sdk.NewCoin(liquidBondDenom, math.NewInt(50000))) + s.Require().Equal(stkXPRTBalance, stkXPRTTotalSupply) + + // liquid unstaking + ubdTime, unbondingAmt, ubds, unbondedAmt, err := s.keeper.LiquidUnstake( + s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], ubdStkXPRT, + ) + s.Require().NoError(err) + s.Require().EqualValues(unbondedAmt, sdk.ZeroInt()) + s.Require().Len(ubds, 3) + + // crumb excepted on unbonding + crumb := ubdStkXPRT.Amount.Sub(ubdStkXPRT.Amount.QuoRaw(3).MulRaw(3)) + s.Require().EqualValues(unbondingAmt, ubdStkXPRT.Amount.Sub(crumb)) + s.Require().Equal(ubds[0].DelegatorAddress, s.delAddrs[0].String()) + s.Require().Equal(ubdTime, testhelpers.ParseTime("2022-03-22T00:00:00Z")) + stkXPRTBalanceAfter := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], liquidBondDenom, + ) + s.Require().Equal(stkXPRTBalanceAfter, + sdk.NewCoin(liquidBondDenom, math.NewInt(40000))) + + balanceBeginUBD := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], sdk.DefaultBondDenom, + ) + s.Require().Equal(balanceBeginUBD.Amount, balanceBeforeUBD.Amount) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[0], + ) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[1], + ) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[2], + ) + s.Require().True(found) + s.Require().Equal(stakingAmt.Sub(unbondingAmt).ToLegacyDec(), + proxyAccDel1.GetShares().Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares)) + + // complete unbonding + s.ctx = s.ctx.WithBlockHeight(200).WithBlockTime(ubdTime.Add(1)) + updates := s.app.StakingKeeper.BlockValidatorUpdates(s.ctx) + s.Require().Empty(updates) + balanceCompleteUBD := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], sdk.DefaultBondDenom, + ) + s.Require().Equal(balanceCompleteUBD.Amount, + balanceBeforeUBD.Amount.Add(unbondingAmt)) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[0], + ) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[1], + ) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation( + s.ctx, types.LiquidStakeProxyAcc, valOpers[2], + ) + s.Require().True(found) + s.Require().Equal(math.LegacyNewDec(13335), proxyAccDel1.Shares) + s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel2.Shares) + s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel3.Shares) + + res = s.keeper.GetAllLiquidValidatorStates(s.ctx) + s.Require().Equal(params.WhitelistedValidators[0].ValidatorAddress, + res[0].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[0].TargetWeight, + res[0].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[0].Status) + s.Require().Equal(math.LegacyNewDec(13335), res[0].DelShares) + + s.Require().Equal(params.WhitelistedValidators[1].ValidatorAddress, + res[1].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[1].TargetWeight, + res[1].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[1].Status) + s.Require().Equal(math.LegacyNewDec(13333), res[1].DelShares) + + s.Require().Equal(params.WhitelistedValidators[2].ValidatorAddress, + res[2].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[2].TargetWeight, + res[2].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[2].Status) + s.Require().Equal(math.LegacyNewDec(13333), res[2].DelShares) + + // stack and withdraw liquid rewards and re-staking + s.advanceHeight(10, true) + rewards, _, _ := s.keeper.CheckDelegationStates( + s.ctx, types.LiquidStakeProxyAcc, + ) + s.Require().EqualValues(rewards, sdk.ZeroDec()) + + // stack rewards on net amount + s.advanceHeight(1, false) + rewards, _, _ = s.keeper.CheckDelegationStates( + s.ctx, types.LiquidStakeProxyAcc, + ) + s.Require().NotEqualValues(rewards, sdk.ZeroDec()) + + // failed requesting liquid unstaking stkXPRTTotalSupply when existing remaining rewards + stkXPRTTotalSupply = s.app.BankKeeper.GetSupply( + s.ctx, liquidBondDenom, + ) + stkxprtBalanceBefore := s.app.BankKeeper.GetBalance( + s.ctx, s.delAddrs[0], params.LiquidBondDenom, + ).Amount + s.Require().EqualValues(stkXPRTTotalSupply.Amount, stkxprtBalanceBefore) + s.Require().ErrorIs( + s.liquidUnstaking(s.delAddrs[0], stkxprtBalanceBefore, true), + sdkerrors.ErrInvalidRequest, + ) + + // all remaining rewards re-staked, request last unstaking, unbond all + s.advanceHeight(1, true) + rewards, _, _ = s.keeper.CheckDelegationStates( + s.ctx, types.LiquidStakeProxyAcc, + ) + s.Require().EqualValues(rewards, sdk.ZeroDec()) + s.Require().NoError( + s.liquidUnstaking(s.delAddrs[0], stkxprtBalanceBefore, true), + ) + + // still active liquid validator after unbond all + alv := s.keeper.GetActiveLiquidValidators( + s.ctx, params.WhitelistedValsMap(), + ) + s.Require().True(len(alv) != 0) + + // no btoken supply and netAmount after unbond all + nas := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(nas.StkxprtTotalSupply, sdk.ZeroInt()) + s.Require().Equal(nas.TotalRemainingRewards, sdk.ZeroDec()) + s.Require().Equal(nas.TotalDelShares, sdk.ZeroDec()) + s.Require().Equal(nas.TotalLiquidTokens, sdk.ZeroInt()) + s.Require().Equal(nas.ProxyAccBalance, sdk.ZeroInt()) + s.Require().Equal(nas.NetAmount, sdk.ZeroDec()) +} + +func (s *KeeperTestSuite) TestLiquidStakeFromVestingAccount() { + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + from := s.delAddrs[0] + vestingAmt := s.app.BankKeeper.GetAllBalances(s.ctx, from) + vestingStartTime := s.ctx.BlockTime().Add(1 * time.Hour) + vestingEndTime := s.ctx.BlockTime().Add(2 * time.Hour) + vestingMidTime := s.ctx.BlockTime().Add(90 * time.Minute) + + vestingAccAddr := "persistence10n3ncmlsaqfuwsmfll8kq6hvt4x7c8czahev75" + vestingAcc, err := sdk.AccAddressFromBech32(vestingAccAddr) + s.Require().NoError(err) + + // createContinuousVestingAccount + cVestingAcc := s.createContinuousVestingAccount(from, vestingAcc, vestingAmt, vestingStartTime, vestingEndTime) + spendableCoins := s.app.BankKeeper.SpendableCoins(s.ctx, cVestingAcc.GetAddress()) + s.Require().True(spendableCoins.IsZero()) + lockedCoins := s.app.BankKeeper.LockedCoins(s.ctx, cVestingAcc.GetAddress()) + s.Require().EqualValues(lockedCoins, vestingAmt) + + // failed liquid stake, no spendable coins on the vesting account ( not allowed locked coins ) + err = s.liquidStaking(vestingAcc, vestingAmt.AmountOf(sdk.DefaultBondDenom)) + s.Require().ErrorIs(err, sdkerrors.ErrInsufficientFunds) + + // release some vesting coins + s.ctx = s.ctx.WithBlockTime(vestingMidTime) + spendableCoins = s.app.BankKeeper.SpendableCoins(s.ctx, cVestingAcc.GetAddress()) + s.Require().True(spendableCoins.IsAllPositive()) + lockedCoins = s.app.BankKeeper.LockedCoins(s.ctx, cVestingAcc.GetAddress()) + s.Require().True(lockedCoins.IsAllPositive()) + + // success with released spendable coins + err = s.liquidStaking(vestingAcc, spendableCoins.AmountOf(sdk.DefaultBondDenom)) + s.Require().NoError(err) + nas := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(nas.TotalLiquidTokens, spendableCoins.AmountOf(sdk.DefaultBondDenom)) +} + +func (s *KeeperTestSuite) TestLiquidStakeEdgeCases() { + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + stakingAmt := math.NewInt(5000000) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // fail Invalid BondDenom case + _, _, err := s.keeper.LiquidStake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin("bad", stakingAmt)) + s.Require().ErrorIs(err, types.ErrInvalidBondDenom) + + // liquid stake, unstaking with huge amount + hugeAmt := math.NewInt(1_000_000_000_000_000_000) + s.fundAddr(s.delAddrs[0], sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, hugeAmt.MulRaw(2)))) + s.Require().NoError(s.liquidStaking(s.delAddrs[0], hugeAmt)) + s.Require().NoError(s.liquidStaking(s.delAddrs[0], hugeAmt)) + s.Require().NoError(s.liquidUnstaking(s.delAddrs[0], math.NewInt(10), true)) + s.Require().NoError(s.liquidUnstaking(s.delAddrs[0], hugeAmt, true)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.completeRedelegationUnbonding() + states := s.keeper.GetNetAmountState(s.ctx) + states.TotalLiquidTokens.Equal(hugeAmt) +} + +func (s *KeeperTestSuite) TestLiquidUnstakeEdgeCases() { + mintParams := s.app.MintKeeper.GetParams(s.ctx) + mintParams.InflationMax = math.LegacyNewDec(0) + mintParams.InflationMin = math.LegacyNewDec(0) + mintParams.InflationRateChange = math.LegacyNewDec(0) + s.app.MintKeeper.SetParams(s.ctx, mintParams) + + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + stakingAmt := math.NewInt(5000000) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + } + s.Require().NoError(s.keeper.SetParams(s.ctx, params)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // success liquid stake + s.Require().NoError(s.liquidStaking(s.delAddrs[0], stakingAmt)) + + // fail when liquid unstaking with too small amount + _, _, _, _, err := s.liquidUnstakingWithResult(s.delAddrs[0], sdk.NewCoin(params.LiquidBondDenom, math.NewInt(2))) + s.Require().ErrorIs(err, types.ErrTooSmallLiquidUnstakingAmount) + + // fail when liquid unstaking with zero amount + _, _, _, _, err = s.liquidUnstakingWithResult(s.delAddrs[0], sdk.NewCoin(params.LiquidBondDenom, math.NewInt(0))) + s.Require().ErrorIs(err, types.ErrTooSmallLiquidUnstakingAmount) + + // fail when invalid liquid bond denom + _, _, _, _, err = s.liquidUnstakingWithResult(s.delAddrs[0], sdk.NewCoin("stake", math.NewInt(10000))) + s.Require().ErrorIs(err, types.ErrInvalidLiquidBondDenom) + + // verify that there is no problem performing liquid unstaking as much as the MaxEntries + stakingParams := s.app.StakingKeeper.GetParams(s.ctx) + for i := uint32(0); i < stakingParams.MaxEntries; i++ { + s.Require().NoError(s.liquidUnstaking(s.delAddrs[0], math.NewInt(1000), false)) + } + + // on sdk 0.47+ shouldn't fail in an attempt to go beyond MaxEntries + err = s.liquidUnstaking(s.delAddrs[0], math.NewInt(1000), false) + s.Require().NoError(err) + + dels := s.app.StakingKeeper.GetUnbondingDelegations(s.ctx, s.delAddrs[0], 100) + for _, ubd := range dels { + s.Require().EqualValues(1, len(ubd.Entries)) + } + + // set empty whitelisted, active liquid validator + params.WhitelistedValidators = []types.WhitelistedValidator{} + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // error case where there is a quantity that are unbonding balance or remaining rewards that is not re-stake or withdrawn in netAmount. + _, _, _, _, err = s.liquidUnstakingWithResult(s.delAddrs[0], sdk.NewCoin(params.LiquidBondDenom, math.NewInt(1000))) + s.Require().ErrorIs(err, types.ErrInsufficientProxyAccBalance) + + // success after complete unbonding + s.completeRedelegationUnbonding() + ubdTime, unbondingAmt, ubds, unbondedAmt, err := s.liquidUnstakingWithResult(s.delAddrs[0], sdk.NewCoin(params.LiquidBondDenom, math.NewInt(1000))) + s.Require().NoError(err) + s.Require().EqualValues(unbondedAmt, math.NewInt(1000)) + s.Require().EqualValues(unbondingAmt, sdk.ZeroInt()) + s.Require().EqualValues(ubdTime, time.Time{}) + s.Require().Len(ubds, 0) +} diff --git a/x/liquidstake/keeper/msg_server.go b/x/liquidstake/keeper/msg_server.go index db745ace8..d0e39359f 100644 --- a/x/liquidstake/keeper/msg_server.go +++ b/x/liquidstake/keeper/msg_server.go @@ -9,7 +9,7 @@ import ( "context" "time" - errorsmod "cosmossdk.io/errors" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -153,7 +153,7 @@ func (k msgServer) UpdateParams(goCtx context.Context, msg *types.MsgUpdateParam ctx := sdk.UnwrapSDKContext(goCtx) if msg.Authority != k.authority { - return nil, errorsmod.Wrapf(sdkerrors.ErrorInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority) + return nil, errors.Wrapf(sdkerrors.ErrorInvalidSigner, "invalid authority; expected %s, got %s", k.authority, msg.Authority) } err := k.SetParams(ctx, msg.Params) diff --git a/x/liquidstake/keeper/rebalancing.go b/x/liquidstake/keeper/rebalancing.go index fd5d5d014..039e8c8d4 100644 --- a/x/liquidstake/keeper/rebalancing.go +++ b/x/liquidstake/keeper/rebalancing.go @@ -49,18 +49,24 @@ func (k Keeper) TryRedelegation(ctx sdk.Context, re types.Redelegation) (complet } // Rebalance argument liquidVals containing ValidatorStatusActive which is containing just added on whitelist(liquidToken 0) and ValidatorStatusInactive to delist -func (k Keeper) Rebalance(ctx sdk.Context, proxyAcc sdk.AccAddress, liquidVals types.LiquidValidators, whitelistedValsMap types.WhitelistedValsMap, rebalancingTrigger sdk.Dec) (redelegations []types.Redelegation) { +func (k Keeper) Rebalance( + ctx sdk.Context, + proxyAcc sdk.AccAddress, + liquidVals types.LiquidValidators, + whitelistedValsMap types.WhitelistedValsMap, + rebalancingTrigger math.LegacyDec, +) (redelegations []types.Redelegation) { logger := k.Logger(ctx) totalLiquidTokens, liquidTokenMap := liquidVals.TotalLiquidTokens(ctx, k.stakingKeeper, false) if !totalLiquidTokens.IsPositive() { - return []types.Redelegation{} + return redelegations } weightMap, totalWeight := k.GetWeightMap(ctx, liquidVals, whitelistedValsMap) // no active liquid validators if !totalWeight.IsPositive() { - return []types.Redelegation{} + return redelegations } // calculate rebalancing target map @@ -72,7 +78,7 @@ func (k Keeper) Rebalance(ctx sdk.Context, proxyAcc sdk.AccAddress, liquidVals t } crumb := totalLiquidTokens.Sub(totalTargetMap) if !totalTargetMap.IsPositive() { - return []types.Redelegation{} + return redelegations } // crumb to first non zero liquid validator for _, val := range liquidVals { @@ -83,7 +89,8 @@ func (k Keeper) Rebalance(ctx sdk.Context, proxyAcc sdk.AccAddress, liquidVals t } failCount := 0 - rebalancingThresholdAmt := rebalancingTrigger.Mul(sdk.NewDecFromInt(totalLiquidTokens)).TruncateInt() + rebalancingThresholdAmt := rebalancingTrigger.Mul(math.LegacyNewDecFromInt(totalLiquidTokens)).TruncateInt() + redelegations = make([]types.Redelegation, 0, liquidVals.Len()) for i := 0; i < liquidVals.Len(); i++ { // get min, max of liquid token gap @@ -104,16 +111,20 @@ func (k Keeper) Rebalance(ctx sdk.Context, proxyAcc sdk.AccAddress, liquidVals t Amount: amountNeeded, Last: last, } + _, err := k.TryRedelegation(ctx, redelegation) if err != nil { redelegation.Error = err failCount++ } + redelegations = append(redelegations, redelegation) } + if failCount > 0 { logger.Error("rebalancing failed due to redelegation hopping", "redelegations", redelegations) } + if len(redelegations) != 0 { ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( @@ -128,10 +139,11 @@ func (k Keeper) Rebalance(ctx sdk.Context, proxyAcc sdk.AccAddress, liquidVals t types.AttributeKeyRedelegationCount, strconv.Itoa(len(redelegations)), types.AttributeKeyRedelegationFailCount, strconv.Itoa(failCount)) } + return redelegations } -func (k Keeper) UpdateLiquidValidatorSet(ctx sdk.Context) []types.Redelegation { +func (k Keeper) UpdateLiquidValidatorSet(ctx sdk.Context) (redelegations []types.Redelegation) { logger := k.Logger(ctx) params := k.GetParams(ctx) liquidValidators := k.GetAllLiquidValidators(ctx) @@ -160,7 +172,13 @@ func (k Keeper) UpdateLiquidValidatorSet(ctx sdk.Context) []types.Redelegation { // rebalancing based updated liquid validators status with threshold, try by cachedCtx // tombstone status also handled on Rebalance - reds := k.Rebalance(ctx, types.LiquidStakeProxyAcc, liquidValidators, whitelistedValsMap, types.RebalancingTrigger) + redelegations = k.Rebalance( + ctx, + types.LiquidStakeProxyAcc, + liquidValidators, + whitelistedValsMap, + types.RebalancingTrigger, + ) // unbond all delShares to proxyAcc if delShares exist on inactive liquid validators for _, lv := range liquidValidators { @@ -202,7 +220,9 @@ func (k Keeper) UpdateLiquidValidatorSet(ctx sdk.Context) []types.Redelegation { } } - return reds + k.AutocompoundStakingRewards(ctx, whitelistedValsMap) + + return redelegations } // AutocompoundStakingRewards withdraws staking rewards and re-stakes when over threshold. @@ -211,26 +231,44 @@ func (k Keeper) AutocompoundStakingRewards(ctx sdk.Context, whitelistedValsMap t // checking over types.AutocompoundTrigger and execute GetRewards proxyAccBalance := k.GetProxyAccBalance(ctx, types.LiquidStakeProxyAcc) - rewardsThreshold := types.AutocompoundTrigger.Mul(sdk.NewDecFromInt(totalLiquidTokens)) + rewardsThreshold := types.AutocompoundTrigger.Mul(math.LegacyNewDecFromInt(totalLiquidTokens)) // skip If it doesn't exceed the rewards threshold - if !sdk.NewDecFromInt(proxyAccBalance.Amount).Add(totalRemainingRewards).GT(rewardsThreshold) { + if !math.LegacyNewDecFromInt(proxyAccBalance.Amount).Add(totalRemainingRewards).GT(rewardsThreshold) { return } // Withdraw rewards of LiquidStakeProxyAcc and re-staking k.WithdrawLiquidRewards(ctx, types.LiquidStakeProxyAcc) - // re-staking with proxyAccBalance, due to auto-withdraw on add staking by f1 + // prepare to re-staking with proxyAccBalance proxyAccBalance = k.GetProxyAccBalance(ctx, types.LiquidStakeProxyAcc) + // move autocompounding fee from the balance to fee account + params := k.GetParams(ctx) + autocompoundFee := sdk.NewCoin(proxyAccBalance.Denom, math.ZeroInt()) + + if !params.AutocompoundFeeRate.IsZero() { + autocompoundFee = sdk.NewCoin(proxyAccBalance.Denom, params.AutocompoundFeeRate.MulInt(proxyAccBalance.Amount).TruncateInt()) + feeAccountAddr := sdk.MustAccAddressFromBech32(params.FeeAccountAddress) + + err := k.bankKeeper.SendCoins(ctx, types.LiquidStakeProxyAcc, feeAccountAddr, sdk.NewCoins(autocompoundFee)) + if err != nil { + k.Logger(ctx).Error("re-staking failed upon fee collection", "error", err) + return + } + + // reset proxyAccBalance + proxyAccBalance = k.GetProxyAccBalance(ctx, types.LiquidStakeProxyAcc) + } + // skip when no active liquid validator activeVals := k.GetActiveLiquidValidators(ctx, whitelistedValsMap) if len(activeVals) == 0 { return } - // re-staking + // re-staking of the accumulated rewards cachedCtx, writeCache := ctx.CacheContext() _, err := k.LiquidDelegate(cachedCtx, types.LiquidStakeProxyAcc, activeVals, proxyAccBalance.Amount, whitelistedValsMap) if err != nil { @@ -245,9 +283,11 @@ func (k Keeper) AutocompoundStakingRewards(ctx sdk.Context, whitelistedValsMap t types.EventTypeAutocompound, sdk.NewAttribute(types.AttributeKeyDelegator, types.LiquidStakeProxyAcc.String()), sdk.NewAttribute(sdk.AttributeKeyAmount, proxyAccBalance.String()), + sdk.NewAttribute(types.AttributeKeyPstakeAutocompoundFee, autocompoundFee.String()), ), }) logger.Info(types.EventTypeAutocompound, types.AttributeKeyDelegator, types.LiquidStakeProxyAcc.String(), - sdk.AttributeKeyAmount, proxyAccBalance.String()) + sdk.AttributeKeyAmount, proxyAccBalance.String(), + types.AttributeKeyPstakeAutocompoundFee, autocompoundFee.String()) } diff --git a/x/liquidstake/keeper/rebalancing_test.go b/x/liquidstake/keeper/rebalancing_test.go new file mode 100644 index 000000000..244281c2a --- /dev/null +++ b/x/liquidstake/keeper/rebalancing_test.go @@ -0,0 +1,569 @@ +package keeper_test + +import ( + "time" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + testhelpers "github.com/persistenceOne/pstake-native/v2/app/helpers" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func (s *KeeperTestSuite) TestRebalancingCase1() { + _, valOpers, pks := s.CreateValidators([]int64{1000000, 1000000, 1000000, 1000000, 1000000}) + s.ctx = s.ctx.WithBlockHeight(100).WithBlockTime(testhelpers.ParseTime("2022-03-01T00:00:00Z")) + params := s.keeper.GetParams(s.ctx) + params.UnstakeFeeRate = sdk.ZeroDec() + params.MinLiquidStakeAmount = math.NewInt(10000) + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := math.NewInt(49998) + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + } + s.keeper.SetParams(s.ctx, params) + reds := s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + newShares, stkXPRTMintAmt, err := s.keeper.LiquidStake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt)) + s.Require().NoError(err) + s.Require().Equal(newShares, stakingAmt.ToLegacyDec()) + s.Require().Equal(stkXPRTMintAmt, stakingAmt) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + proxyAccDel1, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + + s.Require().EqualValues(proxyAccDel1.Shares.TruncateInt(), math.NewInt(16666)) + s.Require().EqualValues(proxyAccDel2.Shares.TruncateInt(), math.NewInt(16666)) + s.Require().EqualValues(proxyAccDel3.Shares.TruncateInt(), math.NewInt(16666)) + totalLiquidTokens, _ := s.keeper.GetAllLiquidValidators(s.ctx).TotalLiquidTokens(s.ctx, s.app.StakingKeeper, false) + s.Require().EqualValues(stakingAmt, totalLiquidTokens) + s.printRedelegationsLiquidTokens() + + // update whitelist validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(10)}, + } + s.keeper.SetParams(s.ctx, params) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 3) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + proxyAccDel4, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[3]) + s.Require().True(found) + + s.Require().EqualValues(proxyAccDel1.Shares.TruncateInt(), math.NewInt(12501)) + s.Require().EqualValues(proxyAccDel2.Shares.TruncateInt(), math.NewInt(12499)) + s.Require().EqualValues(proxyAccDel3.Shares.TruncateInt(), math.NewInt(12499)) + s.Require().EqualValues(proxyAccDel4.Shares.TruncateInt(), math.NewInt(12499)) + totalLiquidTokens, _ = s.keeper.GetAllLiquidValidators(s.ctx).TotalLiquidTokens(s.ctx, s.app.StakingKeeper, false) + s.Require().EqualValues(stakingAmt, totalLiquidTokens) + s.printRedelegationsLiquidTokens() + + //reds := s.app.StakingKeeper.GetRedelegations(s.ctx, types.LiquidStakeProxyAcc, 20) + s.Require().Len(reds, 3) + + testhelpers.PP("before complete") + testhelpers.PP(s.keeper.GetAllLiquidValidatorStates(s.ctx)) + testhelpers.PP(s.keeper.GetNetAmountState(s.ctx)) + + // advance block time and height for complete redelegations + s.completeRedelegationUnbonding() + + testhelpers.PP("after complete") + testhelpers.PP(s.keeper.GetAllLiquidValidatorStates(s.ctx)) + testhelpers.PP(s.keeper.GetNetAmountState(s.ctx)) + + // update whitelist validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(10)}, + } + s.keeper.SetParams(s.ctx, params) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 4) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + proxyAccDel4, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[3]) + s.Require().True(found) + proxyAccDel5, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[4]) + s.Require().True(found) + + s.printRedelegationsLiquidTokens() + s.Require().EqualValues(proxyAccDel1.Shares.TruncateInt(), math.NewInt(10002)) + s.Require().EqualValues(proxyAccDel2.Shares.TruncateInt(), math.NewInt(9999)) + s.Require().EqualValues(proxyAccDel3.Shares.TruncateInt(), math.NewInt(9999)) + s.Require().EqualValues(proxyAccDel4.Shares.TruncateInt(), math.NewInt(9999)) + s.Require().EqualValues(proxyAccDel5.Shares.TruncateInt(), math.NewInt(9999)) + totalLiquidTokens, _ = s.keeper.GetAllLiquidValidators(s.ctx).TotalLiquidTokens(s.ctx, s.app.StakingKeeper, false) + s.Require().EqualValues(stakingAmt, totalLiquidTokens) + + // advance block time and height for complete redelegations + s.completeRedelegationUnbonding() + + // remove whitelist validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(10)}, + } + + testhelpers.PP(s.keeper.GetAllLiquidValidatorStates(s.ctx)) + s.keeper.SetParams(s.ctx, params) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 4) + testhelpers.PP(s.keeper.GetAllLiquidValidatorStates(s.ctx)) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + proxyAccDel4, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[3]) + s.Require().True(found) + proxyAccDel5, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[4]) + s.Require().False(found) + + s.printRedelegationsLiquidTokens() + s.Require().EqualValues(proxyAccDel1.Shares.TruncateInt(), math.NewInt(12501)) + s.Require().EqualValues(proxyAccDel2.Shares.TruncateInt(), math.NewInt(12499)) + s.Require().EqualValues(proxyAccDel3.Shares.TruncateInt(), math.NewInt(12499)) + s.Require().EqualValues(proxyAccDel4.Shares.TruncateInt(), math.NewInt(12499)) + totalLiquidTokens, _ = s.keeper.GetAllLiquidValidators(s.ctx).TotalLiquidTokens(s.ctx, s.app.StakingKeeper, false) + s.Require().EqualValues(stakingAmt, totalLiquidTokens) + + // advance block time and height for complete redelegations + s.completeRedelegationUnbonding() + + // remove whitelist validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + } + + s.keeper.SetParams(s.ctx, params) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 3) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().False(found) + proxyAccDel4, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[3]) + s.Require().False(found) + proxyAccDel5, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[4]) + s.Require().False(found) + + s.printRedelegationsLiquidTokens() + s.Require().EqualValues(proxyAccDel1.Shares.TruncateInt(), math.NewInt(24999)) + s.Require().EqualValues(proxyAccDel2.Shares.TruncateInt(), math.NewInt(24999)) + totalLiquidTokens, _ = s.keeper.GetAllLiquidValidators(s.ctx).TotalLiquidTokens(s.ctx, s.app.StakingKeeper, false) + s.Require().EqualValues(stakingAmt, totalLiquidTokens) + + // advance block time and height for complete redelegations + s.completeRedelegationUnbonding() + + // double sign, tombstone, slash, jail + s.doubleSign(valOpers[1], sdk.ConsAddress(pks[1].Address())) + + // check inactive with zero weight after tombstoned + lvState, found := s.keeper.GetLiquidValidatorState(s.ctx, proxyAccDel2.GetValidatorAddr()) + s.Require().True(found) + s.Require().Equal(lvState.Status, types.ValidatorStatusInactive) + s.Require().Equal(lvState.Weight, sdk.ZeroInt()) + s.Require().NotEqualValues(lvState.DelShares, sdk.ZeroDec()) + s.Require().NotEqualValues(lvState.LiquidTokens, sdk.ZeroInt()) + + // rebalancing, remove tombstoned liquid validator + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 1) + + // all redelegated, no delShares + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().False(found) + + // liquid validator removed, invalid after tombstoned + lvState, found = s.keeper.GetLiquidValidatorState(s.ctx, valOpers[1]) + s.Require().False(found) + s.Require().Equal(lvState.OperatorAddress, valOpers[1].String()) + s.Require().Equal(lvState.Status, types.ValidatorStatusUnspecified) + s.Require().EqualValues(lvState.DelShares, sdk.ZeroDec()) + s.Require().EqualValues(lvState.LiquidTokens, sdk.ZeroInt()) + + // jail last liquid validator, undelegate all liquid tokens to proxy acc + s.doubleSign(valOpers[0], sdk.ConsAddress(pks[0].Address())) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + // no delegation of proxy acc + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().False(found) + val1, found := s.app.StakingKeeper.GetValidator(s.ctx, valOpers[0]) + s.Require().True(found) + s.Require().Equal(val1.Status, stakingtypes.Unbonding) + + // check unbonding delegation to proxy acc + ubd, found := s.app.StakingKeeper.GetUnbondingDelegation(s.ctx, types.LiquidStakeProxyAcc, val1.GetOperator()) + s.Require().True(found) + + // complete unbonding + s.completeRedelegationUnbonding() + + // check validator Unbonded + val1, found = s.app.StakingKeeper.GetValidator(s.ctx, valOpers[0]) + s.Require().True(found) + s.Require().Equal(val1.Status, stakingtypes.Unbonded) + + // no rewards, delShares, liquid tokens + nas := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(nas.TotalRemainingRewards, sdk.ZeroDec()) + s.Require().EqualValues(nas.TotalDelShares, sdk.ZeroDec()) + s.Require().EqualValues(nas.TotalLiquidTokens, sdk.ZeroInt()) + + // unbonded to balance, equal with netAmount + s.Require().EqualValues(ubd.Entries[0].Balance, nas.ProxyAccBalance) + s.Require().EqualValues(nas.NetAmount.TruncateInt(), nas.ProxyAccBalance) + + // mintRate over 1 due to slashing + s.Require().True(nas.MintRate.GT(sdk.OneDec())) + stkXPRTBalanceBefore := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], params.LiquidBondDenom).Amount + nativeTokenBalanceBefore := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], sdk.DefaultBondDenom).Amount + s.Require().EqualValues(nas.StkxprtTotalSupply, stkXPRTBalanceBefore) + + // withdraw directly unstaking when no totalLiquidTokens + s.Require().NoError(s.liquidUnstaking(s.delAddrs[0], stkXPRTBalanceBefore, false)) + stkXPRTBalanceAfter := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], params.LiquidBondDenom).Amount + nativeTokenBalanceAfter := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], sdk.DefaultBondDenom).Amount + s.Require().EqualValues(stkXPRTBalanceAfter, sdk.ZeroInt()) + s.Require().EqualValues(nativeTokenBalanceAfter.Sub(nativeTokenBalanceBefore), nas.NetAmount.TruncateInt()) + + // zero net amount states + s.RequireNetAmountStateZero() +} + +func (s *KeeperTestSuite) TestRebalancingConsecutiveCase() { + _, valOpers, _ := s.CreateValidators([]int64{ + 1000000, 1000000, 1000000, 1000000, 1000000, + 1000000, 1000000, 1000000, 1000000, 1000000, + 1000000, 1000000, 1000000, 1000000, 1000000}) + s.ctx = s.ctx.WithBlockHeight(100).WithBlockTime(testhelpers.ParseTime("2022-03-01T00:00:00Z")) + params := s.keeper.GetParams(s.ctx) + params.UnstakeFeeRate = sdk.ZeroDec() + params.MinLiquidStakeAmount = math.NewInt(10000) + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := math.NewInt(10000000000000) + s.fundAddr(s.delAddrs[0], sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt))) + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + reds := s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + newShares, stkXPRTMintAmt, err := s.keeper.LiquidStake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt)) + s.Require().NoError(err) + s.Require().Equal(newShares, stakingAmt.ToLegacyDec()) + s.Require().Equal(stkXPRTMintAmt, stakingAmt) + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[8].String(), TargetWeight: math.NewInt(5)}, + } + s.keeper.SetParams(s.ctx, params) + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24)) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 8) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[8].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[9].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24)) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 9) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + // complete redelegations + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24 * 20).Add(time.Hour)) + staking.EndBlocker(s.ctx, s.app.StakingKeeper) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + // assert rebalanced + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() + + // remove active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[8].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[9].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24)) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 9) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[8].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[9].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[10].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[11].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[12].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24)) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 11) + // fail rebalancing due to redelegation hopping + s.Require().Equal(s.redelegationsErrorCount(reds), 11) + s.printRedelegationsLiquidTokens() + + // complete redelegation and retry + s.completeRedelegationUnbonding() + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.printRedelegationsLiquidTokens() + s.Require().Len(reds, 11) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + + // modify weight + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[3].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[4].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[5].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[6].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[7].String(), TargetWeight: math.NewInt(5)}, + {ValidatorAddress: valOpers[8].String(), TargetWeight: math.NewInt(5)}, + {ValidatorAddress: valOpers[9].String(), TargetWeight: math.NewInt(5)}, + {ValidatorAddress: valOpers[10].String(), TargetWeight: math.NewInt(5)}, + {ValidatorAddress: valOpers[11].String(), TargetWeight: math.NewInt(5)}, + {ValidatorAddress: valOpers[12].String(), TargetWeight: math.NewInt(5)}, + } + s.keeper.SetParams(s.ctx, params) + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24)) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 6) + // fail rebalancing partially due to redelegation hopping + s.Require().Equal(s.redelegationsErrorCount(reds), 3) + s.printRedelegationsLiquidTokens() + + // additional liquid stake when not rebalanced + _, _, err = s.keeper.LiquidStake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000000000))) + s.Require().NoError(err) + s.printRedelegationsLiquidTokens() + + // complete some redelegations + s.ctx = s.ctx.WithBlockHeight(s.ctx.BlockHeight() + 100).WithBlockTime(s.ctx.BlockTime().Add(time.Hour * 24 * 20).Add(time.Hour)) + staking.EndBlocker(s.ctx, s.app.StakingKeeper) + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 9) + + // failed redelegations with small amount (less than rebalancing trigger) + s.Require().Equal(s.redelegationsErrorCount(reds), 6) + s.printRedelegationsLiquidTokens() + + // assert rebalanced + reds = s.keeper.UpdateLiquidValidatorSet(s.ctx) + s.Require().Len(reds, 0) + s.Require().Equal(s.redelegationsErrorCount(reds), 0) + s.printRedelegationsLiquidTokens() +} + +func (s *KeeperTestSuite) TestAutocompoundStakingRewards() { + _, valOpers, _ := s.CreateValidators([]int64{2000000, 2000000, 2000000}) + params := s.keeper.GetParams(s.ctx) + + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := math.NewInt(100000000) + s.Require().NoError(s.liquidStaking(s.delAddrs[0], stakingAmt)) + + // no rewards + totalRewards, totalDelShares, totalLiquidTokens := s.keeper.CheckDelegationStates(s.ctx, types.LiquidStakeProxyAcc) + s.EqualValues(totalRewards, sdk.ZeroDec()) + s.EqualValues(totalDelShares, stakingAmt.ToLegacyDec(), totalLiquidTokens) + + // allocate rewards + s.advanceHeight(100, false) + totalRewards, totalDelShares, totalLiquidTokens = s.keeper.CheckDelegationStates(s.ctx, types.LiquidStakeProxyAcc) + s.NotEqualValues(totalRewards, sdk.ZeroDec()) + s.NotEqualValues(totalLiquidTokens, sdk.ZeroDec()) + + // withdraw rewards and re-staking + whitelistedValsMap := types.GetWhitelistedValsMap(params.WhitelistedValidators) + s.keeper.AutocompoundStakingRewards(s.ctx, whitelistedValsMap) + totalRewardsAfter, totalDelSharesAfter, totalLiquidTokensAfter := s.keeper.CheckDelegationStates(s.ctx, types.LiquidStakeProxyAcc) + s.EqualValues(totalRewardsAfter, sdk.ZeroDec()) + + autocompoundFee := params.AutocompoundFeeRate.Mul(totalRewards) + s.EqualValues(totalDelSharesAfter, totalRewards.Sub(autocompoundFee).Add(totalDelShares).TruncateDec(), totalLiquidTokensAfter) + + stakingParams := s.app.StakingKeeper.GetParams(s.ctx) + feeAccountBalance := s.app.BankKeeper.GetBalance( + s.ctx, + sdk.MustAccAddressFromBech32(params.FeeAccountAddress), + stakingParams.BondDenom, + ) + s.EqualValues(autocompoundFee.TruncateInt(), feeAccountBalance.Amount) +} + +func (s *KeeperTestSuite) TestRemoveAllLiquidValidator() { + _, valOpers, _ := s.CreateValidators([]int64{2000000, 2000000, 2000000}) + params := s.keeper.GetParams(s.ctx) + + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(10)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(10)}, + } + s.Require().NoError(s.keeper.SetParams(s.ctx, params)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := math.NewInt(100000000) + s.Require().NoError(s.liquidStaking(s.delAddrs[0], stakingAmt)) + + // allocate rewards + s.advanceHeight(1, false) + nasBefore := s.keeper.GetNetAmountState(s.ctx) + s.Require().NotEqualValues(sdk.ZeroDec(), nasBefore.TotalRemainingRewards) + s.Require().NotEqualValues(sdk.ZeroDec(), nasBefore.TotalDelShares) + s.Require().NotEqualValues(sdk.ZeroDec(), nasBefore.NetAmount) + s.Require().NotEqualValues(sdk.ZeroInt(), nasBefore.TotalLiquidTokens) + s.Require().EqualValues(sdk.ZeroInt(), nasBefore.ProxyAccBalance) + + // remove all whitelist + params.WhitelistedValidators = []types.WhitelistedValidator{} + s.Require().NoError(s.keeper.SetParams(s.ctx, params)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + // no liquid validator + lvs := s.keeper.GetAllLiquidValidators(s.ctx) + s.Require().Len(lvs, 0) + + autocompoundFee := params.AutocompoundFeeRate.Mul(nasBefore.TotalRemainingRewards) + + nasAfter := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(sdk.ZeroDec(), nasAfter.TotalRemainingRewards) + s.Require().EqualValues(nasBefore.TotalRemainingRewards.Sub(autocompoundFee).TruncateInt(), nasAfter.ProxyAccBalance) + s.Require().EqualValues(sdk.ZeroDec(), nasAfter.TotalDelShares) + s.Require().EqualValues(sdk.ZeroInt(), nasAfter.TotalLiquidTokens) + s.Require().EqualValues(nasBefore.NetAmount.TruncateInt(), nasAfter.NetAmount.Add(autocompoundFee).TruncateInt()) + + s.completeRedelegationUnbonding() + nasAfter2 := s.keeper.GetNetAmountState(s.ctx) + s.Require().EqualValues(nasAfter.ProxyAccBalance.Add(nasBefore.TotalLiquidTokens), nasAfter2.ProxyAccBalance) + s.Require().EqualValues(nasBefore.NetAmount.TruncateInt(), nasAfter2.NetAmount.Add(autocompoundFee).TruncateInt()) + + stakingParams := s.app.StakingKeeper.GetParams(s.ctx) + feeAccountBalance := s.app.BankKeeper.GetBalance( + s.ctx, + sdk.MustAccAddressFromBech32(params.FeeAccountAddress), + stakingParams.BondDenom, + ) + + s.EqualValues(autocompoundFee.TruncateInt(), feeAccountBalance.Amount) +} diff --git a/x/liquidstake/module.go b/x/liquidstake/module.go index 8ff27a6fa..127fb309c 100644 --- a/x/liquidstake/module.go +++ b/x/liquidstake/module.go @@ -28,7 +28,6 @@ var ( // AppModuleBasic defines the basic application module used by the liquidstake module. type AppModuleBasic struct { - cdc codec.Codec } // Name returns the liquidstake module's name. diff --git a/x/liquidstake/types/errors.go b/x/liquidstake/types/errors.go index 1ec22b4f1..531a4ddc0 100644 --- a/x/liquidstake/types/errors.go +++ b/x/liquidstake/types/errors.go @@ -1,24 +1,24 @@ package types -import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +import "cosmossdk.io/errors" // Sentinel errors for the liquidstake module. var ( - ErrActiveLiquidValidatorsNotExists = sdkerrors.Register(ModuleName, 2, "active liquid validators not exists") - ErrInvalidDenom = sdkerrors.Register(ModuleName, 3, "invalid denom") - ErrInvalidBondDenom = sdkerrors.Register(ModuleName, 4, "invalid bond denom") - ErrInvalidLiquidBondDenom = sdkerrors.Register(ModuleName, 5, "invalid liquid bond denom") - ErrNotImplementedYet = sdkerrors.Register(ModuleName, 6, "not implemented yet") - ErrLessThanMinLiquidStakeAmount = sdkerrors.Register(ModuleName, 7, "staking amount should be over params.min_liquid_stake_amount") - ErrInvalidStkXPRTSupply = sdkerrors.Register(ModuleName, 8, "invalid liquid bond denom supply") - ErrInvalidActiveLiquidValidators = sdkerrors.Register(ModuleName, 9, "invalid active liquid validators") - ErrLiquidValidatorsNotExists = sdkerrors.Register(ModuleName, 10, "liquid validators not exists") - ErrInsufficientProxyAccBalance = sdkerrors.Register(ModuleName, 11, "insufficient liquid tokens or balance of proxy account, need to wait for new liquid validator to be added or unbonding of proxy account to be completed") - ErrTooSmallLiquidStakeAmount = sdkerrors.Register(ModuleName, 12, "liquid staking amount is too small, the result becomes zero") - ErrTooSmallLiquidUnstakingAmount = sdkerrors.Register(ModuleName, 13, "liquid unstaking amount is too small, the result becomes zero") - ErrNoLPContractAddress = sdkerrors.Register(ModuleName, 14, "CW address of an LP contract is not set") - ErrDisabledLSM = sdkerrors.Register(ModuleName, 15, "LSM delegation is disabled") - ErrLSMTokenizeFailed = sdkerrors.Register(ModuleName, 16, "LSM tokenization failed") - ErrLSMRedeemFailed = sdkerrors.Register(ModuleName, 17, "LSM redemption failed") - ErrLPContract = sdkerrors.Register(ModuleName, 18, "CW contract execution failed") + ErrActiveLiquidValidatorsNotExists = errors.Register(ModuleName, 2, "active liquid validators not exists") + ErrInvalidDenom = errors.Register(ModuleName, 3, "invalid denom") + ErrInvalidBondDenom = errors.Register(ModuleName, 4, "invalid bond denom") + ErrInvalidLiquidBondDenom = errors.Register(ModuleName, 5, "invalid liquid bond denom") + ErrNotImplementedYet = errors.Register(ModuleName, 6, "not implemented yet") + ErrLessThanMinLiquidStakeAmount = errors.Register(ModuleName, 7, "staking amount should be over params.min_liquid_stake_amount") + ErrInvalidStkXPRTSupply = errors.Register(ModuleName, 8, "invalid liquid bond denom supply") + ErrInvalidActiveLiquidValidators = errors.Register(ModuleName, 9, "invalid active liquid validators") + ErrLiquidValidatorsNotExists = errors.Register(ModuleName, 10, "liquid validators not exists") + ErrInsufficientProxyAccBalance = errors.Register(ModuleName, 11, "insufficient liquid tokens or balance of proxy account, need to wait for new liquid validator to be added or unbonding of proxy account to be completed") + ErrTooSmallLiquidStakeAmount = errors.Register(ModuleName, 12, "liquid stake amount is too small, the result becomes zero") + ErrTooSmallLiquidUnstakingAmount = errors.Register(ModuleName, 13, "liquid unstaking amount is too small, the result becomes zero") + ErrNoLPContractAddress = errors.Register(ModuleName, 14, "CW address of an LP contract is not set") + ErrDisabledLSM = errors.Register(ModuleName, 15, "LSM delegation is disabled") + ErrLSMTokenizeFailed = errors.Register(ModuleName, 16, "LSM tokenization failed") + ErrLSMRedeemFailed = errors.Register(ModuleName, 17, "LSM redemption failed") + ErrLPContract = errors.Register(ModuleName, 18, "CW contract execution failed") ) diff --git a/x/liquidstake/types/events.go b/x/liquidstake/types/events.go index ff1d4825e..0ca195fb4 100644 --- a/x/liquidstake/types/events.go +++ b/x/liquidstake/types/events.go @@ -23,6 +23,7 @@ const ( AttributeKeyRedelegationFailCount = "redelegation_fail_count" AttributeKeyLiquidAmount = "liquid_amount" AttributeKeyStakedAmount = "staked_amount" + AttributeKeyPstakeAutocompoundFee = "pstake_autocompound_fee" AttributeKeyAuthority = "authority" AttributeKeyUpdatedParams = "updated_params" diff --git a/x/liquidstake/types/expected_keepers.go b/x/liquidstake/types/expected_keepers.go index fec71ae38..4cd3d77a6 100644 --- a/x/liquidstake/types/expected_keepers.go +++ b/x/liquidstake/types/expected_keepers.go @@ -50,10 +50,10 @@ type StakingKeeper interface { Delegate( ctx sdk.Context, delAddr sdk.AccAddress, bondAmt math.Int, tokenSrc stakingtypes.BondStatus, validator stakingtypes.Validator, subtractAccount bool, - ) (newShares sdk.Dec, err error) + ) (newShares math.LegacyDec, err error) BondDenom(ctx sdk.Context) (res string) - Unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec) (amount math.Int, err error) + Unbond(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares math.LegacyDec) (amount math.Int, err error) UnbondingTime(ctx sdk.Context) (res time.Duration) SetUnbondingDelegationEntry( ctx sdk.Context, delegatorAddr sdk.AccAddress, validatorAddr sdk.ValAddress, @@ -63,10 +63,10 @@ type StakingKeeper interface { completionTime time.Time) ValidateUnbondAmount( ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt math.Int, - ) (shares sdk.Dec, err error) + ) (shares math.LegacyDec, err error) GetAllUnbondingDelegations(ctx sdk.Context, delegator sdk.AccAddress) []stakingtypes.UnbondingDelegation BeginRedelegation( - ctx sdk.Context, delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress, sharesAmount sdk.Dec, + ctx sdk.Context, delAddr sdk.AccAddress, valSrcAddr, valDstAddr sdk.ValAddress, sharesAmount math.LegacyDec, ) (completionTime time.Time, err error) GetAllRedelegations( ctx sdk.Context, delegator sdk.AccAddress, srcValAddress, dstValAddress sdk.ValAddress, @@ -97,5 +97,5 @@ type StakingHooks interface { BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) // Must be called when a delegation is created BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) // Must be called when a delegation's shares are modified AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) - BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) + BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction math.LegacyDec) } diff --git a/x/liquidstake/types/genesis.go b/x/liquidstake/types/genesis.go index ae0c72304..08ea0f393 100644 --- a/x/liquidstake/types/genesis.go +++ b/x/liquidstake/types/genesis.go @@ -1,6 +1,9 @@ package types -import sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +import ( + "cosmossdk.io/errors" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) // NewGenesisState returns new GenesisState instance. func NewGenesisState(params Params, liquidValidators []LiquidValidator) *GenesisState { @@ -25,7 +28,7 @@ func ValidateGenesis(data GenesisState) error { } for _, lv := range data.LiquidValidators { if err := lv.Validate(); err != nil { - return sdkerrors.Wrapf( + return errors.Wrapf( sdkerrors.ErrInvalidAddress, "invalid liquid validator %s: %v", lv, err) } diff --git a/x/liquidstake/types/genesis_test.go b/x/liquidstake/types/genesis_test.go new file mode 100644 index 000000000..3b1892892 --- /dev/null +++ b/x/liquidstake/types/genesis_test.go @@ -0,0 +1,64 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + "github.com/stretchr/testify/require" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func TestGenesisState_Validate(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(genState *types.GenesisState) + expectedErr string + }{ + { + "default is valid", + func(genState *types.GenesisState) {}, + "", + }, + { + "invalid liquid validator address", + func(genState *types.GenesisState) { + genState.LiquidValidators = []types.LiquidValidator{ + { + OperatorAddress: "invalidAddr", + }, + } + }, + "invalid liquid validator {invalidAddr}: decoding bech32 failed: string not all lowercase or all uppercase: invalid address", + }, + { + "empty liquid validator address", + func(genState *types.GenesisState) { + genState.LiquidValidators = []types.LiquidValidator{ + { + OperatorAddress: "", + }, + } + }, + "invalid liquid validator {}: empty address string is not allowed: invalid address", + }, + { + "invalid params(UnstakeFeeRate)", + func(genState *types.GenesisState) { + genState.Params.UnstakeFeeRate = math.LegacyDec{} + }, + "unstake fee rate must not be nil", + }, + } { + t.Run(tc.name, func(t *testing.T) { + genState := types.DefaultGenesisState() + tc.malleate(genState) + err := types.ValidateGenesis(*genState) + if tc.expectedErr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expectedErr) + } + }) + } +} diff --git a/x/liquidstake/types/liquidstake.go b/x/liquidstake/types/liquidstake.go index 1ffabfffb..7b27c6704 100644 --- a/x/liquidstake/types/liquidstake.go +++ b/x/liquidstake/types/liquidstake.go @@ -12,9 +12,9 @@ type WhitelistedValsMap map[string]WhitelistedValidator func (whitelistedValsMap WhitelistedValsMap) IsListed(operatorAddr string) bool { if _, ok := whitelistedValsMap[operatorAddr]; ok { return true - } else { - return false } + + return false } func GetWhitelistedValsMap(whitelistedValidators []WhitelistedValidator) WhitelistedValsMap { @@ -45,7 +45,7 @@ func (v LiquidValidator) GetOperator() sdk.ValAddress { return addr } -func (v LiquidValidator) GetDelShares(ctx sdk.Context, sk StakingKeeper) sdk.Dec { +func (v LiquidValidator) GetDelShares(ctx sdk.Context, sk StakingKeeper) math.LegacyDec { del, found := sk.GetDelegation(ctx, LiquidStakeProxyAcc, v.GetOperator()) if !found { return sdk.ZeroDec() @@ -68,17 +68,17 @@ func (v LiquidValidator) GetLiquidTokens(ctx sdk.Context, sk StakingKeeper, only func (v LiquidValidator) GetWeight(whitelistedValsMap WhitelistedValsMap, active bool) math.Int { if wv, ok := whitelistedValsMap[v.OperatorAddress]; ok && active { return wv.TargetWeight - } else { - return sdk.ZeroInt() } + + return sdk.ZeroInt() } func (v LiquidValidator) GetStatus(activeCondition bool) ValidatorStatus { if activeCondition { return ValidatorStatusActive - } else { - return ValidatorStatusInactive } + + return ValidatorStatusInactive } // ActiveCondition checks the liquid validator could be active by below cases @@ -165,29 +165,29 @@ func (avs ActiveLiquidValidators) TotalWeight(whitelistedValsMap WhitelistedVals } // NativeTokenToStkXPRT returns StkxprtTotalSupply * nativeTokenAmount / netAmount -func NativeTokenToStkXPRT(nativeTokenAmount, StkxprtTotalSupplyAmount math.Int, netAmount sdk.Dec) (stkXPRTAmount math.Int) { - return sdk.NewDecFromInt(StkxprtTotalSupplyAmount).MulTruncate(sdk.NewDecFromInt(nativeTokenAmount)).QuoTruncate(netAmount.TruncateDec()).TruncateInt() +func NativeTokenToStkXPRT(nativeTokenAmount, stkXPRTTotalSupplyAmount math.Int, netAmount math.LegacyDec) (stkXPRTAmount math.Int) { + return math.LegacyNewDecFromInt(stkXPRTTotalSupplyAmount).MulTruncate(math.LegacyNewDecFromInt(nativeTokenAmount)).QuoTruncate(netAmount.TruncateDec()).TruncateInt() } // StkXPRTToNativeToken returns stkXPRTAmount * netAmount / StkxprtTotalSupply with truncations -func StkXPRTToNativeToken(stkXPRTAmount, StkxprtTotalSupplyAmount math.Int, netAmount sdk.Dec) (nativeTokenAmount sdk.Dec) { - return sdk.NewDecFromInt(stkXPRTAmount).MulTruncate(netAmount).Quo(sdk.NewDecFromInt(StkxprtTotalSupplyAmount)).TruncateDec() +func StkXPRTToNativeToken(stkXPRTAmount, stkXPRTTotalSupplyAmount math.Int, netAmount math.LegacyDec) (nativeTokenAmount math.LegacyDec) { + return math.LegacyNewDecFromInt(stkXPRTAmount).MulTruncate(netAmount).Quo(math.LegacyNewDecFromInt(stkXPRTTotalSupplyAmount)).TruncateDec() } // DeductFeeRate returns Input * (1-FeeRate) with truncations -func DeductFeeRate(input sdk.Dec, feeRate sdk.Dec) (feeDeductedOutput sdk.Dec) { +func DeductFeeRate(input math.LegacyDec, feeRate math.LegacyDec) (feeDeductedOutput math.LegacyDec) { return input.MulTruncate(sdk.OneDec().Sub(feeRate)).TruncateDec() } -func (nas NetAmountState) CalcNetAmount() sdk.Dec { - return sdk.NewDecFromInt(nas.ProxyAccBalance.Add(nas.TotalLiquidTokens).Add(nas.TotalUnbondingBalance)).Add(nas.TotalRemainingRewards) +func (nas NetAmountState) CalcNetAmount() math.LegacyDec { + return math.LegacyNewDecFromInt(nas.ProxyAccBalance.Add(nas.TotalLiquidTokens).Add(nas.TotalUnbondingBalance)).Add(nas.TotalRemainingRewards) } -func (nas NetAmountState) CalcMintRate() sdk.Dec { +func (nas NetAmountState) CalcMintRate() math.LegacyDec { if nas.NetAmount.IsNil() || !nas.NetAmount.IsPositive() { return sdk.ZeroDec() } - return sdk.NewDecFromInt(nas.StkxprtTotalSupply).QuoTruncate(nas.NetAmount) + return math.LegacyNewDecFromInt(nas.StkxprtTotalSupply).QuoTruncate(nas.NetAmount) } type LiquidValidatorStates []LiquidValidatorState diff --git a/x/liquidstake/types/liquidstake.pb.go b/x/liquidstake/types/liquidstake.pb.go index fe2f88c9a..5c21e19d6 100644 --- a/x/liquidstake/types/liquidstake.pb.go +++ b/x/liquidstake/types/liquidstake.pb.go @@ -75,9 +75,16 @@ type Params struct { // to the active liquid validators on liquid staking to minimize decimal loss // and consider gas efficiency. MinLiquidStakeAmount github_com_cosmos_cosmos_sdk_types.Int `protobuf:"bytes,5,opt,name=min_liquid_stake_amount,json=minLiquidStakeAmount,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Int" json:"min_liquid_stake_amount"` - // cw_locked_pool_address defines the bech32-encoded address of + // CwLockedPoolAddress defines the bech32-encoded address of // a CW smart-contract representing a time locked LP (e.g. Superfluid LP). CwLockedPoolAddress string `protobuf:"bytes,6,opt,name=cw_locked_pool_address,json=cwLockedPoolAddress,proto3" json:"cw_locked_pool_address,omitempty"` + // FeeAccountAddress defines the bech32-encoded address of + // a an account responsible for accumulating protocol fees. + FeeAccountAddress string `protobuf:"bytes,7,opt,name=fee_account_address,json=feeAccountAddress,proto3" json:"fee_account_address,omitempty"` + // AutocompoundFeeRate specifies the fee rate for auto redelegating the stake + // rewards. The fee is taken in favour of the fee account (see + // FeeAccountAddress). + AutocompoundFeeRate github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,8,opt,name=autocompound_fee_rate,json=autocompoundFeeRate,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"autocompound_fee_rate"` } func (m *Params) Reset() { *m = Params{} } @@ -324,65 +331,68 @@ func init() { } var fileDescriptor_8f87e6d47a5a3bba = []byte{ - // 917 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x96, 0x41, 0x4f, 0x1b, 0x47, - 0x14, 0xc7, 0xbd, 0x40, 0x5c, 0x98, 0x50, 0x6c, 0x26, 0x26, 0x18, 0xb7, 0x32, 0x2e, 0x87, 0x0a, - 0xa5, 0xc5, 0x6e, 0xa8, 0xd4, 0x03, 0x87, 0xaa, 0x36, 0x06, 0xc9, 0x2a, 0x21, 0x68, 0xd7, 0x90, - 0x28, 0x87, 0x4e, 0xc6, 0xbb, 0x0f, 0xb3, 0xf2, 0xee, 0xcc, 0x76, 0x67, 0x8c, 0xe1, 0x1b, 0x44, - 0x9c, 0x7a, 0xaa, 0x7a, 0x41, 0x8a, 0xd4, 0xaf, 0xd0, 0x43, 0x3f, 0x42, 0x2e, 0x95, 0xa2, 0x9e, - 0xaa, 0x1e, 0xa2, 0x08, 0x2e, 0xfd, 0x14, 0x55, 0x35, 0x33, 0x6b, 0x63, 0x9c, 0xb4, 0x15, 0x4e, - 0x4e, 0x5e, 0xcf, 0xdb, 0xff, 0xef, 0xff, 0xe6, 0xbd, 0x7d, 0xb3, 0x8b, 0x3e, 0x8f, 0x84, 0xa4, - 0x1d, 0xa8, 0x04, 0xfe, 0xf7, 0x5d, 0xdf, 0x33, 0xd7, 0xc7, 0xf7, 0x5b, 0x20, 0xe9, 0xfd, 0xe1, - 0xb5, 0x72, 0x14, 0x73, 0xc9, 0x71, 0xc1, 0xdc, 0x5d, 0x1e, 0x8e, 0x24, 0x77, 0x17, 0x72, 0x6d, - 0xde, 0xe6, 0xfa, 0xb6, 0x8a, 0xba, 0x32, 0x8a, 0xc2, 0x92, 0xcb, 0x45, 0xc8, 0x05, 0x31, 0x01, - 0xf3, 0xc7, 0x84, 0x56, 0x5e, 0x4f, 0xa2, 0xf4, 0x1e, 0x8d, 0x69, 0x28, 0xf0, 0x3d, 0x34, 0x6f, - 0x90, 0xa4, 0xc5, 0x99, 0x47, 0x3c, 0x60, 0x3c, 0xcc, 0x5b, 0x25, 0x6b, 0x75, 0xc6, 0xce, 0x98, - 0x40, 0x8d, 0x33, 0xaf, 0xae, 0x96, 0x71, 0x88, 0xee, 0xf6, 0x8e, 0x7c, 0x09, 0x81, 0x2f, 0x24, - 0x78, 0xe4, 0x98, 0x06, 0xbe, 0x47, 0x25, 0x8f, 0x45, 0x7e, 0xa2, 0x34, 0xb9, 0x7a, 0x7b, 0xfd, - 0x8b, 0xf2, 0xbf, 0x27, 0x59, 0x7e, 0x74, 0xa5, 0x3c, 0xe8, 0x0b, 0x6b, 0x53, 0x2f, 0x5e, 0x2d, - 0xa7, 0xec, 0x85, 0xde, 0x5b, 0x62, 0x02, 0x3f, 0x46, 0xd9, 0x2e, 0xd3, 0x10, 0x72, 0x08, 0x40, - 0x62, 0x2a, 0x21, 0x3f, 0xa9, 0x32, 0xab, 0x95, 0x95, 0xec, 0xcf, 0x57, 0xcb, 0x9f, 0xb6, 0x7d, - 0x79, 0xd4, 0x6d, 0x95, 0x5d, 0x1e, 0x26, 0x1b, 0x4c, 0x7e, 0xd6, 0x84, 0xd7, 0xa9, 0xc8, 0xd3, - 0x08, 0x44, 0xb9, 0x0e, 0xae, 0x3d, 0x97, 0x70, 0xb6, 0x01, 0x6c, 0x2a, 0x01, 0x7f, 0x82, 0x66, - 0x03, 0x11, 0x12, 0xcf, 0x17, 0xb4, 0x15, 0x80, 0x97, 0x9f, 0x2a, 0x59, 0xab, 0xd3, 0xf6, 0xed, - 0x40, 0x84, 0xf5, 0x64, 0x09, 0x03, 0x5a, 0x0c, 0x7d, 0x46, 0x92, 0xda, 0x98, 0x2c, 0x68, 0xc8, - 0xbb, 0x4c, 0xe6, 0x6f, 0xdd, 0x38, 0x87, 0x06, 0x93, 0x76, 0x2e, 0xf4, 0xd9, 0x8e, 0xa6, 0x39, - 0x0a, 0x56, 0xd5, 0x2c, 0xfc, 0x00, 0xdd, 0x75, 0x7b, 0x24, 0xe0, 0x6e, 0x07, 0x3c, 0x12, 0x71, - 0x1e, 0x10, 0xea, 0x79, 0x31, 0x08, 0x91, 0x4f, 0x6b, 0x97, 0xfc, 0xef, 0xbf, 0xac, 0xe5, 0x92, - 0xde, 0x55, 0x4d, 0xc4, 0x91, 0xb1, 0xcf, 0xda, 0xf6, 0x1d, 0xb7, 0xb7, 0xa3, 0x65, 0x7b, 0x9c, - 0x07, 0x49, 0x68, 0x63, 0xfa, 0xd9, 0xf3, 0xe5, 0xd4, 0x4f, 0xcf, 0x97, 0x53, 0x2b, 0xbf, 0x5a, - 0x28, 0xf7, 0xb6, 0x92, 0xe3, 0x2d, 0x34, 0x3f, 0x68, 0xdc, 0xc0, 0xcc, 0xfa, 0x1f, 0xb3, 0xec, - 0x40, 0x92, 0xac, 0x63, 0x07, 0x7d, 0x28, 0x69, 0xdc, 0x06, 0x49, 0x7a, 0xe0, 0xb7, 0x8f, 0x64, - 0x7e, 0x62, 0xac, 0xaa, 0xcc, 0x1a, 0xc8, 0x23, 0xcd, 0xd8, 0x98, 0x52, 0xe9, 0xaf, 0x3c, 0x45, - 0x19, 0x53, 0xa8, 0xab, 0xa4, 0x37, 0x51, 0x96, 0x47, 0x10, 0xdf, 0x28, 0xe7, 0x4c, 0x5f, 0x71, - 0xad, 0x38, 0x7f, 0x29, 0x87, 0x1f, 0x27, 0x51, 0x6e, 0xc4, 0xc2, 0x91, 0xea, 0xc1, 0x78, 0x1f, - 0x3e, 0x78, 0x1b, 0xa5, 0xdf, 0xa9, 0x26, 0x89, 0x1a, 0x6f, 0xa2, 0xb4, 0x90, 0x54, 0x76, 0x85, - 0x7e, 0xea, 0xe7, 0xd6, 0x3f, 0xfb, 0xaf, 0xf1, 0xba, 0xb6, 0x91, 0xae, 0xb0, 0x13, 0x29, 0x7e, - 0x80, 0x90, 0x07, 0x01, 0x11, 0x47, 0x34, 0x06, 0xa1, 0x1f, 0xf4, 0x9b, 0x8f, 0xcf, 0x8c, 0x07, - 0x81, 0xa3, 0x01, 0xaa, 0xed, 0xc9, 0x48, 0x48, 0xde, 0x01, 0x26, 0xc6, 0x1c, 0x86, 0x59, 0x03, - 0x69, 0x6a, 0xc6, 0x50, 0x63, 0xfe, 0xbe, 0x85, 0xe6, 0x76, 0x41, 0x9a, 0xe1, 0x30, 0x2d, 0xf9, - 0x16, 0xcd, 0x84, 0x3e, 0x93, 0x66, 0xfc, 0xad, 0xb1, 0xf2, 0x9f, 0x56, 0x00, 0x3d, 0xf8, 0x4f, - 0x51, 0x4e, 0xc8, 0xce, 0x49, 0x14, 0x4b, 0x22, 0xb9, 0xa4, 0x01, 0x11, 0xdd, 0x28, 0x0a, 0x4e, - 0xc7, 0x6c, 0x14, 0x4e, 0x58, 0x4d, 0x85, 0x72, 0x34, 0x49, 0xd5, 0x9b, 0x81, 0xec, 0x1f, 0x15, - 0xe3, 0x1d, 0x57, 0x33, 0xac, 0x5f, 0x02, 0x75, 0x06, 0x9a, 0x44, 0xdf, 0xb9, 0x89, 0x73, 0x9a, - 0x53, 0x1f, 0x74, 0xf2, 0x3b, 0x74, 0xc7, 0x90, 0xdf, 0x47, 0x3f, 0xe7, 0x35, 0x6a, 0x67, 0xa8, - 0xa9, 0xf8, 0x10, 0x2d, 0x1a, 0x7e, 0x0c, 0x21, 0xf5, 0x99, 0xcf, 0xda, 0x24, 0x86, 0x1e, 0x8d, - 0xbd, 0xfe, 0xd1, 0x76, 0xd3, 0x0d, 0x2c, 0x68, 0x9c, 0xdd, 0xa7, 0xd9, 0x06, 0x76, 0xe5, 0xd3, - 0x65, 0xea, 0x0d, 0xa6, 0x7c, 0x5a, 0x34, 0xa0, 0xcc, 0x85, 0xfc, 0x07, 0x63, 0xed, 0xc5, 0xf8, - 0xec, 0xf7, 0x69, 0x35, 0x03, 0xc3, 0x4f, 0xd0, 0x7c, 0x14, 0xf3, 0x93, 0x53, 0x42, 0x5d, 0x77, - 0xe0, 0x30, 0x3d, 0x96, 0x43, 0x46, 0x83, 0xaa, 0xae, 0x9b, 0xb0, 0xf5, 0x00, 0x58, 0x6a, 0x00, - 0xee, 0xfd, 0x66, 0xa1, 0xcc, 0xc8, 0x28, 0xe3, 0x6f, 0xd0, 0xc7, 0x07, 0xd5, 0x9d, 0x46, 0xbd, - 0xda, 0x7c, 0x68, 0x13, 0xa7, 0x59, 0x6d, 0xee, 0x3b, 0x64, 0x7f, 0xd7, 0xd9, 0xdb, 0xda, 0x6c, - 0x6c, 0x37, 0xb6, 0xea, 0xd9, 0x54, 0xa1, 0x78, 0x76, 0x5e, 0x2a, 0x8c, 0xc8, 0xf6, 0x99, 0x88, - 0xc0, 0xf5, 0x0f, 0x7d, 0xf0, 0xf0, 0x57, 0x68, 0xf1, 0x0d, 0x42, 0x75, 0xb3, 0xd9, 0x38, 0xd8, - 0xca, 0x5a, 0x85, 0xa5, 0xb3, 0xf3, 0xd2, 0xc2, 0x88, 0xb8, 0xea, 0x4a, 0xff, 0x18, 0xf0, 0x06, - 0x5a, 0x7a, 0x43, 0xd7, 0xd8, 0x4d, 0x94, 0x13, 0x85, 0x8f, 0xce, 0xce, 0x4b, 0x8b, 0x23, 0xca, - 0x06, 0xa3, 0x5a, 0x5b, 0x98, 0x7a, 0xf6, 0x73, 0x31, 0x55, 0x7b, 0xfc, 0xe2, 0xa2, 0x68, 0xbd, - 0xbc, 0x28, 0x5a, 0xaf, 0x2f, 0x8a, 0xd6, 0x0f, 0x97, 0xc5, 0xd4, 0xcb, 0xcb, 0x62, 0xea, 0x8f, - 0xcb, 0x62, 0xea, 0xc9, 0xd7, 0x43, 0xc5, 0x8a, 0x20, 0x16, 0xea, 0x35, 0xc5, 0x5c, 0x78, 0xc8, - 0xa0, 0x62, 0x8e, 0xb9, 0x35, 0x46, 0x15, 0xa8, 0x72, 0xbc, 0x5e, 0x39, 0xb9, 0xf6, 0x91, 0xa4, - 0x0b, 0xd9, 0x4a, 0xeb, 0x4f, 0x99, 0x2f, 0xff, 0x09, 0x00, 0x00, 0xff, 0xff, 0xd7, 0x62, 0xa0, - 0xc1, 0x47, 0x09, 0x00, 0x00, + // 965 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x96, 0xcf, 0x6f, 0x1b, 0x45, + 0x14, 0xc7, 0xbd, 0xf9, 0x61, 0x9c, 0x69, 0x88, 0xed, 0x8d, 0xd3, 0x38, 0x06, 0x39, 0x26, 0x07, + 0x14, 0x15, 0x62, 0xd3, 0x20, 0x71, 0xc8, 0x01, 0x61, 0xc7, 0x89, 0xb0, 0x48, 0xd3, 0x68, 0xed, + 0xa4, 0x55, 0x0f, 0x4c, 0xc7, 0xbb, 0x2f, 0xce, 0xca, 0xbb, 0x33, 0xcb, 0xce, 0x6c, 0x9c, 0xfc, + 0x07, 0x55, 0x0e, 0x88, 0x13, 0xe2, 0x12, 0xa9, 0x12, 0xff, 0x02, 0x07, 0xfe, 0x84, 0x5e, 0x90, + 0x2a, 0x4e, 0x88, 0x43, 0x85, 0x92, 0x0b, 0x7f, 0x05, 0x42, 0x33, 0xb3, 0x76, 0x5c, 0xb7, 0x50, + 0xd9, 0xed, 0x29, 0xce, 0xcc, 0x7e, 0x3f, 0xef, 0xcd, 0xfb, 0xce, 0x7b, 0xbb, 0xe8, 0xd3, 0x80, + 0x0b, 0xd2, 0x85, 0x8a, 0xe7, 0x7e, 0x17, 0xb9, 0x8e, 0xfe, 0x7d, 0x7a, 0xb7, 0x0d, 0x82, 0xdc, + 0x1d, 0x5e, 0x2b, 0x07, 0x21, 0x13, 0xcc, 0x2c, 0xe8, 0xa7, 0xcb, 0xc3, 0x3b, 0xf1, 0xd3, 0x85, + 0x5c, 0x87, 0x75, 0x98, 0x7a, 0xac, 0x22, 0x7f, 0x69, 0x45, 0x61, 0xc5, 0x66, 0xdc, 0x67, 0x1c, + 0xeb, 0x0d, 0xfd, 0x8f, 0xde, 0x5a, 0xfb, 0x7e, 0x16, 0x25, 0x0f, 0x48, 0x48, 0x7c, 0x6e, 0xde, + 0x41, 0x59, 0x8d, 0xc4, 0x6d, 0x46, 0x1d, 0xec, 0x00, 0x65, 0x7e, 0xde, 0x28, 0x19, 0xeb, 0x73, + 0x56, 0x5a, 0x6f, 0xd4, 0x18, 0x75, 0xea, 0x72, 0xd9, 0xf4, 0xd1, 0xed, 0xde, 0x89, 0x2b, 0xc0, + 0x73, 0xb9, 0x00, 0x07, 0x9f, 0x12, 0xcf, 0x75, 0x88, 0x60, 0x21, 0xcf, 0x4f, 0x95, 0xa6, 0xd7, + 0x6f, 0x6d, 0x7e, 0x56, 0xfe, 0xef, 0x24, 0xcb, 0x0f, 0x6e, 0x94, 0x47, 0x7d, 0x61, 0x6d, 0xe6, + 0xd9, 0x8b, 0xd5, 0x84, 0xb5, 0xd4, 0x7b, 0xcd, 0x1e, 0x37, 0x1f, 0xa2, 0x4c, 0x44, 0x15, 0x04, + 0x1f, 0x03, 0xe0, 0x90, 0x08, 0xc8, 0x4f, 0xcb, 0xcc, 0x6a, 0x65, 0x29, 0xfb, 0xf3, 0xc5, 0xea, + 0xc7, 0x1d, 0x57, 0x9c, 0x44, 0xed, 0xb2, 0xcd, 0xfc, 0xf8, 0x80, 0xf1, 0x9f, 0x0d, 0xee, 0x74, + 0x2b, 0xe2, 0x3c, 0x00, 0x5e, 0xae, 0x83, 0x6d, 0x2d, 0xc4, 0x9c, 0x5d, 0x00, 0x8b, 0x08, 0x30, + 0x3f, 0x42, 0xf3, 0x1e, 0xf7, 0xb1, 0xe3, 0x72, 0xd2, 0xf6, 0xc0, 0xc9, 0xcf, 0x94, 0x8c, 0xf5, + 0x94, 0x75, 0xcb, 0xe3, 0x7e, 0x3d, 0x5e, 0x32, 0x01, 0x2d, 0xfb, 0x2e, 0xc5, 0x71, 0x6d, 0x74, + 0x16, 0xc4, 0x67, 0x11, 0x15, 0xf9, 0xd9, 0xb1, 0x73, 0x68, 0x50, 0x61, 0xe5, 0x7c, 0x97, 0xee, + 0x29, 0x5a, 0x53, 0xc2, 0xaa, 0x8a, 0x65, 0xde, 0x43, 0xb7, 0xed, 0x1e, 0xf6, 0x98, 0xdd, 0x05, + 0x07, 0x07, 0x8c, 0x79, 0x98, 0x38, 0x4e, 0x08, 0x9c, 0xe7, 0x93, 0x2a, 0x4a, 0xfe, 0xf7, 0x5f, + 0x36, 0x72, 0xb1, 0x77, 0x55, 0xbd, 0xd3, 0x14, 0xa1, 0x4b, 0x3b, 0xd6, 0xa2, 0xdd, 0xdb, 0x53, + 0xb2, 0x03, 0xc6, 0xbc, 0x78, 0xcb, 0xfc, 0x1a, 0x2d, 0xca, 0x52, 0x11, 0xdb, 0x96, 0xf4, 0x01, + 0xeb, 0xbd, 0x37, 0xb0, 0xb2, 0xc7, 0x00, 0x55, 0xad, 0xe9, 0x93, 0xda, 0x68, 0x89, 0x44, 0x82, + 0xd9, 0xcc, 0x0f, 0x58, 0x44, 0x9d, 0x1b, 0x07, 0x52, 0x13, 0x39, 0xb0, 0x38, 0x0c, 0x8b, 0x6d, + 0xd8, 0x4a, 0x3d, 0x79, 0xba, 0x9a, 0xf8, 0xe9, 0xe9, 0x6a, 0x62, 0xed, 0x57, 0x03, 0xe5, 0x5e, + 0x77, 0x41, 0xcc, 0x1d, 0x94, 0x1d, 0x5c, 0xb3, 0xc1, 0x71, 0x8c, 0x37, 0x1c, 0x27, 0x33, 0x90, + 0xf4, 0x4f, 0xd3, 0x44, 0xef, 0x0b, 0x12, 0x76, 0x40, 0xe0, 0x1e, 0xb8, 0x9d, 0x13, 0x91, 0x9f, + 0x9a, 0xc8, 0xc3, 0x79, 0x0d, 0x79, 0xa0, 0x18, 0x5b, 0x33, 0x32, 0xfd, 0xb5, 0xc7, 0x28, 0xad, + 0x6d, 0xbd, 0x49, 0x7a, 0x1b, 0x65, 0x58, 0x00, 0xe1, 0x58, 0x39, 0xa7, 0xfb, 0x8a, 0x78, 0x59, + 0x17, 0xe7, 0x6f, 0x19, 0xe1, 0xc7, 0x69, 0x94, 0x1b, 0x09, 0xd1, 0x14, 0xf2, 0x1a, 0xbf, 0x8b, + 0x38, 0xe6, 0x2e, 0x4a, 0xbe, 0x55, 0x4d, 0x62, 0xb5, 0xb9, 0x8d, 0x92, 0x5c, 0x10, 0x11, 0x71, + 0xd5, 0xa3, 0x0b, 0x9b, 0x9f, 0xfc, 0xdf, 0x30, 0x78, 0xe9, 0x20, 0x11, 0xb7, 0x62, 0xa9, 0x79, + 0x0f, 0x21, 0x07, 0x3c, 0xcc, 0x4f, 0x48, 0x08, 0x5c, 0xb5, 0xe5, 0xf8, 0x57, 0x6d, 0xce, 0x01, + 0xaf, 0xa9, 0x00, 0xd2, 0xf6, 0xb8, 0x81, 0x05, 0xeb, 0x02, 0xe5, 0x13, 0xb6, 0xee, 0xbc, 0x86, + 0xb4, 0x14, 0x63, 0xc8, 0x98, 0x7f, 0x66, 0xd1, 0xc2, 0x3e, 0x08, 0xdd, 0xca, 0xda, 0x92, 0x6f, + 0xd0, 0x9c, 0xef, 0x52, 0xa1, 0x5b, 0xc5, 0x98, 0x28, 0xff, 0x94, 0x04, 0xa8, 0x31, 0xf5, 0x18, + 0xe5, 0xb8, 0xe8, 0x9e, 0x05, 0xa1, 0xc0, 0x82, 0x09, 0xe2, 0x61, 0x1e, 0x05, 0x81, 0x77, 0x3e, + 0xa1, 0x51, 0x66, 0xcc, 0x6a, 0x49, 0x54, 0x53, 0x91, 0x64, 0xbd, 0x29, 0x88, 0xfe, 0x60, 0x9b, + 0x6c, 0xb8, 0xce, 0xd1, 0x7e, 0x09, 0xe4, 0xc4, 0xd6, 0x89, 0xbe, 0xb5, 0x89, 0x0b, 0x8a, 0x53, + 0x1f, 0x38, 0xf9, 0x2d, 0x5a, 0xd4, 0xe4, 0x77, 0xe1, 0x67, 0x56, 0xa1, 0xf6, 0x86, 0x4c, 0x35, + 0x8f, 0xd1, 0xb2, 0xe6, 0x87, 0xe0, 0x13, 0x97, 0xba, 0xb4, 0x83, 0x43, 0xe8, 0x91, 0xd0, 0xe9, + 0x0f, 0xe2, 0x71, 0x0f, 0xb0, 0xa4, 0x70, 0x56, 0x9f, 0x66, 0x69, 0xd8, 0x4d, 0x9c, 0x88, 0xca, + 0xf7, 0xad, 0x8c, 0xd3, 0x26, 0x1e, 0xa1, 0x36, 0xc4, 0x43, 0x7a, 0xdc, 0xb3, 0xe8, 0x38, 0x87, + 0x7d, 0x5a, 0x4d, 0xc3, 0xcc, 0x47, 0x28, 0x1b, 0x84, 0xec, 0xec, 0x5c, 0xbe, 0x0a, 0x06, 0x11, + 0x52, 0x13, 0x45, 0x48, 0x2b, 0x50, 0xd5, 0xb6, 0x63, 0xb6, 0x6a, 0x00, 0x43, 0x36, 0xc0, 0x9d, + 0xdf, 0x0c, 0x94, 0x1e, 0x69, 0x65, 0xf3, 0x2b, 0xf4, 0xe1, 0x51, 0x75, 0xaf, 0x51, 0xaf, 0xb6, + 0xee, 0x5b, 0xb8, 0xd9, 0xaa, 0xb6, 0x0e, 0x9b, 0xf8, 0x70, 0xbf, 0x79, 0xb0, 0xb3, 0xdd, 0xd8, + 0x6d, 0xec, 0xd4, 0x33, 0x89, 0x42, 0xf1, 0xe2, 0xb2, 0x54, 0x18, 0x91, 0x1d, 0x52, 0x1e, 0x80, + 0xed, 0x1e, 0xbb, 0xe0, 0x98, 0x5f, 0xa0, 0xe5, 0x57, 0x08, 0xd5, 0xed, 0x56, 0xe3, 0x68, 0x27, + 0x63, 0x14, 0x56, 0x2e, 0x2e, 0x4b, 0x4b, 0x23, 0xe2, 0xaa, 0x2d, 0xdc, 0x53, 0x30, 0xb7, 0xd0, + 0xca, 0x2b, 0xba, 0xc6, 0x7e, 0xac, 0x9c, 0x2a, 0x7c, 0x70, 0x71, 0x59, 0x5a, 0x1e, 0x51, 0x36, + 0x28, 0x51, 0xda, 0xc2, 0xcc, 0x93, 0x9f, 0x8b, 0x89, 0xda, 0xc3, 0x67, 0x57, 0x45, 0xe3, 0xf9, + 0x55, 0xd1, 0xf8, 0xeb, 0xaa, 0x68, 0xfc, 0x70, 0x5d, 0x4c, 0x3c, 0xbf, 0x2e, 0x26, 0xfe, 0xb8, + 0x2e, 0x26, 0x1e, 0x7d, 0x39, 0x54, 0xac, 0x00, 0x42, 0x2e, 0x5f, 0x53, 0xd4, 0x86, 0xfb, 0x14, + 0x2a, 0x7a, 0xcc, 0x6d, 0x50, 0x22, 0x41, 0x95, 0xd3, 0xcd, 0xca, 0xd9, 0x4b, 0x9f, 0x74, 0xaa, + 0x90, 0xed, 0xa4, 0xfa, 0xf0, 0xfa, 0xfc, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf0, 0xf7, 0xf1, + 0x53, 0xf5, 0x09, 0x00, 0x00, } func (m *Params) Marshal() (dAtA []byte, err error) { @@ -405,6 +415,23 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.AutocompoundFeeRate.Size() + i -= size + if _, err := m.AutocompoundFeeRate.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintLiquidstake(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x42 + if len(m.FeeAccountAddress) > 0 { + i -= len(m.FeeAccountAddress) + copy(dAtA[i:], m.FeeAccountAddress) + i = encodeVarintLiquidstake(dAtA, i, uint64(len(m.FeeAccountAddress))) + i-- + dAtA[i] = 0x3a + } if len(m.CwLockedPoolAddress) > 0 { i -= len(m.CwLockedPoolAddress) copy(dAtA[i:], m.CwLockedPoolAddress) @@ -742,6 +769,12 @@ func (m *Params) Size() (n int) { if l > 0 { n += 1 + l + sovLiquidstake(uint64(l)) } + l = len(m.FeeAccountAddress) + if l > 0 { + n += 1 + l + sovLiquidstake(uint64(l)) + } + l = m.AutocompoundFeeRate.Size() + n += 1 + l + sovLiquidstake(uint64(l)) return n } @@ -1041,6 +1074,72 @@ func (m *Params) Unmarshal(dAtA []byte) error { } m.CwLockedPoolAddress = string(dAtA[iNdEx:postIndex]) iNdEx = postIndex + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field FeeAccountAddress", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLiquidstake + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLiquidstake + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLiquidstake + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.FeeAccountAddress = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field AutocompoundFeeRate", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLiquidstake + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLiquidstake + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLiquidstake + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.AutocompoundFeeRate.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipLiquidstake(dAtA[iNdEx:]) diff --git a/x/liquidstake/types/liquidstake_test.go b/x/liquidstake/types/liquidstake_test.go new file mode 100644 index 000000000..e242c70a0 --- /dev/null +++ b/x/liquidstake/types/liquidstake_test.go @@ -0,0 +1,452 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + abci "github.com/cometbft/cometbft/abci/types" + tmproto "github.com/cometbft/cometbft/proto/tendermint/types" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/mint" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + chain "github.com/persistenceOne/pstake-native/v2/app" + testhelpers "github.com/persistenceOne/pstake-native/v2/app/helpers" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/keeper" + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +var ( + whitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "cosmosvaloper10e4vsut6suau8tk9m6dnrm0slgd6npe3jx5xpv", + TargetWeight: math.NewInt(10), + }, + { + ValidatorAddress: "cosmosvaloper18hfzxheyknesfgcrttr5dg50ffnfphtwtar9fz", + TargetWeight: math.NewInt(1), + }, + { + ValidatorAddress: "cosmosvaloper18hfzxheyknesfgcrttr5dg50ffnfphtwtar9fz", + TargetWeight: math.NewInt(-1), + }, + { + ValidatorAddress: "cosmosvaloper1ld6vlyy24906u3aqp5lj54f3nsg2592nm9nj5c", + TargetWeight: math.NewInt(0), + }, + } +) + +func TestStkXPRTToNativeTokenWithFee(t *testing.T) { + testCases := []struct { + stkXPRTAmount math.Int + stkXPRTTotalSupplyAmount math.Int + netAmount math.LegacyDec + feeRate math.LegacyDec + expectedOutput math.LegacyDec + }{ + // reward added case + { + stkXPRTAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(5100000000), + feeRate: math.LegacyMustNewDecFromStr("0.0"), + expectedOutput: math.LegacyMustNewDecFromStr("102000000.0"), + }, + // reward added case with fee + { + stkXPRTAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(5100000000), + feeRate: math.LegacyMustNewDecFromStr("0.005"), + expectedOutput: math.LegacyMustNewDecFromStr("101490000.0"), + }, + // slashed case + { + stkXPRTAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(4000000000), + feeRate: math.LegacyMustNewDecFromStr("0.0"), + expectedOutput: math.LegacyMustNewDecFromStr("80000000.0"), + }, + // slashed case with fee + { + stkXPRTAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(4000000000), + feeRate: math.LegacyMustNewDecFromStr("0.001"), + expectedOutput: math.LegacyMustNewDecFromStr("79920000.0"), + }, + } + + for _, tc := range testCases { + require.IsType(t, math.Int{}, tc.stkXPRTAmount) + require.IsType(t, math.Int{}, tc.stkXPRTTotalSupplyAmount) + require.IsType(t, math.LegacyDec{}, tc.netAmount) + require.IsType(t, math.LegacyDec{}, tc.feeRate) + require.IsType(t, math.LegacyDec{}, tc.expectedOutput) + + output := types.StkXPRTToNativeToken(tc.stkXPRTAmount, tc.stkXPRTTotalSupplyAmount, tc.netAmount) + if tc.feeRate.IsPositive() { + output = types.DeductFeeRate(output, tc.feeRate) + } + require.EqualValues(t, tc.expectedOutput, output) + } +} + +func TestNativeToStkXPRTTo(t *testing.T) { + testCases := []struct { + nativeTokenAmount math.Int + stkXPRTTotalSupplyAmount math.Int + netAmount math.LegacyDec + expectedOutput math.Int + }{ + { + nativeTokenAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(5000000000), + expectedOutput: math.NewInt(100000000), + }, + { + nativeTokenAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(4000000000), + expectedOutput: math.NewInt(125000000), + }, + { + nativeTokenAmount: math.NewInt(100000000), + stkXPRTTotalSupplyAmount: math.NewInt(5000000000), + netAmount: math.LegacyNewDec(55000000000), + expectedOutput: math.NewInt(9090909), + }, + } + + for _, tc := range testCases { + require.IsType(t, math.Int{}, tc.nativeTokenAmount) + require.IsType(t, math.Int{}, tc.stkXPRTTotalSupplyAmount) + require.IsType(t, math.LegacyDec{}, tc.netAmount) + require.IsType(t, math.Int{}, tc.expectedOutput) + + output := types.NativeTokenToStkXPRT(tc.nativeTokenAmount, tc.stkXPRTTotalSupplyAmount, tc.netAmount) + require.EqualValues(t, tc.expectedOutput, output) + } +} + +func TestActiveCondition(t *testing.T) { + testCases := []struct { + validator stakingtypes.Validator + whitelisted bool + tombstoned bool + expectedOutput bool + }{ + // active case 1 + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(100000000), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: true, + tombstoned: false, + expectedOutput: true, + }, + // active case 2 + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: true, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(100000000), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: true, + tombstoned: false, + expectedOutput: true, + }, + // inactive case 1 (not whitelisted) + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(100000000), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: false, + tombstoned: false, + expectedOutput: false, + }, + // inactive case 2 (invalid tokens, delShares) + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.Int{}, + DelegatorShares: math.LegacyDec{}, + }, + whitelisted: true, + tombstoned: false, + expectedOutput: false, + }, + // inactive case 3 (zero tokens) + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: math.NewInt(0), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: true, + tombstoned: false, + expectedOutput: false, + }, + // inactive case 4 (invalid status) + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Unspecified, + Tokens: math.NewInt(100000000), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: true, + tombstoned: false, + expectedOutput: false, + }, + // inactive case 5 (tombstoned) + { + validator: stakingtypes.Validator{ + OperatorAddress: whitelistedValidators[0].ValidatorAddress, + Jailed: false, + Status: stakingtypes.Unbonding, + Tokens: math.NewInt(100000000), + DelegatorShares: math.LegacyNewDec(100000000), + }, + whitelisted: true, + tombstoned: true, + expectedOutput: false, + }, + } + + for _, tc := range testCases { + require.IsType(t, stakingtypes.Validator{}, tc.validator) + output := types.ActiveCondition(tc.validator, tc.whitelisted, tc.tombstoned) + require.EqualValues(t, tc.expectedOutput, output) + } +} + +type KeeperTestSuite struct { + suite.Suite + + app *chain.PstakeApp + ctx sdk.Context + keeper keeper.Keeper + querier keeper.Querier + addrs []sdk.AccAddress + delAddrs []sdk.AccAddress + valAddrs []sdk.ValAddress +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (s *KeeperTestSuite) SetupTest() { + s.app = testhelpers.Setup(s.T(), false, 5) + s.ctx = s.app.BaseApp.NewContext(false, tmproto.Header{}) + stakingParams := stakingtypes.DefaultParams() + stakingParams.MaxEntries = 7 + stakingParams.MaxValidators = 30 + s.Require().NoError(s.app.StakingKeeper.SetParams(s.ctx, stakingParams)) + + s.keeper = s.app.LiquidStakeKeeper + s.querier = keeper.Querier{Keeper: s.keeper} + s.addrs = testhelpers.AddTestAddrs(s.app, s.ctx, 10, math.NewInt(1_000_000_000)) + s.delAddrs = testhelpers.AddTestAddrs(s.app, s.ctx, 10, math.NewInt(1_000_000_000)) + s.valAddrs = testhelpers.ConvertAddrsToValAddrs(s.delAddrs) + + s.ctx = s.ctx.WithBlockHeight(100).WithBlockTime(testhelpers.ParseTime("2022-03-01T00:00:00Z")) + params := s.keeper.GetParams(s.ctx) + params.UnstakeFeeRate = sdk.ZeroDec() + s.Require().NoError(s.keeper.SetParams(s.ctx, params)) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + // call mint.BeginBlocker for init k.SetLastBlockTime(ctx, ctx.BlockTime()) + mint.BeginBlocker(s.ctx, s.app.MintKeeper, minttypes.DefaultInflationCalculationFn) +} + +func (s *KeeperTestSuite) CreateValidators(powers []int64) ([]sdk.AccAddress, []sdk.ValAddress, []cryptotypes.PubKey) { + s.app.BeginBlocker(s.ctx, abci.RequestBeginBlock{}) + num := len(powers) + addrs := testhelpers.AddTestAddrsIncremental(s.app, s.ctx, num, math.NewInt(1000000000)) + valAddrs := testhelpers.ConvertAddrsToValAddrs(addrs) + pks := testhelpers.CreateTestPubKeys(num) + + for i, power := range powers { + val, err := stakingtypes.NewValidator(valAddrs[i], pks[i], stakingtypes.Description{}) + s.Require().NoError(err) + s.app.StakingKeeper.SetValidator(s.ctx, val) + err = s.app.StakingKeeper.SetValidatorByConsAddr(s.ctx, val) + s.Require().NoError(err) + s.app.StakingKeeper.SetNewValidatorByPowerIndex(s.ctx, val) + s.app.StakingKeeper.Hooks().AfterValidatorCreated(s.ctx, val.GetOperator()) + newShares, err := s.app.StakingKeeper.Delegate(s.ctx, addrs[i], math.NewInt(power), stakingtypes.Unbonded, val, true) + s.Require().NoError(err) + s.Require().Equal(newShares.TruncateInt(), math.NewInt(power)) + } + + s.app.EndBlocker(s.ctx, abci.RequestEndBlock{}) + return addrs, valAddrs, pks +} + +func (s *KeeperTestSuite) TestLiquidStake() { + _, valOpers, _ := s.CreateValidators([]int64{1000000, 2000000, 3000000}) + params := s.keeper.GetParams(s.ctx) + params.MinLiquidStakeAmount = math.NewInt(50000) + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + stakingAmt := params.MinLiquidStakeAmount + + // fail, no active validator + cachedCtx, _ := s.ctx.CacheContext() + newShares, stkXPRTMintAmt, err := s.keeper.LiquidStake(cachedCtx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt)) + s.Require().ErrorIs(err, types.ErrActiveLiquidValidatorsNotExists) + s.Require().Equal(newShares, sdk.ZeroDec()) + s.Require().Equal(stkXPRTMintAmt, sdk.ZeroInt()) + + // add active validator + params.WhitelistedValidators = []types.WhitelistedValidator{ + {ValidatorAddress: valOpers[0].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[1].String(), TargetWeight: math.NewInt(1)}, + {ValidatorAddress: valOpers[2].String(), TargetWeight: math.NewInt(1)}, + } + s.keeper.SetParams(s.ctx, params) + s.keeper.UpdateLiquidValidatorSet(s.ctx) + + res := s.keeper.GetAllLiquidValidatorStates(s.ctx) + s.Require().Equal(params.WhitelistedValidators[0].ValidatorAddress, res[0].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[0].TargetWeight, res[0].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[0].Status) + s.Require().Equal(sdk.ZeroDec(), res[0].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[0].LiquidTokens) + + s.Require().Equal(params.WhitelistedValidators[1].ValidatorAddress, res[1].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[1].TargetWeight, res[1].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[1].Status) + s.Require().Equal(sdk.ZeroDec(), res[1].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[1].LiquidTokens) + + s.Require().Equal(params.WhitelistedValidators[2].ValidatorAddress, res[2].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[2].TargetWeight, res[2].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[2].Status) + s.Require().Equal(sdk.ZeroDec(), res[2].DelShares) + s.Require().Equal(sdk.ZeroInt(), res[2].LiquidTokens) + + // liquid stake + newShares, stkXPRTMintAmt, err = s.keeper.LiquidStake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], sdk.NewCoin(sdk.DefaultBondDenom, stakingAmt)) + s.Require().NoError(err) + s.Require().Equal(newShares, stakingAmt.ToLegacyDec()) + s.Require().Equal(stkXPRTMintAmt, stakingAmt) + + _, found := s.app.StakingKeeper.GetDelegation(s.ctx, s.delAddrs[0], valOpers[0]) + s.Require().False(found) + _, found = s.app.StakingKeeper.GetDelegation(s.ctx, s.delAddrs[0], valOpers[1]) + s.Require().False(found) + _, found = s.app.StakingKeeper.GetDelegation(s.ctx, s.delAddrs[0], valOpers[2]) + s.Require().False(found) + + proxyAccDel1, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found := s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + s.Require().Equal(proxyAccDel1.Shares, math.LegacyNewDec(16668)) // 16666 + add crumb 2 to 1st active validator + s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666)) + s.Require().Equal(proxyAccDel2.Shares, math.LegacyNewDec(16666)) + s.Require().Equal(stakingAmt.ToLegacyDec(), proxyAccDel1.Shares.Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares)) + + liquidBondDenom := s.keeper.LiquidBondDenom(s.ctx) + balanceBeforeUBD := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], sdk.DefaultBondDenom) + s.Require().Equal(balanceBeforeUBD.Amount, math.NewInt(999950000)) + ubdStkXPRT := sdk.NewCoin(liquidBondDenom, math.NewInt(10000)) + stkXPRTBalance := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], liquidBondDenom) + stkXPRTTotalSupply := s.app.BankKeeper.GetSupply(s.ctx, liquidBondDenom) + s.Require().Equal(stkXPRTBalance, sdk.NewCoin(liquidBondDenom, math.NewInt(50000))) + s.Require().Equal(stkXPRTBalance, stkXPRTTotalSupply) + + // liquid unstaking + ubdTime, unbondingAmt, ubds, unbondedAmt, err := s.keeper.LiquidUnstake(s.ctx, types.LiquidStakeProxyAcc, s.delAddrs[0], ubdStkXPRT) + s.Require().NoError(err) + s.Require().EqualValues(unbondedAmt, sdk.ZeroInt()) + s.Require().Len(ubds, 3) + + // crumb excepted on unbonding + crumb := ubdStkXPRT.Amount.Sub(ubdStkXPRT.Amount.QuoRaw(3).MulRaw(3)) // 1 + s.Require().EqualValues(unbondingAmt, ubdStkXPRT.Amount.Sub(crumb)) // 9999 + s.Require().Equal(ubds[0].DelegatorAddress, s.delAddrs[0].String()) + s.Require().Equal(ubdTime, testhelpers.ParseTime("2022-03-22T00:00:00Z")) + stkXPRTBalanceAfter := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], liquidBondDenom) + s.Require().Equal(stkXPRTBalanceAfter, sdk.NewCoin(liquidBondDenom, math.NewInt(40000))) + + balanceBeginUBD := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], sdk.DefaultBondDenom) + s.Require().Equal(balanceBeginUBD.Amount, balanceBeforeUBD.Amount) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + s.Require().Equal(stakingAmt.Sub(unbondingAmt).ToLegacyDec(), proxyAccDel1.GetShares().Add(proxyAccDel2.Shares).Add(proxyAccDel3.Shares)) + + // complete unbonding + s.ctx = s.ctx.WithBlockHeight(200).WithBlockTime(ubdTime.Add(1)) + updates := s.app.StakingKeeper.BlockValidatorUpdates(s.ctx) // EndBlock of staking keeper, mature UBD + s.Require().Empty(updates) + balanceCompleteUBD := s.app.BankKeeper.GetBalance(s.ctx, s.delAddrs[0], sdk.DefaultBondDenom) + s.Require().Equal(balanceCompleteUBD.Amount, balanceBeforeUBD.Amount.Add(unbondingAmt)) + + proxyAccDel1, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[0]) + s.Require().True(found) + proxyAccDel2, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[1]) + s.Require().True(found) + proxyAccDel3, found = s.app.StakingKeeper.GetDelegation(s.ctx, types.LiquidStakeProxyAcc, valOpers[2]) + s.Require().True(found) + // crumb added to first valid active liquid validator + s.Require().Equal(math.LegacyNewDec(13335), proxyAccDel1.Shares) + s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel2.Shares) + s.Require().Equal(math.LegacyNewDec(13333), proxyAccDel3.Shares) + + res = s.keeper.GetAllLiquidValidatorStates(s.ctx) + s.Require().Equal(params.WhitelistedValidators[0].ValidatorAddress, res[0].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[0].TargetWeight, res[0].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[0].Status) + s.Require().Equal(math.LegacyNewDec(13335), res[0].DelShares) + + s.Require().Equal(params.WhitelistedValidators[1].ValidatorAddress, res[1].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[1].TargetWeight, res[1].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[1].Status) + s.Require().Equal(math.LegacyNewDec(13333), res[1].DelShares) + + s.Require().Equal(params.WhitelistedValidators[2].ValidatorAddress, res[2].OperatorAddress) + s.Require().Equal(params.WhitelistedValidators[2].TargetWeight, res[2].Weight) + s.Require().Equal(types.ValidatorStatusActive, res[2].Status) + s.Require().Equal(math.LegacyNewDec(13333), res[2].DelShares) + + vs := s.keeper.GetAllLiquidValidators(s.ctx) + s.Require().Len(vs.Map(), 3) + + whitelistedValsMap := types.GetWhitelistedValsMap(params.WhitelistedValidators) + avs := s.keeper.GetActiveLiquidValidators(s.ctx, whitelistedValsMap) + alt, _ := avs.TotalActiveLiquidTokens(s.ctx, s.app.StakingKeeper, true) + s.Require().EqualValues(alt, math.NewInt(40001)) +} diff --git a/x/liquidstake/types/msgs.go b/x/liquidstake/types/msgs.go index c45ab9ea4..65e9bdfc7 100644 --- a/x/liquidstake/types/msgs.go +++ b/x/liquidstake/types/msgs.go @@ -1,7 +1,7 @@ package types import ( - errorsmod "cosmossdk.io/errors" + "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -37,10 +37,10 @@ func (m *MsgLiquidStake) Type() string { return MsgTypeLiquidStake } func (m *MsgLiquidStake) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(m.DelegatorAddress); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) + return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) } if ok := m.Amount.IsZero(); ok { - return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "staking amount must not be zero") + return errors.Wrap(sdkerrors.ErrInvalidRequest, "staking amount must not be zero") } if err := m.Amount.Validate(); err != nil { return err @@ -89,13 +89,13 @@ func (m *MsgStakeToLP) Type() string { return MsgTypeStakeToLP } func (m *MsgStakeToLP) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(m.DelegatorAddress); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) + return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) } if _, err := sdk.ValAddressFromBech32(m.ValidatorAddress); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address %q: %v", m.ValidatorAddress, err) + return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address %q: %v", m.ValidatorAddress, err) } if ok := m.StakedAmount.IsZero(); ok { - return errorsmod.Wrap(sdkerrors.ErrInvalidRequest, "staking amount must not be zero") + return errors.Wrap(sdkerrors.ErrInvalidRequest, "staking amount must not be zero") } if err := m.StakedAmount.Validate(); err != nil { return err @@ -151,10 +151,10 @@ func (m *MsgLiquidUnstake) Type() string { return MsgTypeLiquidUnstake } func (m *MsgLiquidUnstake) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(m.DelegatorAddress); err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) + return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid delegator address %q: %v", m.DelegatorAddress, err) } if ok := m.Amount.IsZero(); ok { - return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "unstaking amount must not be zero") + return errors.Wrap(sdkerrors.ErrInvalidRequest, "unstaking amount must not be zero") } if err := m.Amount.Validate(); err != nil { return err @@ -215,7 +215,7 @@ func (m *MsgUpdateParams) GetSigners() []sdk.AccAddress { func (m *MsgUpdateParams) ValidateBasic() error { if _, err := sdk.AccAddressFromBech32(m.Authority); err != nil { - return errorsmod.Wrapf(sdkerrors.ErrInvalidAddress, "invalid authority address %q: %v", m.Authority, err) + return errors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid authority address %q: %v", m.Authority, err) } err := m.Params.Validate() diff --git a/x/liquidstake/types/msgs_test.go b/x/liquidstake/types/msgs_test.go new file mode 100644 index 000000000..e314ccf6e --- /dev/null +++ b/x/liquidstake/types/msgs_test.go @@ -0,0 +1,92 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + "github.com/cometbft/cometbft/crypto" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func TestMsgLiquidStake(t *testing.T) { + delegatorAddr := sdk.AccAddress(crypto.AddressHash([]byte("delegatorAddr"))) + stakingCoin := sdk.NewCoin("uxprt", math.NewInt(1)) + + testCases := []struct { + expectedErr string + msg *types.MsgLiquidStake + }{ + { + "", // empty means no error expected + types.NewMsgLiquidStake(delegatorAddr, stakingCoin), + }, + { + "invalid delegator address \"\": empty address string is not allowed: invalid address", + types.NewMsgLiquidStake(sdk.AccAddress{}, stakingCoin), + }, + { + "staking amount must not be zero: invalid request", + types.NewMsgLiquidStake(delegatorAddr, sdk.NewCoin("token", math.NewInt(0))), + }, + } + + for _, tc := range testCases { + require.IsType(t, &types.MsgLiquidStake{}, tc.msg) + require.Equal(t, types.MsgTypeLiquidStake, tc.msg.Type()) + require.Equal(t, types.RouterKey, tc.msg.Route()) + require.Equal(t, sdk.MustSortJSON(types.ModuleCdc.MustMarshalJSON(tc.msg)), tc.msg.GetSignBytes()) + + err := tc.msg.ValidateBasic() + if tc.expectedErr == "" { + require.Nil(t, err) + signers := tc.msg.GetSigners() + require.Len(t, signers, 1) + require.Equal(t, tc.msg.GetDelegator(), signers[0]) + } else { + require.EqualError(t, err, tc.expectedErr) + } + } +} + +func TestMsgLiquidUnstake(t *testing.T) { + delegatorAddr := sdk.AccAddress(crypto.AddressHash([]byte("delegatorAddr"))) + stakingCoin := sdk.NewCoin("stk/uxprt", math.NewInt(1)) + + testCases := []struct { + expectedErr string + msg *types.MsgLiquidUnstake + }{ + { + "", // empty means no error expected + types.NewMsgLiquidUnstake(delegatorAddr, stakingCoin), + }, + { + "invalid delegator address \"\": empty address string is not allowed: invalid address", + types.NewMsgLiquidUnstake(sdk.AccAddress{}, stakingCoin), + }, + { + "unstaking amount must not be zero: invalid request", + types.NewMsgLiquidUnstake(delegatorAddr, sdk.NewCoin("btoken", math.NewInt(0))), + }, + } + + for _, tc := range testCases { + require.IsType(t, &types.MsgLiquidUnstake{}, tc.msg) + require.Equal(t, types.MsgTypeLiquidUnstake, tc.msg.Type()) + require.Equal(t, types.RouterKey, tc.msg.Route()) + require.Equal(t, sdk.MustSortJSON(types.ModuleCdc.MustMarshalJSON(tc.msg)), tc.msg.GetSignBytes()) + + err := tc.msg.ValidateBasic() + if tc.expectedErr == "" { + require.Nil(t, err) + signers := tc.msg.GetSigners() + require.Len(t, signers, 1) + require.Equal(t, tc.msg.GetDelegator(), signers[0]) + } else { + require.EqualError(t, err, tc.expectedErr) + } + } +} diff --git a/x/liquidstake/types/params.go b/x/liquidstake/types/params.go index 8439efab7..858d3783b 100644 --- a/x/liquidstake/types/params.go +++ b/x/liquidstake/types/params.go @@ -1,13 +1,13 @@ package types import ( + "encoding/json" "fmt" "strings" "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - "gopkg.in/yaml.v2" ) // Parameter store keys @@ -17,20 +17,26 @@ var ( // DefaultUnstakeFeeRate is the default Unstake Fee Rate. DefaultUnstakeFeeRate = sdk.ZeroDec() - // DefaultMinLiquidStakeAmount is the default minimum liquid staking amount. - DefaultMinLiquidStakeAmount = sdk.NewInt(1000) + // DefaultAutocompoundFeeRate is the default fee rate for auto redelegating the stake rewards. + DefaultAutocompoundFeeRate = sdk.MustNewDecFromStr("0.05") + + // DefaultMinLiquidStakeAmount is the default minimum liquid stake amount. + DefaultMinLiquidStakeAmount = math.NewInt(1000) // Const variables // RebalancingTrigger if the maximum difference and needed each redelegation amount exceeds it, asset rebalacing will be executed. - RebalancingTrigger = sdk.NewDecWithPrec(1, 3) // "0.001000000000000000" + RebalancingTrigger = math.LegacyNewDecWithPrec(1, 3) // "0.001000000000000000" // AutocompoundTrigger If the sum of balance and the upcoming rewards of LiquidStakeProxyAcc exceeds it, // the reward is automatically autocompounded, according to the weights. - AutocompoundTrigger = sdk.NewDecWithPrec(1, 3) // "0.001000000000000000" + AutocompoundTrigger = math.LegacyNewDecWithPrec(1, 3) // "0.001000000000000000" // LiquidStakeProxyAcc is a proxy reserve account for delegation and undelegation. - LiquidStakeProxyAcc = authtypes.NewModuleAddress(ModuleName + "-LiquidStakingProxyAcc") + LiquidStakeProxyAcc = authtypes.NewModuleAddress(ModuleName + "-LiquidStakeProxyAcc") + + // DummyFeeAccountAcc is a dummy fee collection account that should be replaced via params. + DummyFeeAccountAcc = authtypes.NewModuleAddress(ModuleName + "-FeeAcc") ) // DefaultParams returns the default liquidstake module parameters. @@ -40,12 +46,14 @@ func DefaultParams() Params { LiquidBondDenom: DefaultLiquidBondDenom, UnstakeFeeRate: DefaultUnstakeFeeRate, MinLiquidStakeAmount: DefaultMinLiquidStakeAmount, + FeeAccountAddress: DummyFeeAccountAcc.String(), + AutocompoundFeeRate: DefaultAutocompoundFeeRate, } } // String returns a human-readable string representation of the parameters. func (p Params) String() string { - out, _ := yaml.Marshal(p) + out, _ := json.MarshalIndent(p, "", "") return string(out) } @@ -63,6 +71,8 @@ func (p Params) Validate() error { {p.WhitelistedValidators, validateWhitelistedValidators}, {p.UnstakeFeeRate, validateUnstakeFeeRate}, {p.MinLiquidStakeAmount, validateMinLiquidStakeAmount}, + {p.AutocompoundFeeRate, validateAutocompoundFeeRate}, + {p.FeeAccountAddress, validateFeeAccountAddress}, } { if err := v.validator(v.value); err != nil { return err @@ -117,7 +127,7 @@ func validateWhitelistedValidators(i interface{}) error { } func validateUnstakeFeeRate(i interface{}) error { - v, ok := i.(sdk.Dec) + v, ok := i.(math.LegacyDec) if !ok { return fmt.Errorf("invalid parameter type: %T", i) } @@ -144,12 +154,45 @@ func validateMinLiquidStakeAmount(i interface{}) error { } if v.IsNil() { - return fmt.Errorf("min liquid staking amount must not be nil") + return fmt.Errorf("min liquid stake amount must not be nil") + } + + if v.IsNegative() { + return fmt.Errorf("min liquid stake amount must not be negative: %s", v) + } + + return nil +} + +func validateAutocompoundFeeRate(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.IsNil() { + return fmt.Errorf("autocompound fee rate must not be nil") } if v.IsNegative() { - return fmt.Errorf("min liquid staking amount must not be negative: %s", v) + return fmt.Errorf("autocompound fee rate must not be negative: %s", v) } + if v.GT(sdk.OneDec()) { + return fmt.Errorf("autocompound fee rate too large: %s", v) + } + + return nil +} + +func validateFeeAccountAddress(i interface{}) error { + v, ok := i.(string) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + _, err := sdk.AccAddressFromBech32(v) + if err != nil { + return fmt.Errorf("cannot convert fee account address to bech32, invalid address: %s, err: %v", v, err) + } return nil } diff --git a/x/liquidstake/types/params_test.go b/x/liquidstake/types/params_test.go new file mode 100644 index 000000000..ca4f14eca --- /dev/null +++ b/x/liquidstake/types/params_test.go @@ -0,0 +1,205 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +func TestParams(t *testing.T) { + + params := types.DefaultParams() + + paramsStr := `{ +"liquid_bond_denom": "stk/uxprt", +"whitelisted_validators": [], +"unstake_fee_rate": "0.000000000000000000", +"min_liquid_stake_amount": "1000", +"fee_account_address": "persistence1f0lfxf7d4sxe7y4h8k9zp9d5f6avppsrv9uy8r", +"autocompound_fee_rate": "0.050000000000000000" +}` + require.Equal(t, paramsStr, params.String()) + + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: math.NewInt(10), + }, + } + paramsStr = `{ +"liquid_bond_denom": "stk/uxprt", +"whitelisted_validators": [ +{ +"validator_address": "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", +"target_weight": "10" +} +], +"unstake_fee_rate": "0.000000000000000000", +"min_liquid_stake_amount": "1000", +"fee_account_address": "persistence1f0lfxf7d4sxe7y4h8k9zp9d5f6avppsrv9uy8r", +"autocompound_fee_rate": "0.050000000000000000" +}` + require.Equal(t, paramsStr, params.String()) +} + +func TestWhitelistedValsMap(t *testing.T) { + params := types.DefaultParams() + require.EqualValues(t, params.WhitelistedValsMap(), types.WhitelistedValsMap{}) + + params.WhitelistedValidators = []types.WhitelistedValidator{ + whitelistedValidators[0], + whitelistedValidators[1], + } + + wvm := params.WhitelistedValsMap() + require.Len(t, params.WhitelistedValidators, len(wvm)) + + for _, wv := range params.WhitelistedValidators { + require.EqualValues(t, wvm[wv.ValidatorAddress], wv) + require.True(t, wvm.IsListed(wv.ValidatorAddress)) + } + + require.False(t, wvm.IsListed("notExistedAddr")) +} + +func TestValidateWhitelistedValidators(t *testing.T) { + for _, tc := range []struct { + name string + malleate func(*types.Params) + errStr string + }{ + { + "valid default params", + func(params *types.Params) {}, + "", + }, + { + "blank liquid bond denom", + func(params *types.Params) { + params.LiquidBondDenom = "" + }, + "liquid bond denom cannot be blank", + }, + { + "invalid liquid bond denom", + func(params *types.Params) { + params.LiquidBondDenom = "a" + }, + "invalid denom: a", + }, + { + "duplicated whitelisted validators", + func(params *types.Params) { + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: math.NewInt(10), + }, + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: math.NewInt(10), + }, + } + }, + "liquidstake validator cannot be duplicated: persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + }, + { + "invalid whitelisted validator address", + func(params *types.Params) { + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "invalidaddr", + TargetWeight: math.NewInt(10), + }, + } + }, + "decoding bech32 failed: invalid separator index -1", + }, + { + "nil whitelisted validator target weight", + func(params *types.Params) { + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: math.Int{}, + }, + } + }, + "liquidstake validator target weight must not be nil", + }, + { + "negative whitelisted validator target weight", + func(params *types.Params) { + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: math.NewInt(-1), + }, + } + }, + "liquidstake validator target weight must be positive: -1", + }, + { + "zero whitelisted validator target weight", + func(params *types.Params) { + params.WhitelistedValidators = []types.WhitelistedValidator{ + { + ValidatorAddress: "persistencevaloper19rz0gtqf88vwk6dwz522ajpqpv5swunqm9z90m", + TargetWeight: sdk.ZeroInt(), + }, + } + }, + "liquidstake validator target weight must be positive: 0", + }, + { + "nil unstake fee rate", + func(params *types.Params) { + params.UnstakeFeeRate = sdk.Dec{} + }, + "unstake fee rate must not be nil", + }, + { + "negative unstake fee rate", + func(params *types.Params) { + params.UnstakeFeeRate = math.LegacyNewDec(-1) + }, + "unstake fee rate must not be negative: -1.000000000000000000", + }, + { + "too large unstake fee rate", + func(params *types.Params) { + params.UnstakeFeeRate = math.LegacyMustNewDecFromStr("1.0000001") + }, + "unstake fee rate too large: 1.000000100000000000", + }, + { + "nil min liquid stake amount", + func(params *types.Params) { + params.MinLiquidStakeAmount = math.Int{} + }, + "min liquid stake amount must not be nil", + }, + { + "negative min liquid stake amount", + func(params *types.Params) { + params.MinLiquidStakeAmount = math.NewInt(-1) + }, + "min liquid stake amount must not be negative: -1", + }, + } { + t.Run(tc.name, func(t *testing.T) { + params := types.DefaultParams() + tc.malleate(¶ms) + err := params.Validate() + if tc.errStr == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.errStr) + } + }) + } +} diff --git a/x/liquidstake/types/rebalancing.go b/x/liquidstake/types/rebalancing.go index 004900f5a..8e1c214a9 100644 --- a/x/liquidstake/types/rebalancing.go +++ b/x/liquidstake/types/rebalancing.go @@ -23,7 +23,7 @@ func DivideByWeight(avs ActiveLiquidValidators, input math.Int, whitelistedValsM } totalOutput := sdk.ZeroInt() - unitInput := sdk.NewDecFromInt(input).QuoTruncate(sdk.NewDecFromInt(totalWeight)) + unitInput := math.LegacyNewDecFromInt(input).QuoTruncate(math.LegacyNewDecFromInt(totalWeight)) for _, val := range avs { output := unitInput.MulInt(val.GetWeight(whitelistedValsMap, true)).TruncateInt() totalOutput = totalOutput.Add(output) @@ -35,15 +35,15 @@ func DivideByWeight(avs ActiveLiquidValidators, input math.Int, whitelistedValsM // DivideByCurrentWeight divide the input value by the ratio of the weight of the liquid validator's liquid token and return it with crumb // which is may occur while dividing according to the weight of liquid validators by decimal error, outputs is truncated decimal. -func DivideByCurrentWeight(lvs LiquidValidators, input sdk.Dec, totalLiquidTokens math.Int, liquidTokenMap map[string]math.Int) (outputs []sdk.Dec, crumb sdk.Dec) { +func DivideByCurrentWeight(lvs LiquidValidators, input math.LegacyDec, totalLiquidTokens math.Int, liquidTokenMap map[string]math.Int) (outputs []math.LegacyDec, crumb math.LegacyDec) { if !totalLiquidTokens.IsPositive() { - return []sdk.Dec{}, sdk.ZeroDec() + return []math.LegacyDec{}, sdk.ZeroDec() } totalOutput := sdk.ZeroDec() - unitInput := input.QuoTruncate(sdk.NewDecFromInt(totalLiquidTokens)) + unitInput := input.QuoTruncate(math.LegacyNewDecFromInt(totalLiquidTokens)) for _, val := range lvs { - output := unitInput.MulTruncate(sdk.NewDecFromInt(liquidTokenMap[val.OperatorAddress])).TruncateDec() + output := unitInput.MulTruncate(math.LegacyNewDecFromInt(liquidTokenMap[val.OperatorAddress])).TruncateDec() totalOutput = totalOutput.Add(output) outputs = append(outputs, output) } diff --git a/x/liquidstake/types/rebalancing_test.go b/x/liquidstake/types/rebalancing_test.go new file mode 100644 index 000000000..3fcce76f7 --- /dev/null +++ b/x/liquidstake/types/rebalancing_test.go @@ -0,0 +1,430 @@ +package types_test + +import ( + "testing" + + "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/persistenceOne/pstake-native/v2/x/liquidstake/types" +) + +var ( + liquidValidators = []types.LiquidValidator{ + { + OperatorAddress: "persistencevaloper15kdfwczhpmccprekhlzrvkhzw92940l3w37qqj", + }, + { + OperatorAddress: "persistencevaloper1x73gyvh74ahs2rt9cqrpjkkk74nczwfpnskv3rczmsf0m6aj5dksqr58m3", + }, + { + OperatorAddress: "persistencevaloper10ngyx42lfpylpllm4k3g7fz4gufnt3ptyhm5pn", + }, + { + OperatorAddress: "persistencevaloper10fcwju2n8vvffkp8judj3skqpvnphasxjar5yx", + }, + } +) + +func TestDivideByWeight(t *testing.T) { + testCases := []struct { + whitelistedVals []types.WhitelistedValidator + addStakingAmt math.Int + currentDelShares []math.Int + expectedOutputs []math.Int + expectedCrumb math.Int + }{ + { + whitelistedVals: []types.WhitelistedValidator{ + { + ValidatorAddress: liquidValidators[0].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + { + ValidatorAddress: liquidValidators[1].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + { + ValidatorAddress: liquidValidators[2].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + }, + addStakingAmt: math.NewInt(10 * 1000000), + currentDelShares: []math.Int{math.NewInt(2000000), math.NewInt(2000000), math.NewInt(1000000)}, + expectedOutputs: []math.Int{math.NewInt(3333333), math.NewInt(3333333), math.NewInt(3333333)}, + expectedCrumb: math.NewInt(1), + }, + { + whitelistedVals: []types.WhitelistedValidator{ + { + ValidatorAddress: liquidValidators[0].OperatorAddress, + TargetWeight: math.NewInt(2), + }, + { + ValidatorAddress: liquidValidators[1].OperatorAddress, + TargetWeight: math.NewInt(2), + }, + { + ValidatorAddress: liquidValidators[2].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + }, + addStakingAmt: math.NewInt(10 * 1000000), + currentDelShares: []math.Int{math.NewInt(1000000), math.NewInt(1000000), math.NewInt(1000000)}, + expectedOutputs: []math.Int{math.NewInt(4000000), math.NewInt(4000000), math.NewInt(2000000)}, + expectedCrumb: math.NewInt(0), + }, + { + whitelistedVals: []types.WhitelistedValidator{ + { + ValidatorAddress: liquidValidators[0].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + { + ValidatorAddress: liquidValidators[1].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + { + ValidatorAddress: liquidValidators[2].OperatorAddress, + TargetWeight: math.NewInt(1), + }, + }, + addStakingAmt: math.NewInt(10), + currentDelShares: []math.Int{math.NewInt(3), math.NewInt(2), math.NewInt(1)}, + expectedOutputs: []math.Int{math.NewInt(3), math.NewInt(3), math.NewInt(3)}, + expectedCrumb: math.NewInt(1), + }, + } + + for _, tc := range testCases { + require.IsType(t, []types.WhitelistedValidator{}, tc.whitelistedVals) + require.IsType(t, math.Int{}, tc.addStakingAmt) + require.IsType(t, math.Int{}, tc.expectedCrumb) + require.IsType(t, []math.Int{}, tc.expectedOutputs) + + totalTargetAmt := sdk.ZeroInt() + valsMap := types.GetWhitelistedValsMap(tc.whitelistedVals) + var activeVals types.ActiveLiquidValidators + for _, v := range tc.whitelistedVals { + activeVals = append(activeVals, types.LiquidValidator{ + OperatorAddress: v.ValidatorAddress, + }) + } + outputs, crumb := types.DivideByWeight(activeVals, tc.addStakingAmt, valsMap) + for _, v := range outputs { + totalTargetAmt = totalTargetAmt.Add(v) + } + require.EqualValues(t, tc.expectedOutputs, outputs) + require.EqualValues(t, tc.addStakingAmt, totalTargetAmt.Add(crumb)) + require.Equal(t, tc.expectedCrumb.String(), crumb.String()) + } +} + +func TestMinMaxGap(t *testing.T) { + testCases := []struct { + name string + liquidVals types.LiquidValidators + targetMap map[string]math.Int + liquidTokenMap map[string]math.Int + expectedMinGapVal types.LiquidValidator + expectedMaxGapVal types.LiquidValidator + expectedAmountNeeded math.Int + expectedLastRedelegation bool + }{ + { + name: "zero case", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: sdk.ZeroInt(), + liquidValidators[1].OperatorAddress: sdk.ZeroInt(), + liquidValidators[2].OperatorAddress: sdk.ZeroInt(), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: sdk.ZeroInt(), + liquidValidators[1].OperatorAddress: sdk.ZeroInt(), + liquidValidators[2].OperatorAddress: sdk.ZeroInt(), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + expectedMinGapVal: types.LiquidValidator{}, + expectedMaxGapVal: types.LiquidValidator{}, + expectedAmountNeeded: sdk.ZeroInt(), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 1-1", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + expectedMinGapVal: liquidValidators[3], + expectedMaxGapVal: liquidValidators[0], + expectedAmountNeeded: math.NewInt(33333334), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 1-2", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334 - 33333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: math.NewInt(0 + 33333334), + }, + expectedMinGapVal: liquidValidators[3], + expectedMaxGapVal: liquidValidators[1], + expectedAmountNeeded: math.NewInt(33333333), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 1-3", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334 - 33333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333 - 33333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: math.NewInt(33333334 + 33333333), + }, + expectedMinGapVal: liquidValidators[3], + expectedMaxGapVal: liquidValidators[2], + expectedAmountNeeded: math.NewInt(33333333), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 1-4", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334 - 33333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333 - 33333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333 - 33333333), + liquidValidators[3].OperatorAddress: math.NewInt(33333334 + 33333333 + 33333333), + }, + expectedMinGapVal: types.LiquidValidator{}, + expectedMaxGapVal: types.LiquidValidator{}, + expectedAmountNeeded: sdk.ZeroInt(), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 2-1", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000), + }, + expectedMinGapVal: liquidValidators[0], + expectedMaxGapVal: liquidValidators[3], + expectedAmountNeeded: math.NewInt(33333334), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 2-2", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000 + 33333334), + liquidValidators[1].OperatorAddress: math.NewInt(100000000), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000 - 33333334), + }, + expectedMinGapVal: liquidValidators[1], + expectedMaxGapVal: liquidValidators[3], + expectedAmountNeeded: math.NewInt(33333333), + expectedLastRedelegation: false, + }, + { + name: "rebalancing case 2-3, last redelegation", + liquidVals: liquidValidators, + targetMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(133333334), + liquidValidators[1].OperatorAddress: math.NewInt(133333333), + liquidValidators[2].OperatorAddress: math.NewInt(133333333), + liquidValidators[3].OperatorAddress: sdk.ZeroInt(), + }, + liquidTokenMap: map[string]math.Int{ + liquidValidators[0].OperatorAddress: math.NewInt(100000000 + 33333334), + liquidValidators[1].OperatorAddress: math.NewInt(100000000 + 33333333), + liquidValidators[2].OperatorAddress: math.NewInt(100000000), + liquidValidators[3].OperatorAddress: math.NewInt(100000000 - 33333334 - 33333333), + }, + expectedMinGapVal: liquidValidators[2], + expectedMaxGapVal: liquidValidators[3], + expectedAmountNeeded: math.NewInt(33333333), + expectedLastRedelegation: true, + }, + } + + for _, tc := range testCases { + minGapVal, maxGapVal, amountNeeded, last := tc.liquidVals.MinMaxGap(tc.targetMap, tc.liquidTokenMap) + require.EqualValues(t, minGapVal, tc.expectedMinGapVal) + require.EqualValues(t, maxGapVal, tc.expectedMaxGapVal) + require.EqualValues(t, amountNeeded, tc.expectedAmountNeeded) + require.EqualValues(t, last, tc.expectedLastRedelegation) + } +} + +func TestDivideByCurrentWeight(t *testing.T) { + testCases := []struct { + liquidValidators []types.LiquidValidatorState + addStakingAmt math.LegacyDec + expectedOutputs []math.LegacyDec + expectedCrumb math.LegacyDec + }{ + { + liquidValidators: []types.LiquidValidatorState{ + { + OperatorAddress: "a", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(2 * 1000000), + }, + { + OperatorAddress: "b", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(2 * 1000000), + }, + { + OperatorAddress: "c", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(1 * 1000000), + }, + }, + addStakingAmt: math.LegacyNewDec(10 * 1000000), + expectedOutputs: []math.LegacyDec{math.LegacyNewDec(4 * 1000000), math.LegacyNewDec(4 * 1000000), math.LegacyNewDec(2 * 1000000)}, + expectedCrumb: math.LegacyNewDec(0), + }, + { + liquidValidators: []types.LiquidValidatorState{ + { + OperatorAddress: "a", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(1 * 1000000), + Weight: math.NewInt(2), + }, + { + OperatorAddress: "b", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(1 * 1000000), + Weight: math.NewInt(2), + }, + { + OperatorAddress: "c", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(1 * 1000000), + Weight: math.NewInt(1), + }, + }, + addStakingAmt: math.LegacyNewDec(10 * 1000000), + expectedOutputs: []math.LegacyDec{math.LegacyMustNewDecFromStr("3333333.000000000000000000"), math.LegacyMustNewDecFromStr("3333333.000000000000000000"), math.LegacyMustNewDecFromStr("3333333.000000000000000000")}, + expectedCrumb: math.LegacyMustNewDecFromStr("1.000000000000000000"), + }, + { + liquidValidators: []types.LiquidValidatorState{ + { + OperatorAddress: "a", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(3), + }, + { + OperatorAddress: "b", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(2), + }, + { + OperatorAddress: "c", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(1), + }, + }, + addStakingAmt: math.LegacyNewDec(10), + expectedOutputs: []math.LegacyDec{math.LegacyMustNewDecFromStr("4.000000000000000000"), math.LegacyMustNewDecFromStr("3.000000000000000000"), math.LegacyMustNewDecFromStr("1.000000000000000000")}, + expectedCrumb: math.LegacyMustNewDecFromStr("2.000000000000000000"), + }, + { + liquidValidators: []types.LiquidValidatorState{ + { + OperatorAddress: "a", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(10000000), + }, + { + OperatorAddress: "b", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(2000000), + }, + { + OperatorAddress: "c", + Status: types.ValidatorStatusActive, + LiquidTokens: math.NewIntFromUint64(3000001), + }, + }, + addStakingAmt: math.LegacyNewDec(10000000), + expectedOutputs: []math.LegacyDec{math.LegacyMustNewDecFromStr("6666666.000000000000000000"), math.LegacyMustNewDecFromStr("1333333.000000000000000000"), math.LegacyMustNewDecFromStr("2000000.000000000000000000")}, + expectedCrumb: math.LegacyMustNewDecFromStr("1.000000000000000000"), + }, + } + + for _, tc := range testCases { + require.IsType(t, []types.LiquidValidatorState{}, tc.liquidValidators) + require.IsType(t, math.LegacyDec{}, tc.addStakingAmt) + require.IsType(t, math.LegacyDec{}, tc.expectedCrumb) + require.IsType(t, []math.LegacyDec{}, tc.expectedOutputs) + + totalTargetAmt := sdk.ZeroDec() + totalLiquidTokens := sdk.ZeroInt() + liquidTokenMap := map[string]math.Int{} + var lvs types.LiquidValidators + for _, v := range tc.liquidValidators { + totalLiquidTokens = totalLiquidTokens.Add(v.LiquidTokens) + liquidTokenMap[v.OperatorAddress] = v.LiquidTokens + lvs = append(lvs, types.LiquidValidator{ + OperatorAddress: v.OperatorAddress}) + } + outputs, crumb := types.DivideByCurrentWeight(lvs, tc.addStakingAmt, totalLiquidTokens, liquidTokenMap) + for _, v := range outputs { + totalTargetAmt = totalTargetAmt.Add(v) + } + require.EqualValues(t, tc.expectedOutputs, outputs) + require.EqualValues(t, tc.addStakingAmt, totalTargetAmt.Add(crumb)) + require.Equal(t, tc.expectedCrumb.String(), crumb.String()) + } +} diff --git a/x/liquidstakeibc/keeper/host_chain_test.go b/x/liquidstakeibc/keeper/host_chain_test.go index 1da88d8d4..017835856 100644 --- a/x/liquidstakeibc/keeper/host_chain_test.go +++ b/x/liquidstakeibc/keeper/host_chain_test.go @@ -933,7 +933,7 @@ func (suite *IntegrationTestSuite) TestProcessHostChainValidatorUpdates() { if t.err == nil { suite.Require().Equal(len(t.validators), len(t.hc.Validators)) - for i, _ := range t.validators { + for i := range t.validators { // get back the HC validator using the shuffled order validator, _ := t.hc.GetValidator(t.validators[i].OperatorAddress) diff --git a/x/liquidstakeibc/keeper/icq.go b/x/liquidstakeibc/keeper/icq.go index 7375c01bd..2e6281f0f 100644 --- a/x/liquidstakeibc/keeper/icq.go +++ b/x/liquidstakeibc/keeper/icq.go @@ -229,6 +229,5 @@ func NonCompoundableRewardsAccountBalanceCallback(k Keeper, ctx sdk.Context, dat } } - return nil }