From d426a5db677506a981cb094249bb6eaaf6324d39 Mon Sep 17 00:00:00 2001 From: Facundo Medica <14063057+facundomedica@users.noreply.github.com> Date: Fri, 28 Jun 2024 17:53:26 +0200 Subject: [PATCH] refactor(x/protocolpool)!: Reducing complexity and removing some bugs (#20786) --- tests/integration/protocolpool/app_config.go | 29 +++ tests/integration/protocolpool/module_test.go | 138 +++++++++++ x/protocolpool/keeper/genesis_test.go | 42 ++++ x/protocolpool/keeper/keeper.go | 176 ++++++-------- x/protocolpool/keeper/keeper_test.go | 42 ++++ x/protocolpool/keeper/msg_server.go | 27 +- x/protocolpool/keeper/msg_server_test.go | 40 +-- x/protocolpool/module.go | 11 +- .../testutil/expected_keepers_mocks.go | 14 -- x/protocolpool/types/errors.go | 2 +- x/protocolpool/types/expected_keepers.go | 1 - x/protocolpool/types/genesis.go | 10 +- x/protocolpool/types/msg.go | 4 +- x/protocolpool/types/types_test.go | 230 ++++++++++++++++++ 14 files changed, 570 insertions(+), 196 deletions(-) create mode 100644 tests/integration/protocolpool/app_config.go create mode 100644 tests/integration/protocolpool/module_test.go create mode 100644 x/protocolpool/keeper/genesis_test.go create mode 100644 x/protocolpool/types/types_test.go diff --git a/tests/integration/protocolpool/app_config.go b/tests/integration/protocolpool/app_config.go new file mode 100644 index 0000000000..15c814073e --- /dev/null +++ b/tests/integration/protocolpool/app_config.go @@ -0,0 +1,29 @@ +package protocolpool + +import ( + _ "cosmossdk.io/x/accounts" // import as blank for app wiring + _ "cosmossdk.io/x/auth" // import as blank for app wiring + _ "cosmossdk.io/x/auth/tx/config" // import as blank for app wiring + _ "cosmossdk.io/x/bank" // import as blank for app wiring + _ "cosmossdk.io/x/consensus" // import as blank for app wiring + _ "cosmossdk.io/x/distribution" // import as blank for app wiring + _ "cosmossdk.io/x/mint" // import as blank for app wiring + _ "cosmossdk.io/x/protocolpool" // import as blank for app wiring + _ "cosmossdk.io/x/staking" // import as blank for app wiring + + "github.com/cosmos/cosmos-sdk/testutil/configurator" + _ "github.com/cosmos/cosmos-sdk/x/genutil" // import as blank for app wiring +) + +var AppConfig = configurator.NewAppConfig( + configurator.AccountsModule(), + configurator.AuthModule(), + configurator.BankModule(), + configurator.StakingModule(), + configurator.TxModule(), + configurator.ConsensusModule(), + configurator.GenutilModule(), + configurator.MintModule(), + configurator.DistributionModule(), + configurator.ProtocolPoolModule(), +) diff --git a/tests/integration/protocolpool/module_test.go b/tests/integration/protocolpool/module_test.go new file mode 100644 index 0000000000..2c527dd0d5 --- /dev/null +++ b/tests/integration/protocolpool/module_test.go @@ -0,0 +1,138 @@ +package protocolpool + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/header" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/math" + authkeeper "cosmossdk.io/x/auth/keeper" + authtypes "cosmossdk.io/x/auth/types" + bankkeeper "cosmossdk.io/x/bank/keeper" + "cosmossdk.io/x/mint/types" + protocolpoolkeeper "cosmossdk.io/x/protocolpool/keeper" + protocolpooltypes "cosmossdk.io/x/protocolpool/types" + stakingkeeper "cosmossdk.io/x/staking/keeper" + + simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// TestWithdrawAnytime tests if withdrawing funds many times vs withdrawing funds once +// yield the same end balance. +func TestWithdrawAnytime(t *testing.T) { + var accountKeeper authkeeper.AccountKeeper + var protocolpoolKeeper protocolpoolkeeper.Keeper + var bankKeeper bankkeeper.Keeper + var stakingKeeper *stakingkeeper.Keeper + + app, err := simtestutil.SetupAtGenesis( + depinject.Configs( + AppConfig, + depinject.Supply(log.NewNopLogger()), + ), &accountKeeper, &protocolpoolKeeper, &bankKeeper, &stakingKeeper) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false).WithBlockHeight(1).WithHeaderInfo(header.Info{Height: 1}) + acc := accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) + require.NotNil(t, acc) + + testAddrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 5, math.NewInt(1)) + testAddr0Str, err := accountKeeper.AddressCodec().BytesToString(testAddrs[0]) + require.NoError(t, err) + + msgServer := protocolpoolkeeper.NewMsgServerImpl(protocolpoolKeeper) + _, err = msgServer.CreateContinuousFund( + ctx, + &protocolpooltypes.MsgCreateContinuousFund{ + Authority: protocolpoolKeeper.GetAuthority(), + Recipient: testAddr0Str, + Percentage: math.LegacyMustNewDecFromStr("0.5"), + }, + ) + require.NoError(t, err) + + // increase the community pool by a bunch + for i := 0; i < 30; i++ { + ctx, err = simtestutil.NextBlock(app, ctx, time.Minute) + require.NoError(t, err) + + // withdraw funds randomly, but it must always land on the same end balance + if rand.Intn(100) > 50 { + _, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{ + RecipientAddress: testAddr0Str, + }) + require.NoError(t, err) + } + } + + pool, err := protocolpoolKeeper.GetCommunityPool(ctx) + require.NoError(t, err) + require.True(t, pool.IsAllGT(sdk.NewCoins(sdk.NewInt64Coin("stake", 100000)))) + + _, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{ + RecipientAddress: testAddr0Str, + }) + require.NoError(t, err) + + endBalance := bankKeeper.GetBalance(ctx, testAddrs[0], sdk.DefaultBondDenom) + require.Equal(t, "11883031stake", endBalance.String()) +} + +// TestExpireInTheMiddle tests if a continuous fund that expires without anyone +// calling the withdraw function, the funds are still distributed correctly. +func TestExpireInTheMiddle(t *testing.T) { + t.Skip("This is a bug @facu found, will fix in another PR") + var accountKeeper authkeeper.AccountKeeper + var protocolpoolKeeper protocolpoolkeeper.Keeper + var bankKeeper bankkeeper.Keeper + var stakingKeeper *stakingkeeper.Keeper + + app, err := simtestutil.SetupAtGenesis( + depinject.Configs( + AppConfig, + depinject.Supply(log.NewNopLogger()), + ), &accountKeeper, &protocolpoolKeeper, &bankKeeper, &stakingKeeper) + require.NoError(t, err) + + ctx := app.BaseApp.NewContext(false).WithBlockHeight(1).WithHeaderInfo(header.Info{Height: 1}) + acc := accountKeeper.GetAccount(ctx, authtypes.NewModuleAddress(types.ModuleName)) + require.NotNil(t, acc) + + testAddrs := simtestutil.AddTestAddrs(bankKeeper, stakingKeeper, ctx, 5, math.NewInt(1)) + testAddr0Str, err := accountKeeper.AddressCodec().BytesToString(testAddrs[0]) + require.NoError(t, err) + + msgServer := protocolpoolkeeper.NewMsgServerImpl(protocolpoolKeeper) + + expirationTime := ctx.BlockTime().Add(time.Minute * 2) + _, err = msgServer.CreateContinuousFund( + ctx, + &protocolpooltypes.MsgCreateContinuousFund{ + Authority: protocolpoolKeeper.GetAuthority(), + Recipient: testAddr0Str, + Percentage: math.LegacyMustNewDecFromStr("0.1"), + Expiry: &expirationTime, + }, + ) + require.NoError(t, err) + + // increase the community pool by a bunch + for i := 0; i < 30; i++ { + ctx, err = simtestutil.NextBlock(app, ctx, time.Minute) + require.NoError(t, err) + } + + _, err = msgServer.WithdrawContinuousFund(ctx, &protocolpooltypes.MsgWithdrawContinuousFund{ + RecipientAddress: testAddr0Str, + }) + require.Error(t, err) + + endBalance := bankKeeper.GetBalance(ctx, testAddrs[0], sdk.DefaultBondDenom) + require.Equal(t, "158441stake", endBalance.String()) +} diff --git a/x/protocolpool/keeper/genesis_test.go b/x/protocolpool/keeper/genesis_test.go new file mode 100644 index 0000000000..4653ff6163 --- /dev/null +++ b/x/protocolpool/keeper/genesis_test.go @@ -0,0 +1,42 @@ +package keeper_test + +import ( + "time" + + "cosmossdk.io/math" + "cosmossdk.io/x/protocolpool/types" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (suite *KeeperTestSuite) TestInitGenesis() { + hour := time.Hour + gs := types.NewGenesisState( + []*types.ContinuousFund{ + { + Recipient: "cosmos1qy3529yj3v4xw2z3vz3vz3vz3vz3vz3v3k0vyf", + Percentage: math.LegacyMustNewDecFromStr("0.1"), + Expiry: nil, + }, + }, + []*types.Budget{ + { + RecipientAddress: "cosmos1qy3529yj3v4xw2z3vz3vz3vz3vz3vz3v3k0vyf", + ClaimedAmount: &sdk.Coin{}, + LastClaimedAt: &time.Time{}, + TranchesLeft: 10, + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)}, + Period: &hour, + }, + }, + ) + + err := suite.poolKeeper.InitGenesis(suite.ctx, gs) + suite.Require().NoError(err) + + // Export + exportedGenState, err := suite.poolKeeper.ExportGenesis(suite.ctx) + suite.Require().NoError(err) + suite.Require().Equal(gs.ContinuousFund, exportedGenState.ContinuousFund) + suite.Require().Equal(gs.Budget, exportedGenState.Budget) +} diff --git a/x/protocolpool/keeper/keeper.go b/x/protocolpool/keeper/keeper.go index a74bdc6253..4b5899200c 100644 --- a/x/protocolpool/keeper/keeper.go +++ b/x/protocolpool/keeper/keeper.go @@ -33,11 +33,9 @@ type Keeper struct { Schema collections.Schema BudgetProposal collections.Map[sdk.AccAddress, types.Budget] ContinuousFund collections.Map[sdk.AccAddress, types.ContinuousFund] - // RecipientFundPercentage key: RecipientAddr | value: Percentage in math.Int - RecipientFundPercentage collections.Map[sdk.AccAddress, math.Int] // RecipientFundDistribution key: RecipientAddr | value: Claimable amount RecipientFundDistribution collections.Map[sdk.AccAddress, math.Int] - // ToDistribute is to keep track of funds distributed + // ToDistribute is to keep track of funds to be distributed. It gets zeroed out in iterateAndUpdateFundsDistribution. ToDistribute collections.Item[math.Int] } @@ -63,7 +61,6 @@ func NewKeeper(cdc codec.BinaryCodec, env appmodule.Environment, ak types.Accoun authority: authority, BudgetProposal: collections.NewMap(sb, types.BudgetKey, "budget", sdk.AccAddressKey, codec.CollValue[types.Budget](cdc)), ContinuousFund: collections.NewMap(sb, types.ContinuousFundKey, "continuous_fund", sdk.AccAddressKey, codec.CollValue[types.ContinuousFund](cdc)), - RecipientFundPercentage: collections.NewMap(sb, types.RecipientFundPercentageKey, "recipient_fund_percentage", sdk.AccAddressKey, sdk.IntValue), RecipientFundDistribution: collections.NewMap(sb, types.RecipientFundDistributionKey, "recipient_fund_distribution", sdk.AccAddressKey, sdk.IntValue), ToDistribute: collections.NewItem(sb, types.ToDistributeKey, "to_distribute", sdk.IntValue), } @@ -125,20 +122,13 @@ func (k Keeper) withdrawContinuousFund(ctx context.Context, recipientAddr string return sdk.Coin{}, fmt.Errorf("cannot withdraw continuous funds: continuous fund expired for recipient: %s", recipientAddr) } - toDistributeAmount, err := k.ToDistribute.Get(ctx) + err = k.IterateAndUpdateFundsDistribution(ctx) if err != nil { - return sdk.Coin{}, err - } - - if !toDistributeAmount.Equal(math.ZeroInt()) { - err = k.iterateAndUpdateFundsDistribution(ctx, toDistributeAmount) - if err != nil { - return sdk.Coin{}, fmt.Errorf("error while iterating all the continuous funds: %w", err) - } + return sdk.Coin{}, fmt.Errorf("error while iterating all the continuous funds: %w", err) } // withdraw continuous fund - withdrawnAmount, err := k.withdrawRecipientFunds(ctx, recipientAddr) + withdrawnAmount, err := k.withdrawRecipientFunds(ctx, recipient) if err != nil { return sdk.Coin{}, fmt.Errorf("error while withdrawing recipient funds for recipient: %s", recipientAddr) } @@ -146,12 +136,7 @@ func (k Keeper) withdrawContinuousFund(ctx context.Context, recipientAddr string return withdrawnAmount, nil } -func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string) (sdk.Coin, error) { - recipient, err := k.authKeeper.AddressCodec().StringToBytes(recipientAddr) - if err != nil { - return sdk.Coin{}, sdkerrors.ErrInvalidAddress.Wrapf("invalid recipient address: %s", err) - } - +func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipient []byte) (sdk.Coin, error) { // get allocated continuous fund fundsAllocated, err := k.RecipientFundDistribution.Get(ctx, recipient) if err != nil { @@ -170,7 +155,7 @@ func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string withdrawnAmount := sdk.NewCoin(denom, fundsAllocated) err = k.DistributeFromStreamFunds(ctx, sdk.NewCoins(withdrawnAmount), recipient) if err != nil { - return sdk.Coin{}, fmt.Errorf("error while distributing funds to the recipient %s: %w", recipientAddr, err) + return sdk.Coin{}, fmt.Errorf("error while distributing funds: %w", err) } // reset fund distribution @@ -181,7 +166,8 @@ func (k Keeper) withdrawRecipientFunds(ctx context.Context, recipientAddr string return withdrawnAmount, nil } -// SetToDistribute sets the amount to be distributed among recipients. +// SetToDistribute sets the amount to be distributed among recipients, usually called by x/distribution while allocating +// reward and fee distribution. // This could be only set by the authority address. func (k Keeper) SetToDistribute(ctx context.Context, amount sdk.Coins, addr string) error { authAddr, err := k.authKeeper.AddressCodec().StringToBytes(addr) @@ -201,46 +187,49 @@ func (k Keeper) SetToDistribute(ctx context.Context, amount sdk.Coins, addr stri return err } - totalStreamFundsPercentage := math.ZeroInt() - err = k.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) { - totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value) + totalStreamFundsPercentage := math.LegacyZeroDec() + err = k.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, cf types.ContinuousFund) (stop bool, err error) { + // Check if the continuous fund has expired + if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) { + return false, nil + } + + totalStreamFundsPercentage = totalStreamFundsPercentage.Add(cf.Percentage) + if totalStreamFundsPercentage.GT(math.LegacyOneDec()) { + return true, fmt.Errorf("total funds percentage cannot exceed 100") + } + return false, nil }) - if totalStreamFundsPercentage.GT(math.NewInt(100)) { - return fmt.Errorf("total funds percentage cannot exceed 100") - } if err != nil { return err } + // if percentage is 0 then return early + if totalStreamFundsPercentage.IsZero() { + return nil + } + // send streaming funds to the stream module account - if err := k.sendFundsToStreamModule(ctx, denom, totalStreamFundsPercentage); err != nil { - return err - } - - err = k.ToDistribute.Set(ctx, amount.AmountOf(denom)) - if err != nil { - return fmt.Errorf("error while setting ToDistribute: %w", err) - } - return nil -} - -func (k Keeper) sendFundsToStreamModule(ctx context.Context, denom string, percentage math.Int) error { - totalPoolAmt, err := k.GetCommunityPool(ctx) - if err != nil { - return err - } - - poolAmt := totalPoolAmt.AmountOf(denom) - poolAmtDec := sdk.NewDecCoins(sdk.NewDecCoin(denom, poolAmt)) - amt := poolAmtDec.MulDec(math.LegacyNewDecFromIntWithPrec(percentage, 2)) - streamAmt := sdk.NewCoins(sdk.NewCoin(denom, amt.AmountOf(denom).TruncateInt())) - - // Send streaming funds to the StreamModuleAccount + toDistributeAmt := math.LegacyNewDecFromInt(amount.AmountOf(denom)).Mul(totalStreamFundsPercentage).TruncateInt() + streamAmt := sdk.NewCoins(sdk.NewCoin(denom, toDistributeAmt)) if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, types.ModuleName, types.StreamAccount, streamAmt); err != nil { return err } + amountToDistribute, err := k.ToDistribute.Get(ctx) + if err != nil { + if errors.Is(err, collections.ErrNotFound) { + amountToDistribute = math.ZeroInt() + } else { + return err + } + } + + err = k.ToDistribute.Set(ctx, amountToDistribute.Add(amount.AmountOf(denom))) + if err != nil { + return fmt.Errorf("error while setting ToDistribute: %w", err) + } return nil } @@ -254,79 +243,56 @@ func (k Keeper) hasPermission(addr []byte) (bool, error) { return bytes.Equal(authAcc, addr), nil } -type recipientFund struct { - RecipientAddr string - Percentage math.Int -} - -func (k Keeper) iterateAndUpdateFundsDistribution(ctx context.Context, toDistributeAmount math.Int) error { - totalPercentageToBeDistributed := math.ZeroInt() - - recipientFundList := []recipientFund{} - - // Calculate totalPercentageToBeDistributed and store values - err := k.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) { - addr, err := k.authKeeper.AddressCodec().BytesToString(key) - 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, - Percentage: value, - }) - return false, nil - }) +func (k Keeper) IterateAndUpdateFundsDistribution(ctx context.Context) error { + toDistributeAmount, err := k.ToDistribute.Get(ctx) if err != nil { return err } - if totalPercentageToBeDistributed.GT(math.NewInt(100)) { - return fmt.Errorf("total funds percentage cannot exceed 100") + + // if there are no funds to distribute, return + if toDistributeAmount.IsZero() { + return nil } + totalPercentageToBeDistributed := math.LegacyZeroDec() + denom, err := k.stakingKeeper.BondDenom(ctx) if err != nil { return err } - toDistributeDec := sdk.NewDecCoins(sdk.NewDecCoin(denom, toDistributeAmount)) + toDistributeDec := sdk.NewDecCoin(denom, toDistributeAmount) - // Calculate the funds to be distributed based on the total percentage to be distributed - totalAmountToBeDistributed := toDistributeDec.MulDec(math.LegacyNewDecFromIntWithPrec(totalPercentageToBeDistributed, 2)) - totalDistrAmount := totalAmountToBeDistributed.AmountOf(denom) - - for _, value := range recipientFundList { - // Calculate the funds to be distributed based on the percentage - decValue := math.LegacyNewDecFromIntWithPrec(value.Percentage, 2) - percentage := math.LegacyNewDecFromIntWithPrec(totalPercentageToBeDistributed, 2) - recipientAmount := totalDistrAmount.Mul(decValue).Quo(percentage) - recipientCoins := recipientAmount.TruncateInt() - - key, err := k.authKeeper.AddressCodec().StringToBytes(value.RecipientAddr) - if err != nil { - return err + // Calculate totalPercentageToBeDistributed and store values + err = k.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, cf types.ContinuousFund) (stop bool, err error) { + // Check if the continuous fund has expired + if cf.Expiry != nil && cf.Expiry.Before(k.HeaderService.HeaderInfo(ctx).Time) { + return false, nil } + // sanity check for max percentage + totalPercentageToBeDistributed = totalPercentageToBeDistributed.Add(cf.Percentage) + if totalPercentageToBeDistributed.GT(math.LegacyOneDec()) { + return true, fmt.Errorf("total funds percentage cannot exceed 100") + } + + // Calculate the funds to be distributed based on the percentage + recipientAmount := toDistributeDec.Amount.Mul(cf.Percentage).TruncateInt() + // Set funds to be claimed toClaim, err := k.RecipientFundDistribution.Get(ctx, key) if err != nil { - return err + return true, err } - amount := toClaim.Add(recipientCoins) + amount := toClaim.Add(recipientAmount) err = k.RecipientFundDistribution.Set(ctx, key, amount) if err != nil { - return err + return true, err } + + return false, nil + }) + if err != nil { + return err } // Set the coins to be distributed from toDistribute to 0 diff --git a/x/protocolpool/keeper/keeper_test.go b/x/protocolpool/keeper/keeper_test.go index fccb8722b3..ddfcd6d824 100644 --- a/x/protocolpool/keeper/keeper_test.go +++ b/x/protocolpool/keeper/keeper_test.go @@ -113,3 +113,45 @@ func (s *KeeperTestSuite) mockStreamFunds() { func TestKeeperTestSuite(t *testing.T) { suite.Run(t, new(KeeperTestSuite)) } + +func (s *KeeperTestSuite) TestIterateAndUpdateFundsDistribution() { + // We'll create 2 continuous funds of 30% each, and the total pool is 1000000, meaning each fund should get 300000 + + s.SetupTest() + s.authKeeper.EXPECT().GetModuleAccount(s.ctx, types.ModuleName).Return(poolAcc).AnyTimes() + distrBal := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(1000000))) + s.bankKeeper.EXPECT().GetAllBalances(s.ctx, poolAcc.GetAddress()).Return(distrBal).AnyTimes() + s.bankKeeper.EXPECT().SendCoinsFromModuleToModule(s.ctx, poolAcc.GetName(), streamAcc.GetName(), sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(600000)))).AnyTimes() + + _, err := s.msgServer.CreateContinuousFund(s.ctx, &types.MsgCreateContinuousFund{ + Authority: s.poolKeeper.GetAuthority(), + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2srklj6", + Percentage: math.LegacyMustNewDecFromStr("0.3"), + }) + s.Require().NoError(err) + + _, err = s.msgServer.CreateContinuousFund(s.ctx, &types.MsgCreateContinuousFund{ + Authority: s.poolKeeper.GetAuthority(), + Recipient: "cosmos1tygms3xhhs3yv487phx3dw4a95jn7t7lpm470r", + Percentage: math.LegacyMustNewDecFromStr("0.3"), + }) + s.Require().NoError(err) + + _ = s.poolKeeper.SetToDistribute(s.ctx, sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(1000000))), s.poolKeeper.GetAuthority()) + + err = s.poolKeeper.IterateAndUpdateFundsDistribution(s.ctx) + s.Require().NoError(err) + + err = s.poolKeeper.RecipientFundDistribution.Walk(s.ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) { + strAddr, err := s.authKeeper.AddressCodec().BytesToString(key) + s.Require().NoError(err) + + if strAddr == "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2srklj6" { + s.Require().Equal(value, math.NewInt(300000)) + } else if strAddr == "cosmos1tygms3xhhs3yv487phx3dw4a95jn7t7lpm470r" { + s.Require().Equal(value, math.NewInt(300000)) + } + return false, nil + }) + s.Require().NoError(err) +} diff --git a/x/protocolpool/keeper/msg_server.go b/x/protocolpool/keeper/msg_server.go index 3af5f91df8..775826cda7 100644 --- a/x/protocolpool/keeper/msg_server.go +++ b/x/protocolpool/keeper/msg_server.go @@ -126,18 +126,17 @@ func (k MsgServer) CreateContinuousFund(ctx context.Context, msg *types.MsgCreat // Check if total funds percentage exceeds 100% // If exceeds, we should not setup continuous fund proposal. - totalStreamFundsPercentage := math.ZeroInt() - err = k.Keeper.RecipientFundPercentage.Walk(ctx, nil, func(key sdk.AccAddress, value math.Int) (stop bool, err error) { - totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value) + totalStreamFundsPercentage := math.LegacyZeroDec() + err = k.Keeper.ContinuousFund.Walk(ctx, nil, func(key sdk.AccAddress, value types.ContinuousFund) (stop bool, err error) { + totalStreamFundsPercentage = totalStreamFundsPercentage.Add(value.Percentage) return false, nil }) if err != nil { return nil, err } - percentage := msg.Percentage.MulInt64(100) - totalStreamFundsPercentage = totalStreamFundsPercentage.Add(percentage.TruncateInt()) - if totalStreamFundsPercentage.GT(math.NewInt(100)) { - return nil, fmt.Errorf("cannot set continuous fund proposal\ntotal funds percentage exceeds 100\ncurrent total percentage: %v", totalStreamFundsPercentage.Sub(percentage.TruncateInt())) + totalStreamFundsPercentage = totalStreamFundsPercentage.Add(msg.Percentage) + if totalStreamFundsPercentage.GT(math.LegacyOneDec()) { + return nil, fmt.Errorf("cannot set continuous fund proposal\ntotal funds percentage exceeds 100\ncurrent total percentage: %s", totalStreamFundsPercentage.Sub(msg.Percentage).MulInt64(100).TruncateInt().String()) } // Create continuous fund proposal @@ -153,11 +152,6 @@ func (k MsgServer) CreateContinuousFund(ctx context.Context, msg *types.MsgCreat return nil, err } - // Set recipient fund percentage & distribution - err = k.RecipientFundPercentage.Set(ctx, recipient, percentage.TruncateInt()) - if err != nil { - return nil, err - } err = k.RecipientFundDistribution.Set(ctx, recipient, math.ZeroInt()) if err != nil { return nil, err @@ -171,9 +165,6 @@ func (k MsgServer) WithdrawContinuousFund(ctx context.Context, msg *types.MsgWit if err != nil { return nil, err } - if amount.IsNil() { - k.Logger.Info(fmt.Sprintf("no distribution amount found for recipient %s", msg.RecipientAddress)) - } return &types.MsgWithdrawContinuousFundResponse{Amount: amount}, nil } @@ -200,7 +191,7 @@ func (k MsgServer) CancelContinuousFund(ctx context.Context, msg *types.MsgCance } // withdraw funds if any are allocated - withdrawnFunds, err := k.withdrawRecipientFunds(ctx, msg.RecipientAddress) + withdrawnFunds, err := k.withdrawRecipientFunds(ctx, recipient) if err != nil && !errorspkg.Is(err, types.ErrNoRecipientFund) { return nil, fmt.Errorf("error while withdrawing already allocated funds for recipient %s: %w", msg.RecipientAddress, err) } @@ -209,10 +200,6 @@ func (k MsgServer) CancelContinuousFund(ctx context.Context, msg *types.MsgCance return nil, fmt.Errorf("failed to remove continuous fund for recipient %s: %w", msg.RecipientAddress, err) } - if err := k.RecipientFundPercentage.Remove(ctx, recipient); err != nil { - return nil, fmt.Errorf("failed to remove recipient fund percentage for recipient %s: %w", msg.RecipientAddress, err) - } - if err := k.RecipientFundDistribution.Remove(ctx, recipient); err != nil { return nil, fmt.Errorf("failed to remove recipient fund distribution for recipient %s: %w", msg.RecipientAddress, err) } diff --git a/x/protocolpool/keeper/msg_server_test.go b/x/protocolpool/keeper/msg_server_test.go index 683fd21f59..37b122170d 100644 --- a/x/protocolpool/keeper/msg_server_test.go +++ b/x/protocolpool/keeper/msg_server_test.go @@ -421,9 +421,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt()) suite.Require().NoError(err) @@ -439,8 +436,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage = percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt()) suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt()) suite.Require().NoError(err) @@ -486,9 +481,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt()) suite.Require().NoError(err) err = suite.poolKeeper.ToDistribute.Set(suite.ctx, math.ZeroInt()) @@ -510,9 +502,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt()) suite.Require().NoError(err) toDistribute := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000))) @@ -539,9 +528,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt()) suite.Require().NoError(err) toDistribute := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, math.NewInt(100000))) @@ -569,9 +555,6 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient, math.ZeroInt()) suite.Require().NoError(err) @@ -580,16 +563,13 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { suite.Require().NoError(err) cf = types.ContinuousFund{ Recipient: recipient2StrAddr, - Percentage: percentage, + Percentage: percentage2, Expiry: &expiry, } // Set continuous fund err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage = percentage2.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt()) suite.Require().NoError(err) @@ -598,16 +578,13 @@ func (suite *KeeperTestSuite) TestWithdrawContinuousFund() { suite.Require().NoError(err) cf = types.ContinuousFund{ Recipient: recipient3StrAddr, - Percentage: percentage2, + Percentage: percentage3, Expiry: &expiry, } // Set continuous fund err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient3, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage = percentage3.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient3, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient3, math.ZeroInt()) suite.Require().NoError(err) @@ -767,9 +744,6 @@ func (suite *KeeperTestSuite) TestCreateContinuousFund() { } err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf) suite.Require().NoError(err) - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt()) - suite.Require().NoError(err) }, input: &types.MsgCreateContinuousFund{ Authority: suite.poolKeeper.GetAuthority(), @@ -860,9 +834,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipientAddr, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage := percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipientAddr, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipientAddr, math.ZeroInt()) suite.Require().NoError(err) @@ -878,9 +849,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() { err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient2, cf) suite.Require().NoError(err) // Set recipient fund percentage and recipient fund distribution - intPercentage = percentage.MulInt64(100) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient2, intPercentage.TruncateInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient2, math.ZeroInt()) suite.Require().NoError(err) @@ -919,8 +887,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() { suite.mockWithdrawContinuousFund() err = suite.poolKeeper.ContinuousFund.Set(suite.ctx, recipient3, cf) suite.Require().NoError(err) - err = suite.poolKeeper.RecipientFundPercentage.Set(suite.ctx, recipient3, math.ZeroInt()) - suite.Require().NoError(err) err = suite.poolKeeper.RecipientFundDistribution.Set(suite.ctx, recipient3, math.ZeroInt()) suite.Require().NoError(err) }, @@ -955,8 +921,6 @@ func (suite *KeeperTestSuite) TestCancelContinuousFund() { suite.Require().NoError(err) suite.Require().Equal(tc.withdrawnFunds, resp.WithdrawnAllocatedFund) // All items below should return error as they are removed from the store - _, err := suite.poolKeeper.RecipientFundPercentage.Get(suite.ctx, tc.recipientAddr) - suite.Require().Contains(err.Error(), "collections: not found") _, err = suite.poolKeeper.ContinuousFund.Get(suite.ctx, tc.recipientAddr) suite.Require().Contains(err.Error(), "collections: not found") _, err = suite.poolKeeper.RecipientFundDistribution.Get(suite.ctx, tc.recipientAddr) diff --git a/x/protocolpool/module.go b/x/protocolpool/module.go index a3a79decf8..e8cfd7bde2 100644 --- a/x/protocolpool/module.go +++ b/x/protocolpool/module.go @@ -9,7 +9,6 @@ import ( "google.golang.org/grpc" "cosmossdk.io/core/appmodule" - "cosmossdk.io/core/legacy" "cosmossdk.io/core/registry" "cosmossdk.io/x/protocolpool/keeper" "cosmossdk.io/x/protocolpool/types" @@ -24,7 +23,6 @@ const ConsensusVersion = 1 var ( _ module.HasName = AppModule{} - _ module.HasAminoCodec = AppModule{} _ module.HasGRPCGateway = AppModule{} _ module.AppModuleSimulation = AppModule{} @@ -60,9 +58,6 @@ func (AppModule) IsAppModule() {} // Name returns the pool module's name. func (AppModule) Name() string { return types.ModuleName } -// RegisterLegacyAminoCodec registers the pool module's types on the LegacyAmino codec. -func (AppModule) RegisterLegacyAminoCodec(cdc legacy.Amino) {} - // RegisterGRPCGatewayRoutes registers the gRPC Gateway routes func (AppModule) RegisterGRPCGatewayRoutes(clientCtx client.Context, mux *gwruntime.ServeMux) { if err := types.RegisterQueryHandlerClient(context.Background(), mux, types.NewQueryClient(clientCtx)); err != nil { @@ -104,10 +99,8 @@ func (am AppModule) InitGenesis(ctx context.Context, data json.RawMessage) error if err := am.cdc.UnmarshalJSON(data, &genesisState); err != nil { return err } - if err := am.keeper.InitGenesis(ctx, &genesisState); err != nil { - return err - } - return nil + + return am.keeper.InitGenesis(ctx, &genesisState) } // ExportGenesis returns the exported genesis state as raw bytes for the protocolpool module. diff --git a/x/protocolpool/testutil/expected_keepers_mocks.go b/x/protocolpool/testutil/expected_keepers_mocks.go index 6740d784af..9bbdfc5828 100644 --- a/x/protocolpool/testutil/expected_keepers_mocks.go +++ b/x/protocolpool/testutil/expected_keepers_mocks.go @@ -129,20 +129,6 @@ func (mr *MockBankKeeperMockRecorder) GetAllBalances(ctx, addr interface{}) *gom return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllBalances", reflect.TypeOf((*MockBankKeeper)(nil).GetAllBalances), ctx, addr) } -// MintCoins mocks base method. -func (m *MockBankKeeper) MintCoins(ctx context.Context, moduleName string, amt types.Coins) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MintCoins", ctx, moduleName, amt) - ret0, _ := ret[0].(error) - return ret0 -} - -// MintCoins indicates an expected call of MintCoins. -func (mr *MockBankKeeperMockRecorder) MintCoins(ctx, moduleName, amt interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintCoins", reflect.TypeOf((*MockBankKeeper)(nil).MintCoins), ctx, moduleName, amt) -} - // SendCoinsFromAccountToModule mocks base method. func (m *MockBankKeeper) SendCoinsFromAccountToModule(ctx context.Context, senderAddr types.AccAddress, recipientModule string, amt types.Coins) error { m.ctrl.T.Helper() diff --git a/x/protocolpool/types/errors.go b/x/protocolpool/types/errors.go index 5c24b95d17..a3790136b9 100644 --- a/x/protocolpool/types/errors.go +++ b/x/protocolpool/types/errors.go @@ -4,5 +4,5 @@ import "cosmossdk.io/errors" var ( ErrInvalidSigner = errors.Register(ModuleName, 2, "expected authority account as only signer for community pool spend message") - ErrNoRecipientFund = errors.Register(ModuleName, 3, "no recipient fund found for recipient") + ErrNoRecipientFund = errors.Register(ModuleName, 3, "no recipient found") ) diff --git a/x/protocolpool/types/expected_keepers.go b/x/protocolpool/types/expected_keepers.go index c83f921ee7..d967ca5c1a 100644 --- a/x/protocolpool/types/expected_keepers.go +++ b/x/protocolpool/types/expected_keepers.go @@ -17,7 +17,6 @@ type AccountKeeper interface { // BankKeeper defines the expected interface needed to retrieve account balances. type BankKeeper interface { - MintCoins(ctx context.Context, moduleName string, amt sdk.Coins) error GetAllBalances(ctx context.Context, addr sdk.AccAddress) sdk.Coins SpendableCoins(ctx context.Context, addr sdk.AccAddress) sdk.Coins SendCoinsFromModuleToAccount(ctx context.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error diff --git a/x/protocolpool/types/genesis.go b/x/protocolpool/types/genesis.go index 8c23fa13ff..b16e4ff97e 100644 --- a/x/protocolpool/types/genesis.go +++ b/x/protocolpool/types/genesis.go @@ -6,7 +6,6 @@ import ( "cosmossdk.io/errors" "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) @@ -45,12 +44,11 @@ func validateBudget(bp Budget) error { } // Validate BudgetPerTranche - amount := sdk.NewCoins(*bp.BudgetPerTranche) - if amount.IsZero() { + if bp.BudgetPerTranche == nil || bp.BudgetPerTranche.IsZero() { return fmt.Errorf("budget per tranche cannot be zero") } - if err := amount.Validate(); err != nil { - return errors.Wrap(sdkerrors.ErrInvalidCoins, amount.String()) + if err := bp.BudgetPerTranche.Validate(); err != nil { + return errors.Wrap(sdkerrors.ErrInvalidCoins, bp.BudgetPerTranche.String()) } if bp.TranchesLeft == 0 { @@ -69,7 +67,7 @@ func validateContinuousFund(cf ContinuousFund) error { } // Validate percentage - if cf.Percentage.IsZero() || cf.Percentage.IsNil() { + if cf.Percentage.IsNil() || cf.Percentage.IsZero() { return fmt.Errorf("percentage cannot be zero or empty") } if cf.Percentage.IsNegative() { diff --git a/x/protocolpool/types/msg.go b/x/protocolpool/types/msg.go index c25616d8b5..bb35172b2e 100644 --- a/x/protocolpool/types/msg.go +++ b/x/protocolpool/types/msg.go @@ -18,9 +18,9 @@ func NewMsgFundCommunityPool(amount sdk.Coins, depositor string) *MsgFundCommuni } } -// NewCommunityPoolSpend returns a new CommunityPoolSpend with authority, recipient and +// NewMsgCommunityPoolSpend returns a new CommunityPoolSpend with authority, recipient and // a spending amount. -func NewCommunityPoolSpend(amount sdk.Coins, authority, recipient string) *MsgCommunityPoolSpend { +func NewMsgCommunityPoolSpend(amount sdk.Coins, authority, recipient string) *MsgCommunityPoolSpend { return &MsgCommunityPoolSpend{ Authority: authority, Recipient: recipient, diff --git a/x/protocolpool/types/types_test.go b/x/protocolpool/types/types_test.go new file mode 100644 index 0000000000..23d51bc7e7 --- /dev/null +++ b/x/protocolpool/types/types_test.go @@ -0,0 +1,230 @@ +package types + +import ( + "testing" + time "time" + + "github.com/stretchr/testify/require" + + "cosmossdk.io/math" + + codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var hour = time.Hour + +func TestRegisterInterfaces(t *testing.T) { + interfaceRegistry := codectestutil.NewCodecOptionsWithPrefixes("cosmos", "cosmosval").NewInterfaceRegistry() + RegisterInterfaces(interfaceRegistry) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgFundCommunityPool{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCommunityPoolSpend{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgSubmitBudgetProposal{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgClaimBudget{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCreateContinuousFund{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgCancelContinuousFund{})) + require.NoError(t, interfaceRegistry.EnsureRegistered(&MsgWithdrawContinuousFund{})) +} + +func TestNewMsgFundCommunityPool(t *testing.T) { + amount := sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100))) + depositor := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4" + msg := NewMsgFundCommunityPool(amount, depositor) + require.Equal(t, amount, msg.Amount) + require.Equal(t, depositor, msg.Depositor) +} + +func TestNewMsgCommunityPoolSpend(t *testing.T) { + amount := sdk.NewCoins(sdk.NewCoin("stake", math.NewInt(100))) + authority := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4" + recipient := "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z5" + msg := NewMsgCommunityPoolSpend(amount, authority, recipient) + require.Equal(t, amount, msg.Amount) + require.Equal(t, authority, msg.Authority) + require.Equal(t, recipient, msg.Recipient) +} + +func TestValidateGenesis(t *testing.T) { + defaultGenesis := DefaultGenesisState() + err := ValidateGenesis(defaultGenesis) + require.NoError(t, err) + + gs := NewGenesisState( + []*ContinuousFund{ + { + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyMustNewDecFromStr("0.1"), + Expiry: nil, + }, + }, + []*Budget{ + { + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + ClaimedAmount: &sdk.Coin{}, + LastClaimedAt: &time.Time{}, + TranchesLeft: 10, + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)}, + Period: &hour, + }, + }, + ) + + err = ValidateGenesis(gs) + require.NoError(t, err) + + gs.Budget[0].RecipientAddress = "" + err = ValidateGenesis(gs) + require.EqualError(t, err, "recipient cannot be empty") + + gs.ContinuousFund[0].Recipient = "" + err = ValidateGenesis(gs) + require.EqualError(t, err, "recipient cannot be empty") +} + +func TestValidateBudget(t *testing.T) { + testCases := []struct { + name string + budget Budget + expErrMsg string + }{ + { + "valid budget", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + ClaimedAmount: &sdk.Coin{}, + LastClaimedAt: &time.Time{}, + TranchesLeft: 10, + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)}, + Period: &hour, + }, + "", + }, + { + "empty recipient", + Budget{ + RecipientAddress: "", + }, + "recipient cannot be empty", + }, + { + "zero budget per tranche", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(0)}, + }, + "budget per tranche cannot be zero", + }, + { + "nil budget per tranche", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + BudgetPerTranche: nil, + }, + "budget per tranche cannot be zero", + }, + { + "negative budget per tranche", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(-100)}, + }, + "-100stake: invalid coins", + }, + { + "zero tranches left", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + TranchesLeft: 0, + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)}, + }, + "invalid budget proposal: tranches must be greater than zero", + }, + { + "zero period", + Budget{ + RecipientAddress: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + ClaimedAmount: &sdk.Coin{}, + LastClaimedAt: &time.Time{}, + TranchesLeft: 10, + BudgetPerTranche: &sdk.Coin{Denom: "stake", Amount: math.NewInt(100)}, + Period: nil, + }, + "invalid budget proposal: period length should be greater than zero", + }, + } + + for _, tc := range testCases { + err := validateBudget(tc.budget) + if tc.expErrMsg == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expErrMsg) + } + } +} + +func TestValidateContinuousFund(t *testing.T) { + testCases := []struct { + name string + cf ContinuousFund + expErrMsg string + }{ + { + "valid continuous fund", + ContinuousFund{ + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyMustNewDecFromStr("0.1"), + Expiry: nil, + }, + "", + }, + { + "empty recipient", + ContinuousFund{ + Recipient: "", + }, + "recipient cannot be empty", + }, + { + "zero percentage", + ContinuousFund{ + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyZeroDec(), + }, + "percentage cannot be zero or empty", + }, + { + "nil percentage", + ContinuousFund{ + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyDec{}, + }, + "percentage cannot be zero or empty", + }, + { + "negative percentage", + ContinuousFund{ + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyMustNewDecFromStr("-0.1"), + }, + "percentage cannot be negative", + }, + { + "percentage exceeds 100%", + ContinuousFund{ + Recipient: "cosmos1qypq2q2l8z4wz2z2l8z4wz2z2l8z4wz2z2l8z4", + Percentage: math.LegacyMustNewDecFromStr("1.1"), + }, + "percentage cannot be greater than one", + }, + } + + for _, tc := range testCases { + err := validateContinuousFund(tc.cf) + if tc.expErrMsg == "" { + require.NoError(t, err) + } else { + require.EqualError(t, err, tc.expErrMsg) + } + } +}