diff --git a/x/farming/abci.go b/x/farming/abci.go index 7edd7d7c..5080f126 100644 --- a/x/farming/abci.go +++ b/x/farming/abci.go @@ -3,8 +3,6 @@ package farming import ( "time" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/cosmos/cosmos-sdk/telemetry" sdk "github.com/cosmos/cosmos-sdk/types" @@ -12,12 +10,20 @@ import ( "github.com/tendermint/farming/x/farming/types" ) -// BeginBlocker distribute farming rewards for the previous block -func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { - defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyBeginBlocker) -} - -func EndBlocker(ctx sdk.Context, k keeper.Keeper) []abci.ValidatorUpdate { +func EndBlocker(ctx sdk.Context, k keeper.Keeper) { defer telemetry.ModuleMeasureSince(types.ModuleName, time.Now(), telemetry.MetricKeyEndBlocker) - return []abci.ValidatorUpdate{} + + lastEpochTime, found := k.GetLastEpochTime(ctx) + if !found { + k.SetLastEpochTime(ctx, ctx.BlockTime()) + } else if ctx.BlockTime().Day()-lastEpochTime.Day() > 0 { + if err := k.DistributeRewards(ctx); err != nil { + panic(err) + } + k.ProcessQueuedCoins(ctx) + + k.SetLastEpochTime(ctx, ctx.BlockTime()) + } + + // TODO: implement plan termination logic } diff --git a/x/farming/keeper/epoch.go b/x/farming/keeper/epoch.go new file mode 100644 index 00000000..4672e54d --- /dev/null +++ b/x/farming/keeper/epoch.go @@ -0,0 +1,36 @@ +package keeper + +import ( + "time" + + gogotypes "github.com/gogo/protobuf/types" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/tendermint/farming/x/farming/types" +) + +func (k Keeper) GetLastEpochTime(ctx sdk.Context) (time.Time, bool) { + store := ctx.KVStore(k.storeKey) + bz := store.Get(types.GlobalLastEpochTimeKey) + if bz == nil { + return time.Time{}, false + } + var ts gogotypes.Timestamp + k.cdc.MustUnmarshal(bz, &ts) + t, err := gogotypes.TimestampFromProto(&ts) + if err != nil { + panic(err) + } + return t, true +} + +func (k Keeper) SetLastEpochTime(ctx sdk.Context, t time.Time) { + store := ctx.KVStore(k.storeKey) + ts, err := gogotypes.TimestampProto(t) + if err != nil { + panic(err) + } + bz := k.cdc.MustMarshal(ts) + store.Set(types.GlobalLastEpochTimeKey, bz) +} diff --git a/x/farming/keeper/epoch_test.go b/x/farming/keeper/epoch_test.go new file mode 100644 index 00000000..391db020 --- /dev/null +++ b/x/farming/keeper/epoch_test.go @@ -0,0 +1,13 @@ +package keeper_test + +func (suite *KeeperTestSuite) TestLastEpochTime() { + _, found := suite.keeper.GetLastEpochTime(suite.ctx) + suite.Require().False(found) + + t := mustParseRFC3339("2021-07-23T05:01:02Z") + suite.keeper.SetLastEpochTime(suite.ctx, t) + + t2, found := suite.keeper.GetLastEpochTime(suite.ctx) + suite.Require().True(found) + suite.Require().Equal(t, t2) +} diff --git a/x/farming/keeper/keeper.go b/x/farming/keeper/keeper.go index e4a77b8b..0746ca53 100644 --- a/x/farming/keeper/keeper.go +++ b/x/farming/keeper/keeper.go @@ -3,11 +3,11 @@ package keeper import ( "fmt" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/tendermint/tendermint/libs/log" "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/tendermint/farming/x/farming/types" diff --git a/x/farming/keeper/keeper_test.go b/x/farming/keeper/keeper_test.go index 8b1f7a4a..c60fed1f 100644 --- a/x/farming/keeper/keeper_test.go +++ b/x/farming/keeper/keeper_test.go @@ -1,19 +1,80 @@ package keeper_test import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" tmproto "github.com/tendermint/tendermint/proto/tendermint/types" sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/tendermint/farming/app" + simapp "github.com/tendermint/farming/app" + "github.com/tendermint/farming/x/farming/keeper" "github.com/tendermint/farming/x/farming/types" ) -// createTestApp returns a farming app with custom FarmingKeeper. -func createTestApp(isCheckTx bool) (*app.FarmingApp, sdk.Context) { - app := app.Setup(isCheckTx) - ctx := app.BaseApp.NewContext(isCheckTx, tmproto.Header{}) - app.FarmingKeeper.SetParams(ctx, types.DefaultParams()) +const ( + denom1 = "denom1" + denom2 = "denom2" + denom3 = "denom3" +) + +var ( + initialBalances = sdk.NewCoins( + sdk.NewInt64Coin(sdk.DefaultBondDenom, 1_000_000_000), + sdk.NewInt64Coin(denom1, 1_000_000_000), + sdk.NewInt64Coin(denom2, 1_000_000_000), + sdk.NewInt64Coin(denom3, 1_000_000_000)) +) + +type KeeperTestSuite struct { + suite.Suite + + app *simapp.FarmingApp + ctx sdk.Context + keeper keeper.Keeper + addrs []sdk.AccAddress +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} + +func (suite *KeeperTestSuite) SetupTest() { + app := simapp.Setup(false) + ctx := app.BaseApp.NewContext(false, tmproto.Header{}) + + suite.app = app + suite.ctx = ctx + suite.keeper = suite.app.FarmingKeeper + suite.addrs = simapp.AddTestAddrs(suite.app, suite.ctx, 4, sdk.ZeroInt()) + for _, addr := range suite.addrs { + err := simapp.FundAccount(suite.app.BankKeeper, suite.ctx, addr, initialBalances) + suite.Require().NoError(err) + } +} + +// Stake is a convenient method to test Keeper.Stake. +func (suite *KeeperTestSuite) Stake(farmer sdk.AccAddress, amt sdk.Coins) types.Staking { + staking, err := suite.keeper.Stake(suite.ctx, farmer, amt) + suite.Require().NoError(err) + + return staking +} + +func intEq(exp, got sdk.Int) (bool, string, string, string) { + return exp.Equal(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() +} + +func coinsEq(exp, got sdk.Coins) (bool, string, string, string) { + return exp.IsEqual(got), "expected:\t%v\ngot:\t\t%v", exp.String(), got.String() +} - return app, ctx +func mustParseRFC3339(s string) time.Time { + t, err := time.Parse(time.RFC3339, s) + if err != nil { + panic(err) + } + return t } diff --git a/x/farming/keeper/msg_server.go b/x/farming/keeper/msg_server.go index dd9ac073..76a7c301 100644 --- a/x/farming/keeper/msg_server.go +++ b/x/farming/keeper/msg_server.go @@ -29,7 +29,7 @@ var _ types.MsgServer = msgServer{} func (k msgServer) CreateFixedAmountPlan(goCtx context.Context, msg *types.MsgCreateFixedAmountPlan) (*types.MsgCreateFixedAmountPlanResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if err := k.Keeper.CreateFixedAmountPlan(ctx, msg, types.PlanTypePrivate); err != nil { + if _, err := k.Keeper.CreateFixedAmountPlan(ctx, msg, types.PlanTypePrivate); err != nil { return nil, err } @@ -40,7 +40,7 @@ func (k msgServer) CreateFixedAmountPlan(goCtx context.Context, msg *types.MsgCr func (k msgServer) CreateRatioPlan(goCtx context.Context, msg *types.MsgCreateRatioPlan) (*types.MsgCreateRatioPlanResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if err := k.Keeper.CreateRatioPlan(ctx, msg, types.PlanTypePrivate); err != nil { + if _, err := k.Keeper.CreateRatioPlan(ctx, msg, types.PlanTypePrivate); err != nil { return nil, err } @@ -51,7 +51,7 @@ func (k msgServer) CreateRatioPlan(goCtx context.Context, msg *types.MsgCreateRa func (k msgServer) Stake(goCtx context.Context, msg *types.MsgStake) (*types.MsgStakeResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if err := k.Keeper.Stake(ctx, msg.GetFarmer(), msg.StakingCoins); err != nil { + if _, err := k.Keeper.Stake(ctx, msg.GetFarmer(), msg.StakingCoins); err != nil { return nil, err } @@ -62,7 +62,7 @@ func (k msgServer) Stake(goCtx context.Context, msg *types.MsgStake) (*types.Msg func (k msgServer) Unstake(goCtx context.Context, msg *types.MsgUnstake) (*types.MsgUnstakeResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) - if err := k.Keeper.Unstake(ctx, msg.GetFarmer(), msg.UnstakingCoins); err != nil { + if _, err := k.Keeper.Unstake(ctx, msg.GetFarmer(), msg.UnstakingCoins); err != nil { return nil, err } diff --git a/x/farming/keeper/plan.go b/x/farming/keeper/plan.go index 3fa52d23..8c2906b0 100644 --- a/x/farming/keeper/plan.go +++ b/x/farming/keeper/plan.go @@ -163,11 +163,11 @@ func (k Keeper) UnmarshalPlan(bz []byte) (plan types.PlanI, err error) { } // CreateFixedAmountPlan sets fixed amount plan. -func (k Keeper) CreateFixedAmountPlan(ctx sdk.Context, msg *types.MsgCreateFixedAmountPlan, typ types.PlanType) error { +func (k Keeper) CreateFixedAmountPlan(ctx sdk.Context, msg *types.MsgCreateFixedAmountPlan, typ types.PlanType) (types.PlanI, error) { nextId := k.GetNextPlanIDWithUpdate(ctx) farmingPoolAddrAcc, err := sdk.AccAddressFromBech32(msg.FarmingPoolAddress) if err != nil { - return err + return nil, err } terminationAddrAcc := farmingPoolAddrAcc @@ -176,16 +176,16 @@ func (k Keeper) CreateFixedAmountPlan(ctx sdk.Context, msg *types.MsgCreateFixed balances := k.bankKeeper.GetAllBalances(ctx, farmingPoolAddrAcc) _, hasNeg := balances.SafeSub(params.PrivatePlanCreationFee) if hasNeg { - return sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "insufficient balance to pay private plan creation fee") + return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "insufficient balance to pay private plan creation fee") } farmingFeeCollectorAcc, err := sdk.AccAddressFromBech32(params.FarmingFeeCollector) if err != nil { - return err + return nil, err } if err := k.bankKeeper.SendCoins(ctx, farmingPoolAddrAcc, farmingFeeCollectorAcc, params.PrivatePlanCreationFee); err != nil { - return err + return nil, err } basePlan := types.NewBasePlan( @@ -213,15 +213,15 @@ func (k Keeper) CreateFixedAmountPlan(ctx sdk.Context, msg *types.MsgCreateFixed ), }) - return nil + return fixedPlan, nil } // CreateRatioPlan sets ratio plan. -func (k Keeper) CreateRatioPlan(ctx sdk.Context, msg *types.MsgCreateRatioPlan, typ types.PlanType) error { +func (k Keeper) CreateRatioPlan(ctx sdk.Context, msg *types.MsgCreateRatioPlan, typ types.PlanType) (types.PlanI, error) { nextId := k.GetNextPlanIDWithUpdate(ctx) farmingPoolAddrAcc, err := sdk.AccAddressFromBech32(msg.FarmingPoolAddress) if err != nil { - return err + return nil, err } terminationAddrAcc := farmingPoolAddrAcc @@ -230,16 +230,16 @@ func (k Keeper) CreateRatioPlan(ctx sdk.Context, msg *types.MsgCreateRatioPlan, balances := k.bankKeeper.GetAllBalances(ctx, farmingPoolAddrAcc) _, hasNeg := balances.SafeSub(params.PrivatePlanCreationFee) if hasNeg { - return sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "insufficient balance to pay private plan creation fee") + return nil, sdkerrors.Wrap(sdkerrors.ErrInsufficientFunds, "insufficient balance to pay private plan creation fee") } farmingFeeCollectorAcc, err := sdk.AccAddressFromBech32(params.FarmingFeeCollector) if err != nil { - return err + return nil, err } if err := k.bankKeeper.SendCoins(ctx, farmingPoolAddrAcc, farmingFeeCollectorAcc, params.PrivatePlanCreationFee); err != nil { - return err + return nil, err } basePlan := types.NewBasePlan( @@ -267,5 +267,5 @@ func (k Keeper) CreateRatioPlan(ctx sdk.Context, msg *types.MsgCreateRatioPlan, ), }) - return nil + return ratioPlan, nil } diff --git a/x/farming/keeper/plan_test.go b/x/farming/keeper/plan_test.go index 045f69ef..fa411738 100644 --- a/x/farming/keeper/plan_test.go +++ b/x/farming/keeper/plan_test.go @@ -2,63 +2,58 @@ package keeper_test import ( "fmt" - "testing" "time" - "github.com/stretchr/testify/require" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/tendermint/farming/app" "github.com/tendermint/farming/x/farming/types" ) -func TestGetSetNewPlan(t *testing.T) { - simapp, ctx := createTestApp(true) - - farmingPoolAddr := sdk.AccAddress([]byte("farmingPoolAddr")) - terminationAddr := sdk.AccAddress([]byte("terminationAddr")) +func (suite *KeeperTestSuite) TestGetSetNewPlan() { + farmingPoolAddr := sdk.AccAddress("farmingPoolAddr") + terminationAddr := sdk.AccAddress("terminationAddr") stakingCoins := sdk.NewCoins(sdk.NewCoin(sdk.DefaultBondDenom, sdk.NewInt(1000000))) coinWeights := sdk.NewDecCoins( sdk.DecCoin{Denom: "testFarmStakingCoinDenom", Amount: sdk.MustNewDecFromStr("1.0")}, ) - addrs := app.AddTestAddrs(simapp, ctx, 2, sdk.NewInt(2000000)) + addrs := app.AddTestAddrs(suite.app, suite.ctx, 2, sdk.NewInt(2000000)) farmerAddr := addrs[0] startTime := time.Now().UTC() endTime := startTime.AddDate(1, 0, 0) basePlan := types.NewBasePlan(1, 1, farmingPoolAddr.String(), terminationAddr.String(), coinWeights, startTime, endTime) fixedPlan := types.NewFixedAmountPlan(basePlan, sdk.NewCoins(sdk.NewCoin("testFarmCoinDenom", sdk.NewInt(1000000)))) - simapp.FarmingKeeper.SetPlan(ctx, fixedPlan) + suite.keeper.SetPlan(suite.ctx, fixedPlan) - planGet, found := simapp.FarmingKeeper.GetPlan(ctx, 1) - require.True(t, found) - require.Equal(t, fixedPlan, planGet) + planGet, found := suite.keeper.GetPlan(suite.ctx, 1) + suite.Require().True(found) + suite.Require().Equal(fixedPlan, planGet) - plans := simapp.FarmingKeeper.GetAllPlans(ctx) - require.Len(t, plans, 1) - require.Equal(t, fixedPlan, plans[0]) + plans := suite.keeper.GetAllPlans(suite.ctx) + suite.Require().Len(plans, 1) + suite.Require().Equal(fixedPlan, plans[0]) // TODO: tmp test codes for testing functionality, need to separated - err := simapp.FarmingKeeper.Stake(ctx, farmerAddr, stakingCoins) - require.NoError(t, err) + _, err := suite.keeper.Stake(suite.ctx, farmerAddr, stakingCoins) + suite.Require().NoError(err) - stakings := simapp.FarmingKeeper.GetAllStakings(ctx) + stakings := suite.keeper.GetAllStakings(suite.ctx) fmt.Println(stakings) - stakingByFarmer, found := simapp.FarmingKeeper.GetStakingByFarmer(ctx, farmerAddr) - stakingsByDenom := simapp.FarmingKeeper.GetStakingsByStakingCoinDenom(ctx, sdk.DefaultBondDenom) + stakingByFarmer, found := suite.keeper.GetStakingByFarmer(suite.ctx, farmerAddr) + stakingsByDenom := suite.keeper.GetStakingsByStakingCoinDenom(suite.ctx, sdk.DefaultBondDenom) - require.True(t, found) - require.Equal(t, stakings[0], stakingByFarmer) - require.Equal(t, stakings, stakingsByDenom) + suite.Require().True(found) + suite.Require().Equal(stakings[0], stakingByFarmer) + suite.Require().Equal(stakings, stakingsByDenom) - simapp.FarmingKeeper.SetReward(ctx, sdk.DefaultBondDenom, farmerAddr, stakingCoins) + suite.keeper.SetReward(suite.ctx, sdk.DefaultBondDenom, farmerAddr, stakingCoins) - //rewards := simapp.FarmingKeeper.GetAllRewards(ctx) - //rewardsByFarmer := simapp.FarmingKeeper.GetRewardsByFarmer(ctx, farmerAddr) - //rewardsByDenom := simapp.FarmingKeeper.GetRewardsByStakingCoinDenom(ctx, sdk.DefaultBondDenom) + //rewards := suite.keeper.GetAllRewards(ctx) + //rewardsByFarmer := suite.keeper.GetRewardsByFarmer(ctx, farmerAddr) + //rewardsByDenom := suite.keeper.GetRewardsByStakingCoinDenom(ctx, sdk.DefaultBondDenom) // - //require.Equal(t, rewards, rewardsByFarmer) - //require.Equal(t, rewards, rewardsByDenom) + //suite.Require().Equal(rewards, rewardsByFarmer) + //suite.Require().Equal(rewards, rewardsByDenom) } diff --git a/x/farming/keeper/proposal_handler.go b/x/farming/keeper/proposal_handler.go index 75571ba6..a6d385fd 100644 --- a/x/farming/keeper/proposal_handler.go +++ b/x/farming/keeper/proposal_handler.go @@ -26,7 +26,10 @@ func HandlePublicPlanProposal(ctx sdk.Context, k Keeper, plansAny []*codectypes. p.EpochAmount, ) - fixedPlan := k.CreateFixedAmountPlan(ctx, msg, types.PlanTypePublic) + fixedPlan, err := k.CreateFixedAmountPlan(ctx, msg, types.PlanTypePublic) + if err != nil { + return err + } logger := k.Logger(ctx) logger.Info("created public fixed amount plan", "fixed_amount_plan", fixedPlan) @@ -40,7 +43,10 @@ func HandlePublicPlanProposal(ctx sdk.Context, k Keeper, plansAny []*codectypes. p.EpochRatio, ) - ratioPlan := k.CreateRatioPlan(ctx, msg, types.PlanTypePublic) + ratioPlan, err := k.CreateRatioPlan(ctx, msg, types.PlanTypePublic) + if err != nil { + return err + } logger := k.Logger(ctx) logger.Info("created public fixed amount plan", "ratio_plan", ratioPlan) diff --git a/x/farming/keeper/reward.go b/x/farming/keeper/reward.go index 401c9de0..31e6524e 100644 --- a/x/farming/keeper/reward.go +++ b/x/farming/keeper/reward.go @@ -1,8 +1,6 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -131,24 +129,12 @@ func (k Keeper) Harvest(ctx sdk.Context, farmerAcc sdk.AccAddress, stakingCoinDe amount = amount.Add(reward.RewardCoins...) } - if err := k.ReleaseStakingCoins(ctx, farmerAcc, amount); err != nil { - return err - } + // TODO: send reward from the reward pool for _, denom := range stakingCoinDenoms { k.DeleteReward(ctx, denom, farmerAcc) } - if len(k.GetRewardsByFarmer(ctx, farmerAcc)) == 0 { - staking, found := k.GetStakingByFarmer(ctx, farmerAcc) - if !found { // TODO: remove this check - return fmt.Errorf("staking not found") - } - if staking.StakedCoins.IsZero() && staking.QueuedCoins.IsZero() { - k.DeleteStaking(ctx, staking) - } - } - ctx.EventManager().EmitEvents(sdk.Events{ sdk.NewEvent( types.EventTypeHarvest, @@ -159,3 +145,118 @@ func (k Keeper) Harvest(ctx sdk.Context, farmerAcc sdk.AccAddress, stakingCoinDe return nil } + +type DistributionInfo struct { + Plan types.PlanI + Amount sdk.Coins +} + +func (k Keeper) DistributionInfos(ctx sdk.Context) []DistributionInfo { + farmingPoolBalances := make(map[string]sdk.Coins) // farmingPoolAddress => sdk.Coins + distrCoins := make(map[string]map[uint64]sdk.Coins) // farmingPoolAddress => (planId => sdk.Coins) + + plans := make(map[uint64]types.PlanI) + for _, plan := range k.GetAllPlans(ctx) { + // Filter plans by their start time and end time. + if !plan.GetStartTime().After(ctx.BlockTime()) && plan.GetEndTime().After(ctx.BlockTime()) { + plans[plan.GetId()] = plan + } + } + + for _, plan := range plans { + farmingPoolAcc := plan.GetFarmingPoolAddress() + farmingPool := farmingPoolAcc.String() + + balances, ok := farmingPoolBalances[farmingPool] + if !ok { + balances = k.bankKeeper.GetAllBalances(ctx, farmingPoolAcc) + farmingPoolBalances[farmingPool] = balances + } + + dc, ok := distrCoins[farmingPool] + if !ok { + dc = make(map[uint64]sdk.Coins) + distrCoins[farmingPool] = dc + } + + switch plan := plan.(type) { + case *types.FixedAmountPlan: + dc[plan.GetId()] = plan.EpochAmount + case *types.RatioPlan: + dc[plan.GetId()], _ = sdk.NewDecCoinsFromCoins(balances...).MulDecTruncate(plan.EpochRatio).TruncateDecimal() + } + } + + var distrInfos []DistributionInfo + for farmingPool, coins := range distrCoins { + totalCoins := sdk.NewCoins() + for _, amt := range coins { + totalCoins = totalCoins.Add(amt...) + } + + balances := farmingPoolBalances[farmingPool] + if !totalCoins.IsAllLT(balances) { + continue + } + + for planID, amt := range coins { + distrInfos = append(distrInfos, DistributionInfo{ + Plan: plans[planID], + Amount: amt, + }) + } + } + + return distrInfos +} + +func (k Keeper) DistributeRewards(ctx sdk.Context) error { + stakingsByDenom := make(map[string][]types.Staking) + totalStakedAmtByDenom := make(map[string]sdk.Int) + + for _, distrInfo := range k.DistributionInfos(ctx) { + stakingCoinWeights := distrInfo.Plan.GetStakingCoinWeights() + totalDistrAmt := sdk.NewCoins() + + totalWeight := sdk.ZeroDec() + for _, coinWeight := range stakingCoinWeights { + totalWeight = totalWeight.Add(coinWeight.Amount) + } + + for _, coinWeight := range stakingCoinWeights { + stakings, ok := stakingsByDenom[coinWeight.Denom] + if !ok { + stakings = k.GetStakingsByStakingCoinDenom(ctx, coinWeight.Denom) + stakingsByDenom[coinWeight.Denom] = stakings + + for _, staking := range stakings { + totalStakedAmtByDenom[coinWeight.Denom] = totalStakedAmtByDenom[coinWeight.Denom].Add(staking.StakedCoins.AmountOf(coinWeight.Denom)) + } + } + + totalStakedAmt := totalStakedAmtByDenom[coinWeight.Denom] + + for _, staking := range stakings { + stakedAmt := staking.StakedCoins.AmountOf(coinWeight.Denom) + if !stakedAmt.IsPositive() { + continue + } + + stakedProportion := stakedAmt.ToDec().QuoTruncate(totalStakedAmt.ToDec()) + weightProportion := coinWeight.Amount.QuoTruncate(totalWeight) + distrAmt, _ := sdk.NewDecCoinsFromCoins(distrInfo.Amount...).MulDecTruncate(stakedProportion.MulTruncate(weightProportion)).TruncateDecimal() + + reward, _ := k.GetReward(ctx, coinWeight.Denom, staking.GetFarmer()) + reward.RewardCoins = reward.RewardCoins.Add(distrAmt...) + k.SetReward(ctx, coinWeight.Denom, staking.GetFarmer(), reward.RewardCoins) + totalDistrAmt = totalDistrAmt.Add(distrAmt...) + } + } + + if err := k.bankKeeper.SendCoins(ctx, distrInfo.Plan.GetFarmingPoolAddress(), distrInfo.Plan.GetRewardPoolAddress(), totalDistrAmt); err != nil { + return err + } + } + + return nil +} diff --git a/x/farming/keeper/reward_test.go b/x/farming/keeper/reward_test.go new file mode 100644 index 00000000..362ff7f9 --- /dev/null +++ b/x/farming/keeper/reward_test.go @@ -0,0 +1,113 @@ +package keeper_test + +import ( + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/tendermint/farming/x/farming/types" +) + +func (suite *KeeperTestSuite) TestDistributionInfos() { + normalPlans := []types.PlanI{ + types.NewFixedAmountPlan( + types.NewBasePlan(1, types.PlanTypePrivate, suite.addrs[0].String(), suite.addrs[0].String(), + sdk.NewDecCoins(sdk.NewDecCoinFromDec(denom1, sdk.NewDec(1))), + mustParseRFC3339("2021-07-27T00:00:00Z"), mustParseRFC3339("2021-07-28T00:00:00Z")), + sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))), + types.NewFixedAmountPlan( + types.NewBasePlan(2, types.PlanTypePrivate, suite.addrs[0].String(), suite.addrs[0].String(), + sdk.NewDecCoins(sdk.NewDecCoinFromDec(denom1, sdk.NewDec(1))), + mustParseRFC3339("2021-07-27T12:00:00Z"), mustParseRFC3339("2021-07-28T12:00:00Z")), + sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))), + } + + for _, tc := range []struct { + name string + plans []types.PlanI + t time.Time + distrAmts map[uint64]sdk.Coins + }{ + { + "insufficient farming pool balances", + []types.PlanI{ + types.NewFixedAmountPlan( + types.NewBasePlan(1, types.PlanTypePrivate, suite.addrs[0].String(), suite.addrs[0].String(), + sdk.NewDecCoins(sdk.NewDecCoinFromDec(denom1, sdk.NewDec(1))), + mustParseRFC3339("2021-07-27T00:00:00Z"), mustParseRFC3339("2021-07-30T00:00:00Z")), + sdk.NewCoins(sdk.NewInt64Coin(denom3, 10_000_000_000))), + }, + mustParseRFC3339("2021-07-28T00:00:00Z"), + nil, + }, + { + "start time & end time edgecase #1", + normalPlans, + mustParseRFC3339("2021-07-26T23:59:59Z"), + nil, + }, + { + "start time & end time edgecase #2", + normalPlans, + mustParseRFC3339("2021-07-27T00:00:00Z"), + map[uint64]sdk.Coins{1: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #3", + normalPlans, + mustParseRFC3339("2021-07-27T11:59:59Z"), + map[uint64]sdk.Coins{1: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #4", + normalPlans, + mustParseRFC3339("2021-07-27T12:00:00Z"), + map[uint64]sdk.Coins{1: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000)), 2: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #5", + normalPlans, + mustParseRFC3339("2021-07-27T23:59:59Z"), + map[uint64]sdk.Coins{1: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000)), 2: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #6", + normalPlans, + mustParseRFC3339("2021-07-28T00:00:00Z"), + map[uint64]sdk.Coins{2: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #7", + normalPlans, + mustParseRFC3339("2021-07-28T11:59:59Z"), + map[uint64]sdk.Coins{2: sdk.NewCoins(sdk.NewInt64Coin(denom3, 1000))}, + }, + { + "start time & end time edgecase #8", + normalPlans, + mustParseRFC3339("2021-07-28T12:00:00Z"), + nil, + }, + } { + suite.Run(tc.name, func() { + for _, plan := range tc.plans { + suite.keeper.SetPlan(suite.ctx, plan) + } + + suite.ctx = suite.ctx.WithBlockTime(tc.t) + distrInfos := suite.keeper.DistributionInfos(suite.ctx) + if suite.Len(distrInfos, len(tc.distrAmts)) { + for _, distrInfo := range distrInfos { + distrAmt, ok := tc.distrAmts[distrInfo.Plan.GetId()] + if suite.True(ok) { + suite.True(coinsEq(distrAmt, distrInfo.Amount)) + } + } + } + }) + } +} + +func (suite *KeeperTestSuite) TestDistributeRewards() { + +} diff --git a/x/farming/keeper/staking.go b/x/farming/keeper/staking.go index 1bf1b440..d58e94bb 100644 --- a/x/farming/keeper/staking.go +++ b/x/farming/keeper/staking.go @@ -3,9 +3,11 @@ package keeper import ( "encoding/binary" - sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" gogotypes "github.com/gogo/protobuf/types" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/farming/x/farming/types" ) @@ -111,7 +113,9 @@ func (k Keeper) DeleteStaking(ctx sdk.Context, staking types.Staking) { store := ctx.KVStore(k.storeKey) store.Delete(types.GetStakingKey(staking.Id)) store.Delete(types.GetStakingByFarmerIndexKey(staking.GetFarmer())) - k.DeleteStakingCoinDenomsIndex(ctx, staking.Id, staking.StakingCoinDenoms()) + if denoms := staking.StakingCoinDenoms(); len(denoms) > 0 { + k.DeleteStakingCoinDenomsIndex(ctx, staking.Id, denoms) + } } // DeleteStakingCoinDenomsIndex removes an staking for the staking mapper store. @@ -175,22 +179,21 @@ func (k Keeper) ReleaseStakingCoins(ctx sdk.Context, farmer sdk.AccAddress, unst } // Stake stores staking coins to queued coins and it will be processed in the next epoch. -func (k Keeper) Stake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins) error { +func (k Keeper) Stake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins) (types.Staking, error) { if err := k.ReserveStakingCoins(ctx, farmer, amount); err != nil { - return err + return types.Staking{}, err } params := k.GetParams(ctx) staking, found := k.GetStakingByFarmer(ctx, farmer) if !found { - farmingFeeCollectorAcc, err := sdk.AccAddressFromBech32(params.FarmingFeeCollector) - if err != nil { - return err - } + // We don't need to check error here because it is already checked + // in Params validation. + farmingFeeCollectorAcc, _ := sdk.AccAddressFromBech32(params.FarmingFeeCollector) if err := k.bankKeeper.SendCoins(ctx, farmer, farmingFeeCollectorAcc, params.StakingCreationFee); err != nil { - return err + return types.Staking{}, sdkerrors.Wrap(types.ErrFeeCollectionFailure, err.Error()) } staking = k.NewStaking(ctx, farmer) @@ -208,18 +211,18 @@ func (k Keeper) Stake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins) ), }) - return nil + return staking, nil } // Unstake unstakes an amount of staking coins from the staking reserve account. -func (k Keeper) Unstake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins) error { +func (k Keeper) Unstake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins) (types.Staking, error) { staking, found := k.GetStakingByFarmer(ctx, farmer) if !found { - return types.ErrStakingNotExists + return types.Staking{}, types.ErrStakingNotExists } if err := k.ReleaseStakingCoins(ctx, farmer, amount); err != nil { - return err + return types.Staking{}, err } prevDenoms := staking.StakingCoinDenoms() @@ -239,6 +242,7 @@ func (k Keeper) Unstake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins // Remove the Staking object from the kvstore when all coins has been unstaked // and there's no rewards left. + // TODO: find more efficient way to check if the farmer has no rewards if staking.StakedCoins.IsZero() && staking.QueuedCoins.IsZero() && len(k.GetRewardsByFarmer(ctx, farmer)) == 0 { k.DeleteStaking(ctx, staking) } else { @@ -256,7 +260,9 @@ func (k Keeper) Unstake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins } } - k.DeleteStakingCoinDenomsIndex(ctx, staking.Id, removedDenoms) + if len(removedDenoms) > 0 { + k.DeleteStakingCoinDenomsIndex(ctx, staking.Id, removedDenoms) + } } ctx.EventManager().EmitEvents(sdk.Events{ @@ -267,5 +273,18 @@ func (k Keeper) Unstake(ctx sdk.Context, farmer sdk.AccAddress, amount sdk.Coins ), }) - return nil + // We're returning a Staking even if it has deleted. + return staking, nil +} + +// ProcessQueuedCoins moves queued coins into staked coins. +func (k Keeper) ProcessQueuedCoins(ctx sdk.Context) { + k.IterateAllStakings(ctx, func(staking types.Staking) (stop bool) { + if !staking.QueuedCoins.IsZero() { + staking.StakedCoins = staking.StakedCoins.Add(staking.QueuedCoins...) + staking.QueuedCoins = sdk.NewCoins() + k.SetStaking(ctx, staking) + } + return false + }) } diff --git a/x/farming/keeper/staking_test.go b/x/farming/keeper/staking_test.go new file mode 100644 index 00000000..05d30704 --- /dev/null +++ b/x/farming/keeper/staking_test.go @@ -0,0 +1,225 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/tendermint/farming/x/farming" +) + +func (suite *KeeperTestSuite) TestGetNextStakingID() { + for id := uint64(1); id <= 100; id++ { + suite.Require().Equal(id, suite.keeper.GetNextStakingIDWithUpdate(suite.ctx)) + } +} + +func (suite *KeeperTestSuite) TestGetStaking() { + _, found := suite.keeper.GetStaking(suite.ctx, 1) + suite.False(found, "staking should not be present") + + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000))) + + _, found = suite.keeper.GetStaking(suite.ctx, 1) + suite.True(found, "staking should be present") +} + +func (suite *KeeperTestSuite) TestStake() { + for _, tc := range []struct { + name string + amt int64 + remainingStaked int64 + remainingQueued int64 + expectErr bool + }{ + { + "normal", + 1000, + 0, + 1000, + false, + }, + { + "more than balance", + 10_000_000_000, + 0, + 0, + true, + }, + } { + suite.Run(tc.name, func() { + suite.SetupTest() + + _, found := suite.keeper.GetStakingByFarmer(suite.ctx, suite.addrs[0]) + suite.Require().False(found, "staking should not be present") + + staking, err := suite.keeper.Stake(suite.ctx, suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, tc.amt))) + if tc.expectErr { + suite.Error(err) + } else { + suite.NoError(err) + staking2, found := suite.keeper.GetStakingByFarmer(suite.ctx, suite.addrs[0]) + suite.True(found, "staking should be present") + suite.True(staking2.StakedCoins.IsEqual(staking.StakedCoins)) + suite.True(staking2.QueuedCoins.IsEqual(staking2.QueuedCoins)) + + suite.True(intEq(sdk.NewInt(tc.remainingStaked), staking.StakedCoins.AmountOf(denom1))) + suite.True(intEq(sdk.NewInt(tc.remainingQueued), staking.QueuedCoins.AmountOf(denom1))) + } + }) + } +} + +func (suite *KeeperTestSuite) TestStakingCreationFee() { + params := suite.keeper.GetParams(suite.ctx) + params.StakingCreationFee = sdk.NewCoins(sdk.NewInt64Coin(denom1, 1_000_000)) + suite.keeper.SetParams(suite.ctx, params) + + // Test accounts have 1,000,000,000 coins by default. + balance := suite.app.BankKeeper.GetBalance(suite.ctx, suite.addrs[0], denom1) + suite.Require().True(intEq(sdk.NewInt(1_000_000_000), balance.Amount)) + + // Stake 999,000,000 coins and pay 1,000,000 coins as staking creation fee because + // it's the first time staking. + _, err := suite.keeper.Stake(suite.ctx, suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 999_000_000))) + suite.Require().NoError(err) + + // Balance should be zero now. + balance = suite.app.BankKeeper.GetBalance(suite.ctx, suite.addrs[0], denom1) + suite.Require().True(balance.Amount.IsZero()) + + // Taking a new account, staking 1_000_000_000 coins should fail because + // there is no sufficient balance for staking creation fee. + _, err = suite.keeper.Stake(suite.ctx, suite.addrs[1], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1_000_000_000))) + suite.Require().Error(err) +} + +func (suite *KeeperTestSuite) TestUnstake() { + for _, tc := range []struct { + name string + addrIdx int + amt sdk.Coins + remainingStaked sdk.Coins + remainingQueued sdk.Coins + expectErr bool + }{ + { + "from queued coins", + 0, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 5000)), + sdk.NewCoins(sdk.NewInt64Coin(denom1, 500_000), sdk.NewInt64Coin(denom2, 1_000_000)), + sdk.NewCoins(sdk.NewInt64Coin(denom1, 495_000)), + false, + }, + { + "from staked coins", + 0, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 700_000), sdk.NewInt64Coin(denom2, 100_000)), + sdk.NewCoins(sdk.NewInt64Coin(denom1, 300_000), sdk.NewInt64Coin(denom2, 900_000)), + sdk.NewCoins(), + false, + }, + { + "one coin", + 0, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 1_000_000)), + sdk.NewCoins(sdk.NewInt64Coin(denom2, 1_000_000)), + sdk.NewCoins(), + false, + }, + { + "unstake all", + 0, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 1_000_000), sdk.NewInt64Coin(denom2, 1_000_000)), + sdk.NewCoins(), + sdk.NewCoins(), + false, + }, + { + "more than staked", + 0, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 1_100_000), sdk.NewInt64Coin(denom2, 1_100_000)), + // We can use nil since there will be an error and we don't use these fields + nil, + nil, + true, + }, + { + "no staking", + 1, + sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000)), + nil, + nil, + true, + }, + } { + suite.Run(tc.name, func() { + suite.SetupTest() + suite.Stake(suite.addrs[0], sdk.NewCoins( + sdk.NewInt64Coin(denom1, 500_000), + sdk.NewInt64Coin(denom2, 1_000_000))) + + // Make queued coins be staked. + suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-07-23T05:00:00Z")) + farming.EndBlocker(suite.ctx, suite.keeper) + suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-07-24T00:05:00Z")) + farming.EndBlocker(suite.ctx, suite.keeper) + + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 500_000))) + + // At this moment, we have 500,000denom1,1,000,000denom2 staked and + // 500000denom1 queued. + + staking, err := suite.keeper.Unstake(suite.ctx, suite.addrs[tc.addrIdx], tc.amt) + if tc.expectErr { + suite.Error(err) + } else { + if suite.NoError(err) { + suite.True(coinsEq(tc.remainingStaked, staking.StakedCoins)) + suite.True(coinsEq(tc.remainingQueued, staking.QueuedCoins)) + } + } + }) + } +} + +func (suite *KeeperTestSuite) TestProcessQueuedCoins() { + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000))) + + staking, _ := suite.keeper.GetStakingByFarmer(suite.ctx, suite.addrs[0]) + + suite.Require().True(staking.StakedCoins.IsZero()) + suite.Require().True(intEq(sdk.NewInt(1000), staking.QueuedCoins.AmountOf(denom1))) + + suite.keeper.ProcessQueuedCoins(suite.ctx) + + staking, _ = suite.keeper.GetStakingByFarmer(suite.ctx, suite.addrs[0]) + + suite.Require().True(intEq(sdk.NewInt(1000), staking.StakedCoins.AmountOf(denom1))) + suite.Require().True(staking.QueuedCoins.IsZero()) +} + +func (suite *KeeperTestSuite) TestEndBlockerProcessQueuedCoins() { + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 1000))) + + ctx := suite.ctx.WithBlockTime(mustParseRFC3339("2021-07-23T05:00:00Z")) + farming.EndBlocker(ctx, suite.keeper) + + staking, _ := suite.keeper.GetStakingByFarmer(ctx, suite.addrs[0]) + suite.Require().True(intEq(sdk.NewInt(1000), staking.QueuedCoins.AmountOf(denom1))) + suite.Require().True(staking.StakedCoins.IsZero(), "staked coins must be empty") + + suite.Stake(suite.addrs[0], sdk.NewCoins(sdk.NewInt64Coin(denom1, 500))) + + ctx = ctx.WithBlockTime(mustParseRFC3339("2021-07-23T23:59:59Z")) + farming.EndBlocker(ctx, suite.keeper) + + staking, _ = suite.keeper.GetStakingByFarmer(ctx, suite.addrs[0]) + suite.Require().True(intEq(sdk.NewInt(1500), staking.QueuedCoins.AmountOf(denom1))) + suite.Require().True(staking.StakedCoins.IsZero(), "staked coins must be empty") + + ctx = ctx.WithBlockTime(mustParseRFC3339("2021-07-24T00:00:01Z")) + farming.EndBlocker(ctx, suite.keeper) + + staking, _ = suite.keeper.GetStakingByFarmer(ctx, suite.addrs[0]) + suite.Require().True(staking.QueuedCoins.IsZero(), "queued coins must be empty") + suite.Require().True(intEq(sdk.NewInt(1500), staking.StakedCoins.AmountOf(denom1))) +} diff --git a/x/farming/module.go b/x/farming/module.go index 2555e4c8..9b4d5b7f 100644 --- a/x/farming/module.go +++ b/x/farming/module.go @@ -170,13 +170,13 @@ func (am AppModule) ExportGenesis(ctx sdk.Context, cdc codec.JSONCodec) json.Raw func (AppModule) ConsensusVersion() uint64 { return 2 } // BeginBlock returns the begin blocker for the farming module. -func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) { - BeginBlocker(ctx, req, am.keeper) +func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) { } // EndBlock returns the end blocker for the farming module. It returns no validator // updates. -func (AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { +func (am AppModule) EndBlock(ctx sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate { + EndBlocker(ctx, am.keeper) return []abci.ValidatorUpdate{} } diff --git a/x/farming/spec/02_state.md b/x/farming/spec/02_state.md index 5e59ddb8..a2358eb7 100644 --- a/x/farming/spec/02_state.md +++ b/x/farming/spec/02_state.md @@ -104,9 +104,12 @@ The parameters of the Plan state are: - TotalDistributedRewardCoins: `0x14 | PlanId | StakingCoinDenomLen (1 byte) | StakingCoinDenom → ProtocolBuffer(sdk.Coins)` - GlobalPlanIdKey: `[]byte("globalPlanId") -> ProtocolBuffer(uint64)` - store latest plan id -- GlobalLastEpochTime: `[]byte("globalLastEpochTime") | Id -> time.Time` - ModuleName, RouterKey, StoreKey, QuerierRoute: `farming` +## Epoch + +- GlobalLastEpochTime: `[]byte("globalLastEpochTime") -> ProtocolBuffer(Timestamp)` + ## Staking ```go diff --git a/x/farming/types/errors.go b/x/farming/types/errors.go index bf116311..06b31879 100644 --- a/x/farming/types/errors.go +++ b/x/farming/types/errors.go @@ -6,15 +6,13 @@ import ( // farming module sentinel errors var ( - ErrPlanNotExists = sdkerrors.Register(ModuleName, 2, "plan not exists") - ErrPlanTypeNotExists = sdkerrors.Register(ModuleName, 3, "plan type not exists") - ErrInvalidPlanType = sdkerrors.Register(ModuleName, 4, "invalid plan type") - ErrInvalidPlanEndTime = sdkerrors.Register(ModuleName, 5, "invalid plan end time") - ErrInvalidPlanEpochDays = sdkerrors.Register(ModuleName, 6, "invalid plan epoch days") - ErrInvalidPlanEpochRatio = sdkerrors.Register(ModuleName, 7, "invalid plan epoch ratio") - ErrEmptyEpochAmount = sdkerrors.Register(ModuleName, 8, "epoch amount must not be empty") - ErrEmptyStakingCoinWeights = sdkerrors.Register(ModuleName, 9, "staking coin weights must not be empty") - ErrStakingNotExists = sdkerrors.Register(ModuleName, 10, "staking not exists") - ErrRewardNotExists = sdkerrors.Register(ModuleName, 11, "reward not exists") - ErrInsufficientStakingAmount = sdkerrors.Register(ModuleName, 12, "insufficient staking amount") + ErrPlanNotExists = sdkerrors.Register(ModuleName, 2, "plan does not exist") + ErrInvalidPlanType = sdkerrors.Register(ModuleName, 3, "invalid plan type") + ErrInvalidPlanEndTime = sdkerrors.Register(ModuleName, 4, "invalid plan end time") + ErrInvalidPlanEpochRatio = sdkerrors.Register(ModuleName, 5, "invalid plan epoch ratio") + ErrEmptyEpochAmount = sdkerrors.Register(ModuleName, 6, "epoch amount must not be empty") + ErrEmptyStakingCoinWeights = sdkerrors.Register(ModuleName, 7, "staking coin weights must not be empty") + ErrStakingNotExists = sdkerrors.Register(ModuleName, 8, "staking not exists") + ErrRewardNotExists = sdkerrors.Register(ModuleName, 9, "reward not exists") + ErrFeeCollectionFailure = sdkerrors.Register(ModuleName, 10, "fee collection failure") ) diff --git a/x/farming/types/keys.go b/x/farming/types/keys.go index b515587a..f39d4f71 100644 --- a/x/farming/types/keys.go +++ b/x/farming/types/keys.go @@ -27,9 +27,9 @@ const ( var ( // param key for global farming plan IDs - GlobalPlanIdKey = []byte("globalPlanId") - GlobalLastEpochTimePrefix = []byte("globalLastEpochTime") - GlobalStakingIdKey = []byte("globalStakingId") + GlobalPlanIdKey = []byte("globalPlanId") + GlobalLastEpochTimeKey = []byte("globalLastEpochTime") + GlobalStakingIdKey = []byte("globalStakingId") PlanKeyPrefix = []byte{0x11} PlansByFarmerIndexKeyPrefix = []byte{0x12} diff --git a/x/farming/types/plan.go b/x/farming/types/plan.go index 760afae8..9e5be82e 100644 --- a/x/farming/types/plan.go +++ b/x/farming/types/plan.go @@ -21,6 +21,7 @@ var ( // NewBasePlan creates a new BasePlan object //nolint:interfacer +// TODO: accept sdk.AccAddress instead of string for addresses func NewBasePlan(id uint64, typ PlanType, farmingPoolAddr, terminationAddr string, coinWeights sdk.DecCoins, startTime, endTime time.Time) *BasePlan { basePlan := &BasePlan{ Id: id,