diff --git a/x/stakers/keeper/exported_functions.go b/x/stakers/keeper/exported_functions.go index 4f216fbd..84b428e6 100644 --- a/x/stakers/keeper/exported_functions.go +++ b/x/stakers/keeper/exported_functions.go @@ -91,6 +91,16 @@ func (k Keeper) GetDelegationOfPool(ctx sdk.Context, poolId uint64) uint64 { return totalDelegation } +// GetPoolTotalStake returns the amount in uykve which actively secures +// the given pool +func (k Keeper) GetPoolTotalStake(ctx sdk.Context, poolId uint64) (totalStake uint64) { + effectiveStakes := k.GetEffectiveValidatorStakes(ctx, poolId) + for _, stake := range effectiveStakes { + totalStake += stake + } + return +} + // GetTotalAndHighestDelegationOfPool iterates all validators of a given pool and returns the stake of the validator // with the highest stake and the sum of all stakes. func (k Keeper) GetTotalAndHighestDelegationOfPool(ctx sdk.Context, poolId uint64) (totalDelegation, highestDelegation uint64) { @@ -230,11 +240,15 @@ func (k Keeper) GetEffectiveValidatorStakes(ctx sdk.Context, poolId uint64, must }) if totalStake == 0 || maxVotingPower.IsZero() { - return + return make(map[string]uint64) + } + + if math.LegacyOneDec().Quo(maxVotingPower).GT(math.LegacyNewDec(int64(len(addresses)))) { + return make(map[string]uint64) } for i, validator := range validators { - if math.LegacyNewDec(int64(effectiveStakes[validator.Address])).QuoInt64(totalStake).GT(maxVotingPower) { + if math.LegacyNewDec(int64(effectiveStakes[validator.Address])).GT(maxVotingPower.MulInt64(totalStake)) { amount := math.LegacyNewDec(int64(effectiveStakes[validator.Address])).Sub(maxVotingPower.MulInt64(totalStake)).TruncateInt64() totalStakeRemainder -= int64(validator.Stake) @@ -242,7 +256,7 @@ func (k Keeper) GetEffectiveValidatorStakes(ctx sdk.Context, poolId uint64, must if totalStakeRemainder > 0 { for _, v := range validators[i+1:] { - effectiveStakes[v.Address] += uint64(math.LegacyNewDec(int64(effectiveStakes[v.Address])).QuoInt64(totalStakeRemainder).MulInt64(amount).TruncateInt64()) + effectiveStakes[v.Address] += uint64(math.LegacyNewDec(int64(v.Stake)).QuoInt64(totalStakeRemainder).MulInt64(amount).TruncateInt64()) } } } @@ -254,12 +268,14 @@ func (k Keeper) GetEffectiveValidatorStakes(ctx sdk.Context, poolId uint64, must for i := len(validators) - 1; i >= 0; i-- { if effectiveStakes[validators[i].Address] > 0 { scaleFactor = math.LegacyNewDec(int64(validators[i].Stake)).QuoInt64(int64(effectiveStakes[validators[i].Address])) + break } } // scale all effective stakes down to scale factor for _, validator := range validators { - effectiveStakes[validator.Address] = uint64(scaleFactor.MulInt64(int64(effectiveStakes[validator.Address])).TruncateInt64()) + // TODO: is rounding here fine? + effectiveStakes[validator.Address] = uint64(scaleFactor.MulInt64(int64(effectiveStakes[validator.Address])).RoundInt64()) } return diff --git a/x/stakers/keeper/keeper_suite_effective_stake_test.go b/x/stakers/keeper/keeper_suite_effective_stake_test.go new file mode 100644 index 00000000..0314ead5 --- /dev/null +++ b/x/stakers/keeper/keeper_suite_effective_stake_test.go @@ -0,0 +1,318 @@ +package keeper_test + +import ( + "cosmossdk.io/math" + stakerstypes "github.com/KYVENetwork/chain/x/stakers/types" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + i "github.com/KYVENetwork/chain/testutil/integration" + pooltypes "github.com/KYVENetwork/chain/x/pool/types" +) + +/* + +TEST CASES - keeper_suite_effective_stake_test.go + +* Test effective stake with all validators below the max pool voting power +* Test effective stake with one validator above the max pool voting power +* Test effective stake with multiple validators above the max pool voting power +* Test effective stake with fewer validators than required to undercut the max pool voting power +* Test effective stake with some validators having zero delegation +* Test effective stake with all validators having zero delegation + +*/ + +var _ = Describe("keeper_suite_effective_stake_test.go", Ordered, func() { + s := i.NewCleanChain() + + gov := s.App().GovKeeper.GetGovernanceAccount(s.Ctx()).GetAddress().String() + + BeforeEach(func() { + // init new clean chain + s = i.NewCleanChain() + + // create pool + msg := &pooltypes.MsgCreatePool{ + Authority: gov, + UploadInterval: 60, + MaxBundleSize: 100, + InflationShareWeight: math.LegacyZeroDec(), + Binaries: "{}", + } + s.RunTxPoolSuccess(msg) + + params := s.App().PoolKeeper.GetParams(s.Ctx()) + params.MaxVotingPowerPerPool = math.LegacyMustNewDecFromStr("0.5") + s.App().PoolKeeper.SetParams(s.Ctx(), params) + }) + + AfterEach(func() { + s.PerformValidityChecks() + }) + + It("Test effective stake with all validators below the max pool voting power", func() { + // ARRANGE + s.CreateValidator(i.STAKER_0, "Staker-0", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_1, "Staker-1", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_2, "Staker-2", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(Equal(3)) + Expect(effectiveStakes[i.STAKER_0]).To(Equal(100 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_1]).To(Equal(100 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_2]).To(Equal(100 * i.KYVE)) + + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(Equal(300 * i.KYVE)) + }) + + It("Test effective stake with one validator above the max pool voting power", func() { + // ARRANGE + s.CreateValidator(i.STAKER_0, "Staker-0", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_1, "Staker-1", int64(250*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_2, "Staker-2", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(Equal(3)) + Expect(effectiveStakes[i.STAKER_0]).To(Equal(100 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_1]).To(Equal(200 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_2]).To(Equal(100 * i.KYVE)) + + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(Equal(400 * i.KYVE)) + }) + + It("Test effective stake with multiple validators above the max pool voting power", func() { + // ARRANGE + params := s.App().PoolKeeper.GetParams(s.Ctx()) + params.MaxVotingPowerPerPool = math.LegacyMustNewDecFromStr("0.35") + s.App().PoolKeeper.SetParams(s.Ctx(), params) + + s.CreateValidator(i.STAKER_0, "Staker-0", int64(600*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_1, "Staker-1", int64(500*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_2, "Staker-2", int64(120*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(Equal(3)) + Expect(effectiveStakes[i.STAKER_0]).To(Equal(140 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_1]).To(Equal(140 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_2]).To(Equal(120 * i.KYVE)) + + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(Equal(400 * i.KYVE)) + }) + + It("Test effective stake with fewer validators than required to undercut the max pool voting power", func() { + // ARRANGE + params := s.App().PoolKeeper.GetParams(s.Ctx()) + params.MaxVotingPowerPerPool = math.LegacyMustNewDecFromStr("0.2") + s.App().PoolKeeper.SetParams(s.Ctx(), params) + + s.CreateValidator(i.STAKER_0, "Staker-0", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_1, "Staker-1", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_2, "Staker-2", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(BeZero()) + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(BeZero()) + }) + + It("Test effective stake with some validators having zero delegation", func() { + // ARRANGE + s.CreateValidator(i.STAKER_0, "Staker-0", int64(200*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateZeroDelegationValidator(i.STAKER_1, "Staker-1") + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateValidator(i.STAKER_2, "Staker-2", int64(100*i.KYVE)) + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(Equal(3)) + Expect(effectiveStakes[i.STAKER_0]).To(Equal(100 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_1]).To(Equal(0 * i.KYVE)) + Expect(effectiveStakes[i.STAKER_2]).To(Equal(100 * i.KYVE)) + + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(Equal(200 * i.KYVE)) + }) + + It("Test effective stake with all validators having zero delegation", func() { + // ARRANGE + s.CreateZeroDelegationValidator(i.STAKER_0, "Staker-0") + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_0, + PoolId: 0, + Valaddress: i.VALADDRESS_0_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateZeroDelegationValidator(i.STAKER_1, "Staker-1") + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_1, + PoolId: 0, + Valaddress: i.VALADDRESS_1_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + s.CreateZeroDelegationValidator(i.STAKER_2, "Staker-2") + s.RunTxStakersSuccess(&stakerstypes.MsgJoinPool{ + Creator: i.STAKER_2, + PoolId: 0, + Valaddress: i.VALADDRESS_2_A, + Amount: 100 * i.KYVE, + Commission: math.LegacyMustNewDecFromStr("0.1"), + StakeFraction: math.LegacyMustNewDecFromStr("1"), + }) + + // ACT + effectiveStakes := s.App().StakersKeeper.GetEffectiveValidatorStakes(s.Ctx(), 0) + + // ASSERT + Expect(len(effectiveStakes)).To(BeZero()) + Expect(s.App().StakersKeeper.GetPoolTotalStake(s.Ctx(), 0)).To(BeZero()) + }) +})