Skip to content

Commit

Permalink
fix: do not allow to answer a poll after voting period ends (#964)
Browse files Browse the repository at this point in the history
## Description

This PR fixes a bug within the `x/posts` module that currently allows users to answer a poll even when it's already ended.

---

### Author Checklist

*All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.*

I have...

- [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] added `!` to the type prefix if API or client breaking change
- [x] targeted the correct branch (see [PR Targeting](https://github.com/desmos-labs/desmos/blob/master/CONTRIBUTING.md#pr-targeting))
- [ ] provided a link to the relevant issue or specification
- [x] followed the guidelines for [building modules](https://docs.cosmos.network/v0.44/building-modules/intro.html)
- [x] included the necessary unit and integration [tests](https://github.com/desmos-labs/desmos/blob/master/CONTRIBUTING.md#testing)
- [x] added a changelog entry to `CHANGELOG.md`
- [x] included comments for [documenting Go code](https://blog.golang.org/godoc)
- [x] updated the relevant documentation or specification
- [x] reviewed "Files changed" and left comments if necessary
- [x] confirmed all CI checks have passed

### Reviewers Checklist

*All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.*

I have...

- [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title
- [ ] confirmed `!` in the type prefix if API or client breaking change
- [ ] confirmed all author checklist items have been addressed
- [ ] reviewed state machine logic
- [ ] reviewed API design and naming
- [ ] reviewed documentation is accurate
- [ ] reviewed tests and test coverage
- [ ] manually tested (if applicable)
  • Loading branch information
RiccardoM authored Jul 14, 2022
1 parent 128cbdc commit bc97e72
Show file tree
Hide file tree
Showing 9 changed files with 348 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: fix
module: x/posts
pull_request: 964
description: Do not allow to answer a poll after voting period ends
backward_compatible: false
date: 2022-07-08T05:38:47.837745373Z
7 changes: 7 additions & 0 deletions x/posts/keeper/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"

v4 "github.com/desmos-labs/desmos/v4/x/posts/legacy/v4"

v3 "github.com/desmos-labs/desmos/v4/x/posts/legacy/v3"

v2 "github.com/desmos-labs/desmos/v4/x/posts/legacy/v2"
Expand Down Expand Up @@ -34,3 +36,8 @@ func (m Migrator) Migrate1to2(ctx sdk.Context) error {
func (m Migrator) Migrate2to3(ctx sdk.Context) error {
return v3.MigrateStore(ctx, m.k.storeKey, m.k.cdc)
}

// Migrate3to4 migrates from version 3 to 4.
func (m Migrator) Migrate3to4(ctx sdk.Context) error {
return v4.MigrateStore(ctx, m.k.storeKey, m.k.cdc)
}
5 changes: 5 additions & 0 deletions x/posts/keeper/msg_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,11 @@ func (k msgServer) AnswerPoll(goCtx context.Context, msg *types.MsgAnswerPoll) (
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "poll with id %d does not exist", msg.PollID)
}

// Make sure the poll is still active
if ctx.BlockTime().After(poll.EndDate) {
return nil, sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "the poll voting period has already ended")
}

alreadyAnswered := k.HasUserAnswer(ctx, msg.SubspaceID, msg.PostID, msg.PollID, msg.Signer)

// Make sure the user is not trying to edit the answer when the poll does not allow it
Expand Down
91 changes: 91 additions & 0 deletions x/posts/keeper/msg_server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1797,8 +1797,87 @@ func (suite *KeeperTestsuite) TestMsgServer_AnswerPoll() {
),
shouldErr: true,
},
{
name: "voting after end time returns error",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2100, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)

suite.sk.SaveSubspace(ctx, subspacestypes.NewSubspace(
1,
"Test",
"Testing subspace",
"cosmos1sg2j68v5n8qvehew6ml0etun3lmv7zg7r49s67",
"cosmos1sg2j68v5n8qvehew6ml0etun3lmv7zg7r49s67",
"cosmos1sg2j68v5n8qvehew6ml0etun3lmv7zg7r49s67",
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
))

suite.sk.SetUserPermissions(ctx,
1,
0,
"cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd",
subspacestypes.NewPermissions(types.PermissionInteractWithContent),
)

suite.k.SavePost(ctx, types.NewPost(
1,
0,
1,
"External ID",
"This is a text",
"cosmos1r9jamre0x0qqy562rhhckt6sryztwhnvhafyz4",
0,
nil,
nil,
nil,
types.REPLY_SETTING_EVERYONE,
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
nil,
))

suite.k.SaveAttachment(ctx, types.NewAttachment(
1,
1,
1,
types.NewPoll(
"What animal is best?",
[]types.Poll_ProvidedAnswer{
types.NewProvidedAnswer("Cat", nil),
types.NewProvidedAnswer("Dog", nil),
},
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
false,
false,
nil,
),
))

suite.k.SaveUserAnswer(ctx, types.NewUserAnswer(
1,
1,
1,
[]uint32{0, 1},
"cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd",
))
},
msg: types.NewMsgAnswerPoll(
1,
1,
1,
[]uint32{1},
"cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd",
),
shouldErr: true,
},
{
name: "already answered poll returns error if no answer edits are allowed",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2010, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)
Expand Down Expand Up @@ -1872,6 +1951,9 @@ func (suite *KeeperTestsuite) TestMsgServer_AnswerPoll() {
},
{
name: "multiple answers return error if they are not allowed",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2010, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)
Expand Down Expand Up @@ -1937,6 +2019,9 @@ func (suite *KeeperTestsuite) TestMsgServer_AnswerPoll() {
},
{
name: "invalid answer indexes return error",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2010, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)
Expand Down Expand Up @@ -2002,6 +2087,9 @@ func (suite *KeeperTestsuite) TestMsgServer_AnswerPoll() {
},
{
name: "editing an answer works correctly",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2010, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)
Expand Down Expand Up @@ -2101,6 +2189,9 @@ func (suite *KeeperTestsuite) TestMsgServer_AnswerPoll() {
},
{
name: "new answer is stored correctly",
setupCtx: func(ctx sdk.Context) sdk.Context {
return ctx.WithBlockTime(time.Date(2010, 1, 1, 00, 00, 00, 000, time.UTC))
},
store: func(ctx sdk.Context) {
err := suite.ak.SaveProfile(ctx, profilestesting.ProfileFromAddr("cosmos13t6y2nnugtshwuy0zkrq287a95lyy8vzleaxmd"))
suite.Require().NoError(err)
Expand Down
71 changes: 71 additions & 0 deletions x/posts/legacy/v4/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package v4

import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/desmos-labs/desmos/v4/x/posts/types"
)

// MigrateStore performs the migration from version 3 to version 4 of the store.
// To do this, it iterates over all the polls, and removes from the store the user answers for
// polls that are already ended (and thus should have not accepted new answers).
func MigrateStore(ctx sdk.Context, storeKey sdk.StoreKey, cdc codec.BinaryCodec) error {
store := ctx.KVStore(storeKey)
return removeInvalidPollAnswers(store, cdc)
}

// removeInvalidPollAnswers iterates over all the polls and deletes the user answers for all the polls which
// final results already been tallied. This is to delete all the answers that have been added after that.
func removeInvalidPollAnswers(store sdk.KVStore, cdc codec.BinaryCodec) error {
attachmentStore := prefix.NewStore(store, types.AttachmentPrefix)
attachmentsIterator := attachmentStore.Iterator(nil, nil)
defer attachmentsIterator.Close()

for ; attachmentsIterator.Valid(); attachmentsIterator.Next() {
// Get the attachment
var attachment types.Attachment
err := cdc.Unmarshal(attachmentsIterator.Value(), &attachment)
if err != nil {
return err
}

// Check if the attachment represents a poll and the final results have already been tallied
if poll, ok := attachment.Content.GetCachedValue().(*types.Poll); ok && poll.FinalTallyResults != nil {
// Remove all the answers that might still be there
err = removePollAnswers(store, attachment.SubspaceID, attachment.PostID, attachment.ID)
if err != nil {
return err
}
}
}

return nil
}

// removePollAnswers removes from the store the answers related to the given poll
func removePollAnswers(store sdk.KVStore, subspaceID uint64, postID uint64, pollID uint32) error {
answersStore := prefix.NewStore(store, types.PollAnswersPrefix(subspaceID, postID, pollID))
answersIterator := answersStore.Iterator(nil, nil)

// Get the answers
var keys [][]byte
for ; answersIterator.Valid(); answersIterator.Next() {
user := string(answersIterator.Key())
keys = append(keys, types.PollAnswerStoreKey(subspaceID, postID, pollID, user))
}

// Close the iterator to avoid any conflict
err := answersIterator.Close()
if err != nil {
return err
}

// Delete the various answers
for _, key := range keys {
store.Delete(key)
}

return nil
}
157 changes: 157 additions & 0 deletions x/posts/legacy/v4/store_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package v4_test

import (
"testing"
"time"

v4 "github.com/desmos-labs/desmos/v4/x/posts/legacy/v4"

sdk "github.com/cosmos/cosmos-sdk/types"
capabilitytypes "github.com/cosmos/cosmos-sdk/x/capability/types"
paramstypes "github.com/cosmos/cosmos-sdk/x/params/types"
"github.com/stretchr/testify/require"

"github.com/desmos-labs/desmos/v4/app"
"github.com/desmos-labs/desmos/v4/testutil/storetesting"
"github.com/desmos-labs/desmos/v4/x/posts/types"
subspacestypes "github.com/desmos-labs/desmos/v4/x/subspaces/types"
)

func TestMigrateStore(t *testing.T) {
cdc, _ := app.MakeCodecs()

// Build all the necessary keys
keys := sdk.NewKVStoreKeys(paramstypes.StoreKey, subspacestypes.StoreKey, types.StoreKey)
tKeys := sdk.NewTransientStoreKeys(paramstypes.TStoreKey)
memKeys := sdk.NewMemoryStoreKeys(capabilitytypes.MemStoreKey)

testCases := []struct {
name string
store func(ctx sdk.Context)
shouldErr bool
check func(ctx sdk.Context)
}{
{
name: "user answers are deleted properly for tallied poll",
store: func(ctx sdk.Context) {
poll := types.NewAttachment(1, 1, 1, types.NewPoll(
"What animal is best?",
[]types.Poll_ProvidedAnswer{
types.NewProvidedAnswer("Cat", nil),
types.NewProvidedAnswer("Dog", nil),
},
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
false,
false,
types.NewPollTallyResults([]types.PollTallyResults_AnswerResult{
types.NewAnswerResult(0, 1),
}),
))

userAnswer := types.NewUserAnswer(
1,
1,
1,
[]uint32{1},
"cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw",
)

store := ctx.KVStore(keys[types.StoreKey])
store.Set(types.AttachmentStoreKey(1, 1, 1), cdc.MustMarshal(&poll))
store.Set(types.PollAnswerStoreKey(1, 1, 1, "cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw"), cdc.MustMarshal(&userAnswer))
},
shouldErr: false,
check: func(ctx sdk.Context) {
store := ctx.KVStore(keys[types.StoreKey])

var stored types.Attachment
err := cdc.Unmarshal(store.Get(types.AttachmentStoreKey(1, 1, 1)), &stored)
require.NoError(t, err)
require.Equal(t, types.NewAttachment(1, 1, 1, types.NewPoll(
"What animal is best?",
[]types.Poll_ProvidedAnswer{
types.NewProvidedAnswer("Cat", nil),
types.NewProvidedAnswer("Dog", nil),
},
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
false,
false,
types.NewPollTallyResults([]types.PollTallyResults_AnswerResult{
types.NewAnswerResult(0, 1),
}),
)), stored)

require.False(t, store.Has(types.PollAnswerStoreKey(1, 1, 1, "cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw")))
},
},
{
name: "user answers are not deleted for not tallied poll",
store: func(ctx sdk.Context) {
poll := types.NewAttachment(1, 1, 1, types.NewPoll(
"What animal is best?",
[]types.Poll_ProvidedAnswer{
types.NewProvidedAnswer("Cat", nil),
types.NewProvidedAnswer("Dog", nil),
},
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
false,
false,
nil,
))

userAnswer := types.NewUserAnswer(
1,
1,
1,
[]uint32{1},
"cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw",
)

store := ctx.KVStore(keys[types.StoreKey])
store.Set(types.AttachmentStoreKey(1, 1, 1), cdc.MustMarshal(&poll))
store.Set(types.PollAnswerStoreKey(1, 1, 1, "cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw"), cdc.MustMarshal(&userAnswer))
},
shouldErr: false,
check: func(ctx sdk.Context) {
store := ctx.KVStore(keys[types.StoreKey])

var stored types.Attachment
err := cdc.Unmarshal(store.Get(types.AttachmentStoreKey(1, 1, 1)), &stored)
require.NoError(t, err)
require.Equal(t, types.NewAttachment(1, 1, 1, types.NewPoll(
"What animal is best?",
[]types.Poll_ProvidedAnswer{
types.NewProvidedAnswer("Cat", nil),
types.NewProvidedAnswer("Dog", nil),
},
time.Date(2020, 1, 1, 12, 00, 00, 000, time.UTC),
false,
false,
nil,
)), stored)

require.True(t, store.Has(types.PollAnswerStoreKey(1, 1, 1, "cosmos1jseuux3pktht0kkhlcsv4kqff3mql65udqs4jw")))
},
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
ctx := storetesting.BuildContext(keys, tKeys, memKeys)
if tc.store != nil {
tc.store(ctx)
}

err := v4.MigrateStore(ctx, keys[types.StoreKey], cdc)
if tc.shouldErr {
require.Error(t, err)
} else {
require.NoError(t, err)
if tc.check != nil {
tc.check(ctx)
}
}
})
}
}
Loading

0 comments on commit bc97e72

Please sign in to comment.