diff --git a/x/protocolpool/keeper/keeper.go b/x/protocolpool/keeper/keeper.go index f9dd892674..a74bdc6253 100644 --- a/x/protocolpool/keeper/keeper.go +++ b/x/protocolpool/keeper/keeper.go @@ -270,6 +270,17 @@ func (k Keeper) iterateAndUpdateFundsDistribution(ctx context.Context, toDistrib if err != nil { return true, err } + + cf, err := k.ContinuousFund.Get(ctx, key) + if err != nil { + return true, err + } + + // Check if the continuous fund has expired + if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) { + return false, nil + } + totalPercentageToBeDistributed = totalPercentageToBeDistributed.Add(value) recipientFundList = append(recipientFundList, recipientFund{ RecipientAddr: addr, diff --git a/x/protocolpool/keeper/keeper_test.go b/x/protocolpool/keeper/keeper_test.go index 6df649c7b7..fccb8722b3 100644 --- a/x/protocolpool/keeper/keeper_test.go +++ b/x/protocolpool/keeper/keeper_test.go @@ -95,11 +95,11 @@ func (s *KeeperTestSuite) mockSendCoinsFromModuleToAccount(accAddr sdk.AccAddres } func (s *KeeperTestSuite) mockWithdrawContinuousFund() { - s.authKeeper.EXPECT().GetModuleAccount(s.ctx, types.ModuleName).Return(poolAcc).AnyTimes() + s.authKeeper.EXPECT().GetModuleAccount(gomock.Any(), types.ModuleName).Return(poolAcc).AnyTimes() distrBal := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000))) - s.bankKeeper.EXPECT().GetAllBalances(s.ctx, gomock.Any()).Return(distrBal).AnyTimes() - s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(s.ctx, gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() - s.stakingKeeper.EXPECT().BondDenom(s.ctx).AnyTimes() + s.bankKeeper.EXPECT().GetAllBalances(gomock.Any(), gomock.Any()).Return(distrBal).AnyTimes() + s.bankKeeper.EXPECT().SendCoinsFromModuleToAccount(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes() + s.stakingKeeper.EXPECT().BondDenom(gomock.Any()).Return("stake", nil).AnyTimes() } func (s *KeeperTestSuite) mockStreamFunds() { diff --git a/x/protocolpool/keeper/msg_server_test.go b/x/protocolpool/keeper/msg_server_test.go index 9b17b3cda2..683fd21f59 100644 --- a/x/protocolpool/keeper/msg_server_test.go +++ b/x/protocolpool/keeper/msg_server_test.go @@ -968,3 +968,62 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() { }) } } + +// TestWithdrawExpiredFunds checks that a continuous fund cannot be withdrawn if it has expired. +// There was a case in which an expired continuous fund would keep getting funds allocated when +// other funds were withdrawn. These funds would then get withdrawn if CancelContinuousFund was called. +func (suite *KeeperTestSuite) TestWithdrawExpiredFunds() { + suite.SetupTest() + recipientStrAddr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(recipientAddr) + suite.Require().NoError(err) + recipient2 := sdk.AccAddress([]byte("recipientAddr2___________________")) + recipient2StrAddr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(recipient2) + suite.Require().NoError(err) + + expiration := suite.environment.HeaderService.HeaderInfo(suite.ctx).Time.Add(24 * time.Hour) + _, err = suite.msgServer.CreateContinuousFund(suite.ctx, &types.MsgCreateContinuousFund{ + Authority: suite.poolKeeper.GetAuthority(), + Recipient: recipientStrAddr, + Percentage: math.LegacyMustNewDecFromStr("0.5"), + Expiry: &expiration, + }) + suite.Require().NoError(err) + + _, err = suite.msgServer.CreateContinuousFund(suite.ctx, &types.MsgCreateContinuousFund{ + Authority: suite.poolKeeper.GetAuthority(), + Recipient: recipient2StrAddr, + Percentage: math.LegacyMustNewDecFromStr("0.5"), + }) + suite.Require().NoError(err) + + toDistribute := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000))) + suite.mockStreamFunds() + err = suite.poolKeeper.SetToDistribute(suite.ctx, toDistribute, suite.poolKeeper.GetAuthority()) + suite.Require().NoError(err) + + suite.mockWithdrawContinuousFund() + _, err = suite.msgServer.WithdrawContinuousFund(suite.ctx, &types.MsgWithdrawContinuousFund{RecipientAddress: recipientStrAddr}) + suite.Require().NoError(err) + + header := suite.ctx.HeaderInfo() + header.Time = expiration.Add(1 * time.Second) + suite.ctx = suite.ctx.WithHeaderInfo(header) + + _, err = suite.msgServer.WithdrawContinuousFund(suite.ctx, &types.MsgWithdrawContinuousFund{RecipientAddress: recipientStrAddr}) + suite.Require().ErrorContains(err, "continuous fund expired for recipient") + + suite.mockStreamFunds() + err = suite.poolKeeper.SetToDistribute(suite.ctx, toDistribute, suite.poolKeeper.GetAuthority()) + suite.Require().NoError(err) + + suite.mockWithdrawContinuousFund() + _, err = suite.msgServer.WithdrawContinuousFund(suite.ctx, &types.MsgWithdrawContinuousFund{RecipientAddress: recipient2StrAddr}) + suite.Require().NoError(err) + + res, err := suite.msgServer.CancelContinuousFund(suite.ctx, &types.MsgCancelContinuousFund{ + Authority: suite.poolKeeper.GetAuthority(), + RecipientAddress: recipientStrAddr, + }) + suite.Require().NoError(err) + suite.Require().Equal(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(0)), res.WithdrawnAllocatedFund) +}