diff --git a/app/keepers/keepers.go b/app/keepers/keepers.go index f71e3ae34..155e316e3 100644 --- a/app/keepers/keepers.go +++ b/app/keepers/keepers.go @@ -405,6 +405,7 @@ func (a *AppKeepers) InitKeepers( a.AccountKeeper, a.StakingKeeper, a.IncentivesKeeper, + a.SequencerKeeper, authtypes.NewModuleAddress(govtypes.ModuleName).String(), ) diff --git a/x/sponsorship/keeper/keeper.go b/x/sponsorship/keeper/keeper.go index 200518dfc..a71f0d5d5 100644 --- a/x/sponsorship/keeper/keeper.go +++ b/x/sponsorship/keeper/keeper.go @@ -23,6 +23,7 @@ type Keeper struct { stakingKeeper types.StakingKeeper incentivesKeeper types.IncentivesKeeper + sequencerKeeper types.SequencerKeeper } // NewKeeper returns a new instance of the x/sponsorship keeper. @@ -32,6 +33,7 @@ func NewKeeper( ak types.AccountKeeper, sk types.StakingKeeper, ik types.IncentivesKeeper, + sqk types.SequencerKeeper, authority string, ) Keeper { // ensure the module account is set @@ -53,12 +55,6 @@ func NewKeeper( "params", collcompat.ProtoValue[types.Params](cdc), ), - distribution: collections.NewItem( - sb, - types.DistributionPrefix(), - "distribution", - collcompat.ProtoValue[types.Distribution](cdc), - ), delegatorValidatorPower: collections.NewMap( sb, types.DelegatorValidatorPrefix(), @@ -69,6 +65,12 @@ func NewKeeper( ), collcompat.IntValue, ), + distribution: collections.NewItem( + sb, + types.DistributionPrefix(), + "distribution", + collcompat.ProtoValue[types.Distribution](cdc), + ), votes: collections.NewMap( sb, types.VotePrefix(), @@ -78,5 +80,6 @@ func NewKeeper( ), stakingKeeper: sk, incentivesKeeper: ik, + sequencerKeeper: sqk, } } diff --git a/x/sponsorship/keeper/votes.go b/x/sponsorship/keeper/votes.go index 80aa6a009..2d67f9968 100644 --- a/x/sponsorship/keeper/votes.go +++ b/x/sponsorship/keeper/votes.go @@ -7,15 +7,29 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" + sequencertypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" "github.com/dymensionxyz/dymension/v3/x/sponsorship/types" ) func (k Keeper) Vote(ctx sdk.Context, voter sdk.AccAddress, weights []types.GaugeWeight) (types.Vote, types.Distribution, error) { + // Get module params + params, err := k.GetParams(ctx) + if err != nil { + return types.Vote{}, types.Distribution{}, fmt.Errorf("cannot get module params: %w", err) + } + + // Validate specified weights + err = k.validateWeights(ctx, weights, params.MinAllocationWeight) + if err != nil { + return types.Vote{}, types.Distribution{}, fmt.Errorf("error validating weights: %w", err) + } + + // Check if the user's voted. If they have, revoke the previous vote to place a new one. voted, err := k.Voted(ctx, voter) if err != nil { return types.Vote{}, types.Distribution{}, fmt.Errorf("cannot verify if the voter has already voted: %w", err) } - if voted { _, err := k.RevokeVote(ctx, voter) if err != nil { @@ -23,16 +37,6 @@ func (k Keeper) Vote(ctx sdk.Context, voter sdk.AccAddress, weights []types.Gaug } } - params, err := k.GetParams(ctx) - if err != nil { - return types.Vote{}, types.Distribution{}, fmt.Errorf("cannot get module params: %w", err) - } - - err = k.validateWeights(ctx, weights, params.MinAllocationWeight) - if err != nil { - return types.Vote{}, types.Distribution{}, fmt.Errorf("error validating weights: %w", err) - } - // Get the user’s total voting power from the x/staking vpBreakdown, err := k.GetValidatorBreakdown(ctx, voter) if err != nil { @@ -107,21 +111,43 @@ func (k Keeper) revokeVote(ctx sdk.Context, voter sdk.AccAddress, vote types.Vot return d, nil } -// validateWeights validates that no gauge got less than MinAllocationWeight and all of them are perpetual +// validateWeights validates that +// - No gauge get less than MinAllocationWeight +// - All gauges exist +// - All gauges are perpetual +// - Rollapp gauges have at least one bonded sequencer func (k Keeper) validateWeights(ctx sdk.Context, weights []types.GaugeWeight, minAllocationWeight math.Int) error { for _, weight := range weights { + // No gauge get less than MinAllocationWeight if weight.Weight.LT(minAllocationWeight) { - return fmt.Errorf("gauge weight '%s' is less than min allocation weight '%s'", weight.Weight, minAllocationWeight) + return fmt.Errorf("gauge weight is less than min allocation weight: gauge weight %s, min allocation %s", weight.Weight, minAllocationWeight) } + // All gauges exist gauge, err := k.incentivesKeeper.GetGaugeByID(ctx, weight.GaugeId) if err != nil { - return fmt.Errorf("failed to get gauge by id '%d': %w", weight.GaugeId, err) + return fmt.Errorf("failed to get gauge by id: %d: %w", weight.GaugeId, err) } + // All gauges are perpetual if !gauge.IsPerpetual { - return fmt.Errorf("gauge '%d' is not perpetual", weight.GaugeId) + return fmt.Errorf("gauge is not perpetual: %d", weight.GaugeId) } + + // Rollapp gauges have at least one bonded sequencer + switch distrTo := gauge.DistributeTo.(type) { + case *incentivestypes.Gauge_Asset: + // no additional restrictions for asset gauges + case *incentivestypes.Gauge_Rollapp: + // we allow sponsoring only rollapps with bonded sequencers + bondedSequencers := k.sequencerKeeper.GetSequencersByRollappByStatus(ctx, distrTo.Rollapp.RollappId, sequencertypes.Bonded) + if len(bondedSequencers) == 0 { + return fmt.Errorf("rollapp has no bonded sequencers: %s'", distrTo.Rollapp.RollappId) + } + default: + return fmt.Errorf("gauge has an unsupported distribution type: gauge id %d, type %T", gauge.Id, distrTo) + } + } return nil } diff --git a/x/sponsorship/keeper/votes_test.go b/x/sponsorship/keeper/votes_test.go index 91103a358..0bc4ca1d5 100644 --- a/x/sponsorship/keeper/votes_test.go +++ b/x/sponsorship/keeper/votes_test.go @@ -232,7 +232,7 @@ func (s *KeeperTestSuite) TestMsgVote() { }, }, expectErr: true, - errorContains: "failed to get gauge by id '2'", + errorContains: "failed to get gauge by id: 2", }, { name: "Weight is less than the min allocation", @@ -271,7 +271,7 @@ func (s *KeeperTestSuite) TestMsgVote() { }, }, expectErr: true, - errorContains: "gauge weight '20000000000000000000' is less than min allocation weight '30000000000000000000'", + errorContains: "gauge weight is less than min allocation weight: gauge weight 20000000000000000000, min allocation 30000000000000000000", }, { name: "Not enough voting power", @@ -387,6 +387,83 @@ func (s *KeeperTestSuite) TestMsgVote() { } } +func (s *KeeperTestSuite) TestMsgVoteRollAppGaugeBondedSequencer() { + raName := "testrollapp_1-1" + + // create a rollapp, subsequently the rollapp gauge must be created + s.CreateRollappByName(raName) + // create a bonded sequencer + proposer := s.CreateDefaultSequencer(s.Ctx, raName) + + // verify the sequencer is bonded + seq, found := s.App.SequencerKeeper.GetSequencer(s.Ctx, proposer) + s.Require().True(found) + s.Require().True(seq.IsBonded()) + + // create a validator and a delegator + initial := sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1_000_000)) + val := s.CreateValidator() + del := s.CreateDelegator(val.GetOperator(), initial) + + // case a vote to the rollapp gauge created above + voteResp, err := s.msgServer.Vote(s.Ctx, &types.MsgVote{ + Voter: del.GetDelegatorAddr().String(), + Weights: []types.GaugeWeight{ + {GaugeId: 1, Weight: types.DYM.MulRaw(20)}, + }, + }) + s.Require().NoError(err) + s.Require().NotNil(voteResp) + + // check the distribution is correct + expectedDistr := types.Distribution{ + VotingPower: math.NewInt(1_000_000), + Gauges: []types.Gauge{ + {GaugeId: 1, Power: math.NewInt(200_000)}, + }, + } + distr := s.GetDistribution() + err = distr.Validate() + s.Require().NoError(err) + s.Require().True(expectedDistr.Equal(distr), "expect: %v\nactual: %v", expectedDistr, distr) +} + +func (s *KeeperTestSuite) TestMsgVoteRollAppGaugeNonBondedSequencer() { + raName := "testrollapp_1-1" + + // create a rollapp, subsequently the rollapp gauge must be created + s.CreateRollappByName(raName) + // create a bonded sequencer + proposer := s.CreateDefaultSequencer(s.Ctx, raName) + + // jail the sequencer + err := s.App.SequencerKeeper.JailSequencerOnFraud(s.Ctx, proposer) + s.Require().NoError(err) + + // create a validator and a delegator + initial := sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1_000_000)) + val := s.CreateValidator() + del := s.CreateDelegator(val.GetOperator(), initial) + + // case a vote to the rollapp gauge created above + voteResp, err := s.msgServer.Vote(s.Ctx, &types.MsgVote{ + Voter: del.GetDelegatorAddr().String(), + Weights: []types.GaugeWeight{ + {GaugeId: 1, Weight: types.DYM.MulRaw(20)}, + }, + }) + s.Require().Error(err) + s.Require().ErrorContains(err, "rollapp has no bonded sequencers") + s.Require().Nil(voteResp) + + // check the distribution is correct. the distr is empty. + expectedDistr := types.NewDistribution() + distr := s.GetDistribution() + err = distr.Validate() + s.Require().NoError(err) + s.Require().True(expectedDistr.Equal(distr), "expect: %v\nactual: %v", expectedDistr, distr) +} + func (s *KeeperTestSuite) TestMsgRevokeVote() { addr := apptesting.CreateRandomAccounts(3) diff --git a/x/sponsorship/types/expected_keepers.go b/x/sponsorship/types/expected_keepers.go index 073279f5b..c00e42346 100644 --- a/x/sponsorship/types/expected_keepers.go +++ b/x/sponsorship/types/expected_keepers.go @@ -5,6 +5,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" incentivestypes "github.com/dymensionxyz/dymension/v3/x/incentives/types" + sequencertypes "github.com/dymensionxyz/dymension/v3/x/sequencer/types" ) // AccountKeeper defines the contract required for account APIs. @@ -21,3 +22,7 @@ type StakingKeeper interface { type IncentivesKeeper interface { GetGaugeByID(ctx sdk.Context, gaugeID uint64) (*incentivestypes.Gauge, error) } + +type SequencerKeeper interface { + GetSequencersByRollappByStatus(ctx sdk.Context, rollappId string, status sequencertypes.OperatingStatus) []sequencertypes.Sequencer +}