diff --git a/app/params/params.go b/app/params/params.go index b6aa5fb..9a4bdca 100644 --- a/app/params/params.go +++ b/app/params/params.go @@ -4,4 +4,6 @@ package params const ( StakePerAccount = "stake_per_account" InitiallyBondedValidators = "initially_bonded_validators" + + DefaultWeightUpdateBudgetPlans int = 5 ) diff --git a/x/budget/keeper/budget.go b/x/budget/keeper/budget.go index e0b0e90..c61c4eb 100644 --- a/x/budget/keeper/budget.go +++ b/x/budget/keeper/budget.go @@ -3,7 +3,6 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/tendermint/budget/x/budget/types" ) @@ -27,7 +26,7 @@ func (k Keeper) CollectBudgets(ctx sdk.Context) error { if err != nil { return err } - sourceBalances := sdk.NewDecCoinsFromCoins(k.bankKeeper.GetAllBalances(ctx, sourceAcc)...) + sourceBalances := sdk.NewDecCoinsFromCoins(k.bankKeeper.SpendableCoins(ctx, sourceAcc)...) if sourceBalances.IsZero() { continue } @@ -40,9 +39,8 @@ func (k Keeper) CollectBudgets(ctx sdk.Context) error { if err != nil { return err } - collectionCoins, _ := sourceBalances.MulDecTruncate(budget.Rate).TruncateDecimal() - if collectionCoins.Empty() || !collectionCoins.IsValid() { + if collectionCoins.Empty() || collectionCoins.IsZero() || !collectionCoins.IsValid() { continue } @@ -51,19 +49,19 @@ func (k Keeper) CollectBudgets(ctx sdk.Context) error { budgetsBySource.CollectionCoins[i] = collectionCoins } - if err := k.bankKeeper.InputOutputCoins(ctx, inputs, outputs); err != nil { + if err = k.bankKeeper.InputOutputCoins(ctx, inputs, outputs); err != nil { return err } for i, budget := range budgetsBySource.Budgets { + if budgetsBySource.CollectionCoins[i].Empty() || budgetsBySource.CollectionCoins[i].IsZero() || !budgetsBySource.CollectionCoins[i].IsValid() { + continue + } k.AddTotalCollectedCoins(ctx, budget.Name, budgetsBySource.CollectionCoins[i]) ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeBudgetCollected, sdk.NewAttribute(types.AttributeValueName, budget.Name), - sdk.NewAttribute(types.AttributeValueDestinationAddress, budget.DestinationAddress), - sdk.NewAttribute(types.AttributeValueSourceAddress, budget.SourceAddress), - sdk.NewAttribute(types.AttributeValueRate, budget.Rate.String()), sdk.NewAttribute(types.AttributeValueAmount, budgetsBySource.CollectionCoins[i].String()), ), }) diff --git a/x/budget/keeper/budget_test.go b/x/budget/keeper/budget_test.go index 2587bab..7346080 100644 --- a/x/budget/keeper/budget_test.go +++ b/x/budget/keeper/budget_test.go @@ -20,7 +20,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { epochBlocks uint32 accAsserts []sdk.AccAddress balanceAsserts []sdk.Coins - expectErr bool + expectErr error }{ { "basic budgets case", @@ -44,7 +44,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { {}, mustParseCoinsNormalized("1000000000denom1,1000000000denom2,1000000000denom3,1000000000stake"), }, - false, + nil, }, { "only expired budget case", @@ -58,7 +58,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { {}, mustParseCoinsNormalized("1000000000denom1,1000000000denom2,1000000000denom3,1000000000stake"), }, - false, + nil, }, { "source has small balances case", @@ -74,7 +74,35 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { mustParseCoinsNormalized("1denom2,1denom3,500000000stake"), mustParseCoinsNormalized("1denom1,1denom3"), }, - false, + nil, + }, + { + "source has small balances case 2", + []types.Budget{suite.budgets[6]}, + types.DefaultEpochBlocks, + []sdk.AccAddress{ + suite.destinationAddrs[4], + suite.sourceAddrs[4], + }, + []sdk.Coins{ + mustParseCoinsNormalized(""), + mustParseCoinsNormalized("1denom1,2denom2"), + }, + nil, + }, + { + "empty source case", + []types.Budget{suite.budgets[7]}, + types.DefaultEpochBlocks, + []sdk.AccAddress{ + suite.destinationAddrs[5], + suite.sourceAddrs[5], + }, + []sdk.Coins{ + {}, + {}, + }, + nil, }, { "none budgets case", @@ -100,7 +128,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { mustParseCoinsNormalized("1000000000denom1,1000000000denom2,1000000000denom3,1000000000stake"), mustParseCoinsNormalized("1denom1,2denom2,3denom3,1000000000stake"), }, - false, + nil, }, { "disabled budget epoch", @@ -126,7 +154,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { mustParseCoinsNormalized("1000000000denom1,1000000000denom2,1000000000denom3,1000000000stake"), mustParseCoinsNormalized("1denom1,2denom2,3denom3,1000000000stake"), }, - false, + nil, }, { "disabled budget epoch with budgets", @@ -152,7 +180,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { mustParseCoinsNormalized("1000000000denom1,1000000000denom2,1000000000denom3,1000000000stake"), mustParseCoinsNormalized("1denom1,2denom2,3denom3,1000000000stake"), }, - false, + nil, }, } { suite.Run(tc.name, func() { @@ -163,7 +191,7 @@ func (suite *KeeperTestSuite) TestCollectBudgets() { suite.keeper.SetParams(suite.ctx, params) err := suite.keeper.CollectBudgets(suite.ctx) - if tc.expectErr { + if tc.expectErr != nil { suite.Error(err) } else { suite.NoError(err) @@ -211,7 +239,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" } ]`, }), @@ -240,7 +268,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" }, { "name": "gravity-dex-farming-2", @@ -266,7 +294,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }, }, { - "add budget 3 with invalid total rate case 1", + "add budget 3 overlapped with 2, invalid total rate, 1.5", testProposal(proposal.ParamChange{ Subspace: types.ModuleName, Key: string(types.KeyBudgets), @@ -277,7 +305,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" }, { "name": "gravity-dex-farming-2", @@ -292,7 +320,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "rate": "0.500000000000000000", "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1e0n8jmeg4u8q3es2tmhz5zlte8a4q8687ndns8pj4q8grdl74a0sw3045s", - "start_time": "2021-09-30T00:00:00Z", + "start_time": "2021-09-29T00:00:00Z", "end_time": "2021-10-10T00:00:00Z" } ]`, @@ -301,7 +329,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { 1, // left last budgets of 2nd tc types.MustParseRFC3339("2021-09-29T00:00:00Z"), types.MustParseRFC3339("2021-09-30T00:00:00Z"), - types.ErrInvalidTotalBudgetRate, + types.ErrInvalidTotalBudgetRate, // 1.5 ( 0.5 + 0.5 + 0.5 ) []sdk.AccAddress{budgetSource, suite.destinationAddrs[0], suite.destinationAddrs[1], suite.destinationAddrs[2]}, []sdk.Coins{ mustParseCoinsNormalized("500000000denom1,500000000denom2,500000000denom3,500000000stake"), @@ -311,7 +339,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }, }, { - "add budget 3 with invalid total rate case 2", + "add budget 3 with invalid total rate, 1.01", testProposal(proposal.ParamChange{ Subspace: types.ModuleName, Key: string(types.KeyBudgets), @@ -322,7 +350,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" }, { "name": "gravity-dex-farming-2", @@ -334,7 +362,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }, { "name": "gravity-dex-farming-3", - "rate": "0.500000000000000000", + "rate": "0.510000000000000000", "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1e0n8jmeg4u8q3es2tmhz5zlte8a4q8687ndns8pj4q8grdl74a0sw3045s", "start_time": "2021-09-30T00:00:00Z", @@ -346,7 +374,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { 1, // left last budgets of 2nd tc types.MustParseRFC3339("2021-10-01T00:00:00Z"), types.MustParseRFC3339("2021-10-01T00:00:00Z"), - types.ErrInvalidTotalBudgetRate, + types.ErrInvalidTotalBudgetRate, // 1.01 ( 0.5 + 0.0 + 0.51 ) []sdk.AccAddress{budgetSource, suite.destinationAddrs[0], suite.destinationAddrs[1], suite.destinationAddrs[2]}, []sdk.Coins{ mustParseCoinsNormalized("750000000denom1,750000000denom2,750000000denom3,750000000stake"), @@ -367,7 +395,15 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" + }, + { + "name": "gravity-dex-farming-2", + "rate": "0.500000000000000000", + "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", + "destination_address": "cosmos1czyx0dj2yd26gv3stpxzv23ddy8pld4j6p90a683mdcg8vzy72jqa8tm6p", + "start_time": "2021-09-01T00:00:00Z", + "end_time": "2021-09-30T00:00:00Z" }, { "name": "gravity-dex-farming-3", @@ -379,7 +415,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { } ]`, }), - 2, + 3, 2, types.MustParseRFC3339("2021-10-01T00:00:00Z"), types.MustParseRFC3339("2021-10-01T00:00:00Z"), @@ -393,7 +429,7 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { }, }, { - "add budget 4 without date range overlap", + "add budget 4 without date range overlap, remove 2, 3", testProposal(proposal.ParamChange{ Subspace: types.ModuleName, Key: string(types.KeyBudgets), @@ -404,22 +440,22 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1qceyjmnrl6hapntjq3z25vn38nh68u7yxvufs2thptxvqm7huxeqj7zyrq", "start_time": "2021-09-01T00:00:00Z", - "end_time": "2031-09-30T00:00:00Z" + "end_time": "2021-12-31T00:00:00Z" }, { "name": "gravity-dex-farming-4", "rate": "1.000000000000000000", "source_address": "cosmos10wy60v3zuks7rkwnqxs3e878zqfhus6m98l77q6rppz40kxwgllsruc0az", "destination_address": "cosmos1e0n8jmeg4u8q3es2tmhz5zlte8a4q8687ndns8pj4q8grdl74a0sw3045s", - "start_time": "2031-09-30T00:00:01Z", - "end_time": "2031-12-10T00:00:00Z" + "start_time": "2021-12-31T00:00:00Z", + "end_time": "2022-12-31T00:00:00Z" } ]`, }), 2, 1, - types.MustParseRFC3339("2021-09-29T00:00:00Z"), - types.MustParseRFC3339("2021-09-30T00:00:00Z"), + types.MustParseRFC3339("2021-10-02T00:00:00Z"), + types.MustParseRFC3339("2021-10-14T00:00:00Z"), nil, []sdk.AccAddress{budgetSource, suite.destinationAddrs[0], suite.destinationAddrs[1], suite.destinationAddrs[2]}, []sdk.Coins{ @@ -475,10 +511,10 @@ func (suite *KeeperTestSuite) TestBudgetChangeSituation() { suite.ctx = suite.ctx.WithBlockHeight(int64(height)) suite.ctx = suite.ctx.WithBlockTime(tc.nextBlockTime) - params := suite.keeper.GetParams(suite.ctx) + params = suite.keeper.GetParams(suite.ctx) suite.Require().Len(params.Budgets, tc.budgetCount) for _, budget := range params.Budgets { - err := budget.Validate() + err = budget.Validate() suite.Require().NoError(err) } diff --git a/x/budget/keeper/keeper_test.go b/x/budget/keeper/keeper_test.go index 217a1c0..23c528f 100644 --- a/x/budget/keeper/keeper_test.go +++ b/x/budget/keeper/keeper_test.go @@ -3,7 +3,6 @@ package keeper_test import ( "testing" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" "github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params/types/proposal" @@ -29,7 +28,8 @@ var ( sdk.NewInt64Coin(denom1, 1_000_000_000), sdk.NewInt64Coin(denom2, 1_000_000_000), sdk.NewInt64Coin(denom3, 1_000_000_000)) - smallBalances = mustParseCoinsNormalized("1denom1,2denom2,3denom3,1000000000stake") + smallBalances = mustParseCoinsNormalized("1denom1,2denom2,3denom3,1000000000stake") + smallBalances2 = mustParseCoinsNormalized("1denom1,2denom2") ) type KeeperTestSuite struct { @@ -67,21 +67,23 @@ func (suite *KeeperTestSuite) SetupTest() { dAddr3 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "destinationAddr3") dAddr4 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "destinationAddr4") dAddr5 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "destinationAddr5") - dAddr6 := types.DeriveAddress(types.AddressType32Bytes, "farming", "GravityDEXFarmingBudget") + dAddr6 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "destinationAddr6") sAddr1 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr1") sAddr2 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr2") sAddr3 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr3") sAddr4 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr4") sAddr5 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr5") - sAddr6 := suite.app.AccountKeeper.GetModuleAccount(suite.ctx, authtypes.FeeCollectorName).GetAddress() + sAddr6 := types.DeriveAddress(types.AddressType32Bytes, types.ModuleName, "sourceAddr6") suite.destinationAddrs = []sdk.AccAddress{dAddr1, dAddr2, dAddr3, dAddr4, dAddr5, dAddr6} - suite.sourceAddrs = []sdk.AccAddress{sAddr1, sAddr2, sAddr3, sAddr4, sAddr5, sAddr6} + suite.sourceAddrs = []sdk.AccAddress{sAddr1, sAddr2, sAddr3, sAddr4, sAddr5, sAddr6, sAddr6} for _, addr := range append(suite.addrs, suite.sourceAddrs[:3]...) { err := simapp.FundAccount(suite.app.BankKeeper, suite.ctx, addr, initialBalances) suite.Require().NoError(err) } err := simapp.FundAccount(suite.app.BankKeeper, suite.ctx, suite.sourceAddrs[3], smallBalances) suite.Require().NoError(err) + err = simapp.FundAccount(suite.app.BankKeeper, suite.ctx, suite.sourceAddrs[4], smallBalances2) + suite.Require().NoError(err) suite.budgets = []types.Budget{ { @@ -133,12 +135,20 @@ func (suite *KeeperTestSuite) SetupTest() { EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, { - Name: "gravity-dex-farming-20213Q-20313Q", - Rate: sdk.MustNewDecFromStr("0.5"), + Name: "small2-source-budget", + Rate: sdk.MustNewDecFromStr("0.1"), + SourceAddress: suite.sourceAddrs[4].String(), + DestinationAddress: suite.destinationAddrs[4].String(), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), + }, + { + Name: "empty-source-budget", + Rate: sdk.MustNewDecFromStr("0.1"), SourceAddress: suite.sourceAddrs[5].String(), DestinationAddress: suite.destinationAddrs[5].String(), - StartTime: types.MustParseRFC3339("2021-09-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2031-09-30T00:00:00Z"), + StartTime: types.MustParseRFC3339("0000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), }, } } diff --git a/x/budget/module.go b/x/budget/module.go index 84f6dbb..46ce392 100644 --- a/x/budget/module.go +++ b/x/budget/module.go @@ -175,8 +175,8 @@ func (AppModule) GenerateGenesisState(simState *module.SimulationState) { // ProposalContents returns all the budget content functions used to // simulate governance proposals. -func (am AppModule) ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent { - return nil +func (am AppModule) ProposalContents(_ module.SimulationState) []simtypes.WeightedProposalContent { + return simulation.ProposalContents(am.keeper) } // RandomizedParams creates randomized budget param changes for the simulator. @@ -190,6 +190,6 @@ func (am AppModule) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) { } // WeightedOperations returns the all the gov module operations with their respective weights. -func (am AppModule) WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation { +func (am AppModule) WeightedOperations(_ module.SimulationState) []simtypes.WeightedOperation { return nil } diff --git a/x/budget/simulation/genesis.go b/x/budget/simulation/genesis.go index fd77aa8..991c545 100644 --- a/x/budget/simulation/genesis.go +++ b/x/budget/simulation/genesis.go @@ -27,17 +27,32 @@ func GenEpochBlocks(r *rand.Rand) uint32 { } // GenBudgets returns randomized budgets. -func GenBudgets(r *rand.Rand) []types.Budget { +func GenBudgets(r *rand.Rand, ctx sdk.Context, ak types.AccountKeeper, bk types.BankKeeper, accs []simtypes.Account) []types.Budget { ranBudgets := make([]types.Budget, 0) - for i := 0; i < simtypes.RandIntBetween(r, 1, 3); i++ { + for i := 0; i < simtypes.RandIntBetween(r, 1, 20); i++ { + var sourceAddr, destAddr sdk.AccAddress + if i%2 == 1 { + sourceAddr = types.DeriveAddress(types.AddressType20Bytes, "", "fee_collector") + } else { + simAccount, _ := simtypes.RandomAcc(r, accs) + sourceAddr = simAccount.Address + } + for { + simAccount, _ := simtypes.RandomAcc(r, accs) + if !simAccount.Address.Equals(sourceAddr) { + destAddr = simAccount.Address + break + } + } + budget := types.Budget{ Name: "simulation-test-" + simtypes.RandStringOfLength(r, 5), - Rate: sdk.NewDecFromIntWithPrec(sdk.NewInt(int64(simtypes.RandIntBetween(r, 1, 4))), 1), // 10~30% - SourceAddress: "cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta", // Cosmos Hub's FeeCollector module account - DestinationAddress: sdk.AccAddress(address.Module(types.ModuleName, []byte("GravityDEXFarmingBudget"))).String(), - StartTime: types.MustParseRFC3339("2000-01-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), + Rate: sdk.NewDecFromIntWithPrec(sdk.NewInt(int64(simtypes.RandIntBetween(r, 1, 5))), 2), // 1~5% + SourceAddress: sourceAddr.String(), // Cosmos Hub's FeeCollector module account + DestinationAddress: destAddr.String(), + StartTime: ctx.BlockTime(), + EndTime: ctx.BlockTime().AddDate(0, 0, simtypes.RandIntBetween(r, 1, 28)), } ranBudgets = append(ranBudgets, budget) } @@ -45,6 +60,17 @@ func GenBudgets(r *rand.Rand) []types.Budget { return ranBudgets } +func InitBudgets(r *rand.Rand) []types.Budget { + return []types.Budget{{ + Name: "simulation-test-" + simtypes.RandStringOfLength(r, 5), + Rate: sdk.NewDecFromIntWithPrec(sdk.NewInt(int64(simtypes.RandIntBetween(r, 1, 4))), 1), // 10~30% + SourceAddress: types.DeriveAddress(types.AddressType20Bytes, "", "fee_collector").String(), // Cosmos Hub's FeeCollector module account + DestinationAddress: sdk.AccAddress(address.Module(types.ModuleName, []byte("GravityDEXFarmingBudget"))).String(), + StartTime: types.MustParseRFC3339("2000-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("9999-12-31T00:00:00Z"), + }} +} + // RandomizedGenState generates a random GenesisState for budget. func RandomizedGenState(simState *module.SimulationState) { var epochBlocks uint32 @@ -56,7 +82,7 @@ func RandomizedGenState(simState *module.SimulationState) { simState.AppParams.GetOrGenerate( simState.Cdc, Budgets, &budgets, simState.Rand, - func(r *rand.Rand) { budgets = GenBudgets(r) }, + func(r *rand.Rand) { budgets = InitBudgets(r) }, ) budgetGenesis := types.GenesisState{ diff --git a/x/budget/simulation/genesis_test.go b/x/budget/simulation/genesis_test.go index 4fc0572..c2db748 100644 --- a/x/budget/simulation/genesis_test.go +++ b/x/budget/simulation/genesis_test.go @@ -40,7 +40,7 @@ func TestRandomizedGenState(t *testing.T) { var genState types.GenesisState simState.Cdc.MustUnmarshalJSON(simState.GenState[types.ModuleName], &genState) - require.Equal(t, sdk.MustNewDecFromStr("0.3"), genState.Params.Budgets[0].Rate) + require.Equal(t, sdk.MustNewDecFromStr("0.1"), genState.Params.Budgets[0].Rate) require.Equal(t, "cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta", genState.Params.Budgets[0].SourceAddress) require.Equal(t, "cosmos1ke7rn6vl3vmeasmcrxdm3pfrt37fsg5jfrex80pp3hvhwgu4h4usxgvk3e", genState.Params.Budgets[0].DestinationAddress) require.Equal(t, uint32(9), genState.Params.EpochBlocks) diff --git a/x/budget/simulation/params.go b/x/budget/simulation/params.go index 85caa66..41355d6 100644 --- a/x/budget/simulation/params.go +++ b/x/budget/simulation/params.go @@ -24,7 +24,7 @@ func ParamChanges(r *rand.Rand) []simtypes.ParamChange { ), simulation.NewSimParamChange(types.ModuleName, string(types.KeyBudgets), func(r *rand.Rand) string { - bz, err := json.Marshal(GenBudgets(r)) + bz, err := json.Marshal(InitBudgets(r)) if err != nil { panic(err) } diff --git a/x/budget/simulation/params_test.go b/x/budget/simulation/params_test.go index aca1ca8..9bcf09e 100644 --- a/x/budget/simulation/params_test.go +++ b/x/budget/simulation/params_test.go @@ -20,7 +20,7 @@ func TestParamChanges(t *testing.T) { subspace string }{ {"budget/EpochBlocks", "EpochBlocks", "6", "budget"}, - {"budget/Budgets", "Budgets", `[{"name":"simulation-test-MLxiD","rate":"0.300000000000000000","source_address":"cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta","destination_address":"cosmos1ke7rn6vl3vmeasmcrxdm3pfrt37fsg5jfrex80pp3hvhwgu4h4usxgvk3e","start_time":"2000-01-01T00:00:00Z","end_time":"9999-12-31T00:00:00Z"},{"name":"simulation-test-nhwJy","rate":"0.200000000000000000","source_address":"cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta","destination_address":"cosmos1ke7rn6vl3vmeasmcrxdm3pfrt37fsg5jfrex80pp3hvhwgu4h4usxgvk3e","start_time":"2000-01-01T00:00:00Z","end_time":"9999-12-31T00:00:00Z"}]`, "budget"}, + {"budget/Budgets", "Budgets", `[{"name":"simulation-test-FpXzp","rate":"0.300000000000000000","source_address":"cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta","destination_address":"cosmos1ke7rn6vl3vmeasmcrxdm3pfrt37fsg5jfrex80pp3hvhwgu4h4usxgvk3e","start_time":"2000-01-01T00:00:00Z","end_time":"9999-12-31T00:00:00Z"}]`, "budget"}, } paramChanges := simulation.ParamChanges(r) diff --git a/x/budget/simulation/proposals.go b/x/budget/simulation/proposals.go new file mode 100644 index 0000000..7217a1d --- /dev/null +++ b/x/budget/simulation/proposals.go @@ -0,0 +1,43 @@ +package simulation + +import ( + "math/rand" + + sdk "github.com/cosmos/cosmos-sdk/types" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + + "github.com/tendermint/budget/app/params" + "github.com/tendermint/budget/x/budget/keeper" +) + +// Simulation operation weights constants. +const ( + OpWeightSimulateUpdateBudgetPlans = "op_weight_update_budget_plans" +) + +// ProposalContents defines the module weighted proposals' contents for mocking param changes, other actions with keeper +func ProposalContents(k keeper.Keeper) []simtypes.WeightedProposalContent { + return []simtypes.WeightedProposalContent{ + simulation.NewWeightedProposalContent( + OpWeightSimulateUpdateBudgetPlans, + params.DefaultWeightUpdateBudgetPlans, + SimulateUpdateBudgetPlans(k), + ), + } +} + +// SimulateUpdateBudgetPlans generates random update budget plans param change proposal content. +func SimulateUpdateBudgetPlans(k keeper.Keeper) simtypes.ContentSimulatorFn { + return func(r *rand.Rand, ctx sdk.Context, accs []simtypes.Account) simtypes.Content { + params := k.GetParams(ctx) + + params.Budgets = GenBudgets(r, ctx, nil, nil, accs) + params.EpochBlocks = GenEpochBlocks(r) + + // manually set params for simulation + k.SetParams(ctx, params) + + return nil + } +} diff --git a/x/budget/spec/01_concepts.md b/x/budget/spec/01_concepts.md index cdeb43f..1fe3f79 100644 --- a/x/budget/spec/01_concepts.md +++ b/x/budget/spec/01_concepts.md @@ -57,17 +57,21 @@ The Cosmos SDK current reward workflow: Implementation with `x/budget` module: - - A budget module is 100% independent of all other Cosmos SDK modules + - A budget module is independent of all other Cosmos SDK modules - - BeginBlock processing order is: + - In chains that where there will be budget plans with `SourceAddress` set to `FeeCollectorName`, it should be set as follows: - - mint module → budget module → distribution module + - BeginBlock processing order should be mint module → budget module → distribution module + + - if inflation and gas fees occur every block, `params.EpochBlocks` should be set to 1 + + - It should be noted that if the rate sum of these budget plans is 1.0 (100%), inflation and gas fees can not go to validators - Distribute ATOM inflation and transaction gas fees to different budget purposes: - ATOM inflation and gas fees are accumulated in `FeeCollectorName` module account - - Distribute budget amounts from `FeeCollectorName` module account to each budget pool module account + - Distribute budget amounts from `FeeCollectorName` module account to each `DestinationAddress` for budget plans with `SourceAddress` set to `FeeCollectorName` - Remaining amounts stay in `FeeCollectorName` so that distribution module can use them for community fund and staking rewards distribution (no change to current `FeeCollectorName` implementation) diff --git a/x/budget/spec/05_events.md b/x/budget/spec/05_events.md index 7500eb5..acab187 100644 --- a/x/budget/spec/05_events.md +++ b/x/budget/spec/05_events.md @@ -9,9 +9,6 @@ The budget module emits the following events. ### Budget Collection Result for Each Budget on This Block | Type | Attribute Key | Attribute Value | -| ---------------- | ------------------- | -------------------- | +|------------------|---------------------|----------------------| | budget_collected | name | {budgetName} | -| budget_collected | destination_address | {destinationAddress} | -| budget_collected | source_address | {sourceAddress} | -| budget_collected | rate | {budgetRate} | | budget_collected | amount | {collectedAmount} | diff --git a/x/budget/spec/06_params.md b/x/budget/spec/06_params.md index 240e0b1..643c925 100644 --- a/x/budget/spec/06_params.md +++ b/x/budget/spec/06_params.md @@ -5,9 +5,9 @@ The budget module contains the following parameters: -| Key | Type | Example | -| ----------- | -------- | ------------------------------------------------------------------------------------ | -| EpochBlocks | uint32 | {"epoch_blocks":1} | +| Key | Type | Example | +|-------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| EpochBlocks | uint32 | {"epoch_blocks":1} | | Budgets | []Budget | {"budgets":[{"name":"liquidity-farming-20213Q-20221Q","rate":"0.300000000000000000","source_address":"cosmos17xpfvakm2amg962yls6f84z3kell8c5lserqta","destination_address":"cosmos1228ryjucdpdv3t87rxle0ew76a56ulvnfst0hq0sscd3nafgjpqqkcxcky","start_time":"2021-10-01T00:00:00Z","end_time":"2022-04-01T00:00:00Z"}]} | ## EpochBlocks diff --git a/x/budget/types/budget.go b/x/budget/types/budget.go index ffda8a4..c8097f3 100644 --- a/x/budget/types/budget.go +++ b/x/budget/types/budget.go @@ -45,8 +45,12 @@ func (budget Budget) Validate() error { return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid source address %s: %v", budget.SourceAddress, err) } + if budget.DestinationAddress == budget.SourceAddress { + return sdkerrors.Wrapf(ErrSameSourceDestinationAddr, "budget source address and destination address cannot be the same %s", budget.SourceAddress) + } + if !budget.EndTime.After(budget.StartTime) { - return ErrInvalidStartEndTime + return sdkerrors.Wrapf(ErrInvalidStartEndTime, "end time %s must be greater than start time %s", budget.EndTime, budget.StartTime) } if !budget.Rate.IsPositive() { @@ -60,7 +64,7 @@ func (budget Budget) Validate() error { // Collectible validates the budget has reached its start time and that the end time has not elapsed. func (budget Budget) Collectible(blockTime time.Time) bool { - return !budget.StartTime.After(blockTime) && budget.EndTime.After(blockTime) + return DateRangeIncludes(budget.StartTime, budget.EndTime, blockTime) } // CollectibleBudgets returns only the valid and started and not expired budgets based on the given block time. diff --git a/x/budget/types/budget_test.go b/x/budget/types/budget_test.go new file mode 100644 index 0000000..267d334 --- /dev/null +++ b/x/budget/types/budget_test.go @@ -0,0 +1,246 @@ +package types_test + +import ( + "strconv" + "testing" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/address" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/stretchr/testify/require" + + "github.com/tendermint/budget/x/budget/types" +) + +var ( + dAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("destinationAddr1"))) + dAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("destinationAddr2"))) + sAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("sourceAddr1"))) + sAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("sourceAddr2"))) + budgets = []types.Budget{ + { + Name: "budget0", + Rate: sdk.OneDec(), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-03T00:00:00Z"), + }, + { + Name: "budget1", + Rate: sdk.OneDec(), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), + }, + { + Name: "budget2", + Rate: sdk.MustNewDecFromStr("0.1"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), + }, + { + Name: "budget3", + Rate: sdk.MustNewDecFromStr("0.1"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-10T00:00:00Z"), + }, + { + Name: "budget4", + Rate: sdk.OneDec(), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-20T00:00:00Z"), + }, + { + Name: "budget5", + Rate: sdk.MustNewDecFromStr("0.1"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + { + Name: "budget6", + Rate: sdk.MustNewDecFromStr("1.0"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-26T00:00:00Z"), + }, + } +) + +func TestValidateBudgets(t *testing.T) { + testCases := []struct { + budgets []types.Budget + expectedError error + }{ + { + []types.Budget{budgets[0], budgets[1]}, + nil, + }, + { + []types.Budget{budgets[0], budgets[1], budgets[2]}, + types.ErrInvalidTotalBudgetRate, + }, + { + []types.Budget{budgets[1], budgets[4]}, + nil, + }, + { + []types.Budget{budgets[4], budgets[5]}, + types.ErrInvalidTotalBudgetRate, + }, + { + []types.Budget{budgets[3], budgets[3]}, + types.ErrDuplicateBudgetName, + }, + { + []types.Budget{ + { + Name: "same-source-destination-addr", + Rate: sdk.MustNewDecFromStr("0.1"), + SourceAddress: sAddr2.String(), + DestinationAddress: sAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + }, + types.ErrSameSourceDestinationAddr, + }, + { + []types.Budget{ + { + Name: "over-1-rate", + Rate: sdk.MustNewDecFromStr("1.01"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + }, + types.ErrInvalidBudgetRate, + }, + { + []types.Budget{ + { + Name: "not-positive-rate", + Rate: sdk.MustNewDecFromStr("-0.01"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + }, + types.ErrInvalidBudgetRate, + }, + { + []types.Budget{ + { + Name: "invalid budget name", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + }, + types.ErrInvalidBudgetName, + }, + { + []types.Budget{ + { + Name: "invalid-destination-addr", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr2.String(), + DestinationAddress: "invalidaddr", + StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), + }, + }, + sdkerrors.ErrInvalidAddress, + }, + { + []types.Budget{ + { + Name: "invalid-start-time", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr2.String(), + DestinationAddress: dAddr2.String(), + StartTime: types.MustParseRFC3339("2021-08-20T00:00:00Z"), + EndTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), + }, + }, + types.ErrInvalidStartEndTime, + }, + } + + for i, tc := range testCases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + err := types.ValidateBudgets(tc.budgets) + if tc.expectedError == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, tc.expectedError) + } + }) + } +} + +func TestCollectibleBudgets(t *testing.T) { + testCases := []struct { + budgets []types.Budget + blockTime time.Time + expectedLen int + }{ + { + []types.Budget{budgets[0], budgets[1]}, + types.MustParseRFC3339("2021-07-05T00:00:00Z"), + 1, + }, + { + []types.Budget{budgets[0], budgets[1], budgets[2]}, + types.MustParseRFC3339("2021-07-05T00:00:00Z"), + 2, + }, + { + []types.Budget{budgets[4], budgets[5]}, + types.MustParseRFC3339("2021-08-18T00:00:00Z"), + 1, + }, + { + []types.Budget{budgets[4], budgets[5]}, + types.MustParseRFC3339("2021-08-19T00:00:00Z"), + 2, + }, + { + []types.Budget{budgets[4], budgets[5]}, + types.MustParseRFC3339("2021-08-20T00:00:00Z"), + 1, + }, + { + []types.Budget{budgets[5], budgets[6]}, + types.MustParseRFC3339("2021-08-25T00:00:00Z"), + 1, + }, + { + []types.Budget{}, + types.MustParseRFC3339("2021-08-20T00:00:00Z"), + 0, + }, + } + + for i, tc := range testCases { + t.Run(strconv.Itoa(i), func(t *testing.T) { + require.Len(t, types.CollectibleBudgets(tc.budgets, tc.blockTime), tc.expectedLen) + }) + } +} diff --git a/x/budget/types/errors.go b/x/budget/types/errors.go index 4299304..14c40b0 100644 --- a/x/budget/types/errors.go +++ b/x/budget/types/errors.go @@ -6,9 +6,10 @@ import ( // Sentinel errors for the budget module. var ( - ErrInvalidBudgetName = sdkerrors.Register(ModuleName, 2, "budget name only allows letters, digits, and dash(-) without spaces and the maximum length is 50") - ErrInvalidStartEndTime = sdkerrors.Register(ModuleName, 3, "budget end time must be after the start time") - ErrInvalidBudgetRate = sdkerrors.Register(ModuleName, 4, "invalid budget rate") - ErrInvalidTotalBudgetRate = sdkerrors.Register(ModuleName, 5, "invalid total rate of the budgets with the same source address") - ErrDuplicateBudgetName = sdkerrors.Register(ModuleName, 6, "duplicate budget name") + ErrInvalidBudgetName = sdkerrors.Register(ModuleName, 2, "budget name only allows letters, digits, and dash(-) without spaces and the maximum length is 50") + ErrInvalidStartEndTime = sdkerrors.Register(ModuleName, 3, "budget end time must be after the start time") + ErrInvalidBudgetRate = sdkerrors.Register(ModuleName, 4, "invalid budget rate") + ErrInvalidTotalBudgetRate = sdkerrors.Register(ModuleName, 5, "invalid total rate of the budgets with the same source address") + ErrDuplicateBudgetName = sdkerrors.Register(ModuleName, 6, "duplicate budget name") + ErrSameSourceDestinationAddr = sdkerrors.Register(ModuleName, 7, "budget source address and destination address cannot be the same") ) diff --git a/x/budget/types/events.go b/x/budget/types/events.go index 08aa5e5..68f298a 100644 --- a/x/budget/types/events.go +++ b/x/budget/types/events.go @@ -4,9 +4,6 @@ package types const ( EventTypeBudgetCollected = "budget_collected" - AttributeValueName = "name" - AttributeValueDestinationAddress = "destination_address" - AttributeValueSourceAddress = "source_address" - AttributeValueRate = "rate" - AttributeValueAmount = "amount" + AttributeValueName = "name" + AttributeValueAmount = "amount" ) diff --git a/x/budget/types/expected_keepers.go b/x/budget/types/expected_keepers.go index 396e254..1def188 100644 --- a/x/budget/types/expected_keepers.go +++ b/x/budget/types/expected_keepers.go @@ -9,12 +9,11 @@ import ( // BankKeeper defines the expected bank send keeper type BankKeeper interface { InputOutputCoins(ctx sdk.Context, inputs []banktypes.Input, outputs []banktypes.Output) error - GetAllBalances(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins + SpendableCoins(ctx sdk.Context, addr sdk.AccAddress) sdk.Coins } // AccountKeeper defines the expected account keeper type AccountKeeper interface { GetModuleAddress(name string) sdk.AccAddress GetModuleAccount(ctx sdk.Context, moduleName string) authtypes.ModuleAccountI - SetModuleAccount(sdk.Context, authtypes.ModuleAccountI) } diff --git a/x/budget/types/params.go b/x/budget/types/params.go index 75aa450..86b3a24 100644 --- a/x/budget/types/params.go +++ b/x/budget/types/params.go @@ -59,6 +59,7 @@ func (p Params) Validate() error { validator func(interface{}) error }{ {p.Budgets, ValidateBudgets}, + {p.EpochBlocks, ValidateEpochBlocks}, } { if err := v.validator(v.value); err != nil { return err @@ -69,10 +70,10 @@ func (p Params) Validate() error { // ValidateBudgets validates budget name and total rate. // The total rate of budgets with the same source address must not exceed 1. -func ValidateBudgets(i interface{}) error { - budgets, ok := i.([]Budget) +func ValidateBudgets(input interface{}) error { + budgets, ok := input.([]Budget) if !ok { - return fmt.Errorf("invalid parameter type: %T", i) + return fmt.Errorf("invalid parameter type: %T", input) } names := make(map[string]bool) for _, budget := range budgets { @@ -90,10 +91,10 @@ func ValidateBudgets(i interface{}) error { if budgetsBySource.TotalRate.GT(sdk.OneDec()) { // If the TotalRate of Budgets with the same source address exceeds 1, // recalculate and verify the TotalRate of Budgets with overlapping time ranges. - for _, budget := range budgetsBySource.Budgets { - totalRate := sdk.ZeroDec() - for _, budgetToCheck := range budgetsBySource.Budgets { - if DateRangesOverlap(budget.StartTime, budget.EndTime, budgetToCheck.StartTime, budgetToCheck.EndTime) { + for i, budget := range budgetsBySource.Budgets { + totalRate := budget.Rate + for j, budgetToCheck := range budgetsBySource.Budgets { + if i != j && budgetToCheck.Collectible(budget.StartTime) { totalRate = totalRate.Add(budgetToCheck.Rate) } } diff --git a/x/budget/types/params_test.go b/x/budget/types/params_test.go index 477d593..d77ee03 100644 --- a/x/budget/types/params_test.go +++ b/x/budget/types/params_test.go @@ -1,77 +1,21 @@ package types_test import ( + "reflect" "testing" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/types/address" paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" "github.com/tendermint/budget/x/budget/types" ) -var ( - dAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("destinationAddr1"))) - dAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("destinationAddr2"))) - sAddr1 = sdk.AccAddress(address.Module(types.ModuleName, []byte("sourceAddr1"))) - sAddr2 = sdk.AccAddress(address.Module(types.ModuleName, []byte("sourceAddr2"))) - budgets = []types.Budget{ - { - Name: "test", - Rate: sdk.OneDec(), - SourceAddress: sAddr1.String(), - DestinationAddress: dAddr1.String(), - StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-08-03T00:00:00Z"), - }, - { - Name: "test1", - Rate: sdk.OneDec(), - SourceAddress: sAddr2.String(), - DestinationAddress: dAddr2.String(), - StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), - }, - { - Name: "test2", - Rate: sdk.MustNewDecFromStr("0.1"), - SourceAddress: sAddr2.String(), - DestinationAddress: dAddr2.String(), - StartTime: types.MustParseRFC3339("2021-07-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-07-10T00:00:00Z"), - }, - { - Name: "test3", - Rate: sdk.MustNewDecFromStr("0.1"), - SourceAddress: sAddr2.String(), - DestinationAddress: dAddr2.String(), - StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-08-10T00:00:00Z"), - }, - { - Name: "test4", - Rate: sdk.OneDec(), - SourceAddress: sAddr2.String(), - DestinationAddress: dAddr2.String(), - StartTime: types.MustParseRFC3339("2021-08-01T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-08-20T00:00:00Z"), - }, - { - Name: "test5", - Rate: sdk.MustNewDecFromStr("0.1"), - SourceAddress: sAddr2.String(), - DestinationAddress: dAddr2.String(), - StartTime: types.MustParseRFC3339("2021-08-19T00:00:00Z"), - EndTime: types.MustParseRFC3339("2021-08-25T00:00:00Z"), - }, - } -) - -func TestParams(t *testing.T) { +func TestDefaultParams(t *testing.T) { require.IsType(t, paramstypes.KeyTable{}, types.ParamKeyTable()) defaultParams := types.DefaultParams() + require.NoError(t, defaultParams.Validate()) paramsStr := `epoch_blocks: 1 budgets: [] @@ -79,40 +23,6 @@ budgets: [] require.Equal(t, paramsStr, defaultParams.String()) } -func TestValidateBudgets(t *testing.T) { - err := types.ValidateBudgets([]types.Budget{budgets[0], budgets[1]}) - require.NoError(t, err) - - err = types.ValidateBudgets([]types.Budget{budgets[0], budgets[1], budgets[2]}) - require.ErrorIs(t, err, types.ErrInvalidTotalBudgetRate) - - err = types.ValidateBudgets([]types.Budget{budgets[1], budgets[4]}) - require.NoError(t, err) - - err = types.ValidateBudgets([]types.Budget{budgets[4], budgets[5]}) - require.ErrorIs(t, err, types.ErrInvalidTotalBudgetRate) - - err = types.ValidateBudgets([]types.Budget{budgets[3], budgets[3]}) - require.ErrorIs(t, err, types.ErrDuplicateBudgetName) -} - -func TestCollectibleBudgets(t *testing.T) { - collectibleBudgets := types.CollectibleBudgets([]types.Budget{budgets[0], budgets[1]}, types.MustParseRFC3339("2021-07-05T00:00:00Z")) - require.Len(t, collectibleBudgets, 1) - - collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[0], budgets[1], budgets[2]}, types.MustParseRFC3339("2021-07-05T00:00:00Z")) - require.Len(t, collectibleBudgets, 2) - - collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-18T00:00:00Z")) - require.Len(t, collectibleBudgets, 1) - - collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-19T00:00:00Z")) - require.Len(t, collectibleBudgets, 2) - - collectibleBudgets = types.CollectibleBudgets([]types.Budget{budgets[4], budgets[5]}, types.MustParseRFC3339("2021-08-20T00:00:00Z")) - require.Len(t, collectibleBudgets, 1) -} - func TestValidateEpochBlocks(t *testing.T) { err := types.ValidateEpochBlocks(uint32(0)) require.NoError(t, err) @@ -126,3 +36,132 @@ func TestValidateEpochBlocks(t *testing.T) { err = types.ValidateEpochBlocks(10000000000000000) require.EqualError(t, err, "invalid parameter type: int") } + +func TestParamsValidate(t *testing.T) { + testCases := []struct { + name string + configure func(*types.Params) + expectedErr string + }{ + { + "valid independent time budgets", + func(params *types.Params) { + params.Budgets = []types.Budget{ + { + Name: "budget1-2", + Rate: sdk.MustNewDecFromStr("1.0"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-02T00:00:00Z"), + }, + { + Name: "budget3-4", + Rate: sdk.MustNewDecFromStr("1.0"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-03T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-04T00:00:00Z"), + }, + } + }, + "", + }, + { + "valid transition budgets", + func(params *types.Params) { + params.Budgets = []types.Budget{ + { + Name: "budget1-4", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-04T00:00:00Z"), + }, + { + Name: "budget1-2", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-02T00:00:00Z"), + }, + { + Name: "budget3-4", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-03T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-04T00:00:00Z"), + }, + } + }, + "", + }, + { + "overlapped with over 1 total rate budgets", + func(params *types.Params) { + params.Budgets = []types.Budget{ + { + Name: "budget1-4", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-04T00:00:00Z"), + }, + { + Name: "budget1-2", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-01T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-02T00:00:00Z"), + }, + { + Name: "budget3-4", + Rate: sdk.MustNewDecFromStr("0.5"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-03T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-04T00:00:00Z"), + }, + { + Name: "budget3-5", + Rate: sdk.MustNewDecFromStr("0.001"), + SourceAddress: sAddr1.String(), + DestinationAddress: dAddr1.String(), + StartTime: types.MustParseRFC3339("2022-01-03T00:00:00Z"), + EndTime: types.MustParseRFC3339("2022-01-05T00:00:00Z"), + }, + } + }, + "total rate for source address cosmos1g6umphvhteymdm6n2arju4q2h0d8c78p2t7p4tjadlcw98w6ylrqfpwqex must not exceed 1: 1.001000000000000000: invalid total rate of the budgets with the same source address", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + params := types.DefaultParams() + tc.configure(¶ms) + err := params.Validate() + + var err2 error + for _, p := range params.ParamSetPairs() { + err := p.ValidatorFn(reflect.ValueOf(p.Value).Elem().Interface()) + if err != nil { + err2 = err + break + } + } + if tc.expectedErr != "" { + require.EqualError(t, err, tc.expectedErr) + require.EqualError(t, err2, tc.expectedErr) + } else { + require.Nil(t, err) + require.Nil(t, err2) + } + }) + } +} diff --git a/x/budget/types/utils.go b/x/budget/types/utils.go index 40ae989..ad22954 100644 --- a/x/budget/types/utils.go +++ b/x/budget/types/utils.go @@ -23,6 +23,12 @@ func DateRangesOverlap(startTimeA, endTimeA, startTimeB, endTimeB time.Time) boo return startTimeA.Before(endTimeB) && endTimeA.After(startTimeB) } +// DateRangeIncludes returns true if the target date included on the start, end time range. +// End time is exclusive and start time is inclusive. +func DateRangeIncludes(startTime, endTime, targetTime time.Time) bool { + return endTime.After(targetTime) && !startTime.After(targetTime) +} + // DeriveAddress derives an address with the given address length type, module name, and // address derivation name. It is used to derive source or destination address. func DeriveAddress(addressType AddressType, moduleName, name string) sdk.AccAddress { diff --git a/x/budget/types/utils_test.go b/x/budget/types/utils_test.go index ee4b91c..c9bd471 100644 --- a/x/budget/types/utils_test.go +++ b/x/budget/types/utils_test.go @@ -85,6 +85,57 @@ func TestDateRangesOverlap(t *testing.T) { } } +func TestDateRangeIncludes(t *testing.T) { + testCases := []struct { + name string + expectedResult bool + targeTime time.Time + startTime time.Time + endTime time.Time + }{ + { + "not included, before started", + false, + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:01Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "not included, after ended", + false, + types.MustParseRFC3339("2021-12-03T00:00:01Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "included on start time", + true, + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-03T00:00:00Z"), + }, + { + "not included on end time", + false, + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-01T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + }, + { + "not included on same start time and end time", + false, + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + types.MustParseRFC3339("2021-12-02T00:00:00Z"), + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expectedResult, types.DateRangeIncludes(tc.startTime, tc.endTime, tc.targeTime)) + }) + } +} + func TestDeriveAddress(t *testing.T) { testCases := []struct { addressType types.AddressType