Skip to content

Commit

Permalink
feat: add genesis functionality (#87)
Browse files Browse the repository at this point in the history
* feat: add genesis functionality

* fix: set staking index when initializing genesis

* test: add test codes for genesis

* fix: lint issue

Co-authored-by: Hanjun Kim <[email protected]>
  • Loading branch information
dongsam and hallazzang authored Sep 2, 2021
1 parent d721098 commit 6dea151
Show file tree
Hide file tree
Showing 15 changed files with 449 additions and 320 deletions.
2 changes: 1 addition & 1 deletion app/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
dbm "github.com/tendermint/tm-db"
)

func TestSimAppExportAndBlockedAddrs(t *testing.T) {
func TestSimAppExport(t *testing.T) {
encCfg := MakeTestEncodingConfig()
db := dbm.NewMemDB()
app := NewFarmingApp(log.NewTMLogger(log.NewSyncWriter(os.Stdout)), db, nil, true, map[int64]bool{}, DefaultNodeHome, 0, encCfg, EmptyAppOptions{})
Expand Down
2 changes: 1 addition & 1 deletion app/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (app *FarmingApp) prepForZeroHeightGenesis(ctx sdk.Context, jailAllowedAddr
counter := int16(0)

for ; iter.Valid(); iter.Next() {
addr := sdk.ValAddress(iter.Key()[1:])
addr := sdk.ValAddress(stakingtypes.AddressFromValidatorsKey(iter.Key()))
validator, found := app.StakingKeeper.GetValidator(ctx, addr)
if !found {
panic("expected validator, not found")
Expand Down
47 changes: 25 additions & 22 deletions proto/tendermint/farming/v1beta1/genesis.proto
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ message GenesisState {

// rewards defines the reward records used for genesis state
repeated Reward rewards = 4 [(gogoproto.nullable) = false];

// staking_reserve_coins specifies balance of the staking reserve pool staked in the plans
// this param is needed for import/export validation
repeated cosmos.base.v1beta1.Coin staking_reserve_coins = 5 [
(gogoproto.moretags) = "yaml:\"staking_reserve_coins\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];

// reward_pool_coins specifies balance of the reward pool to be distributed in the plans
// this param is needed for import/export validation
repeated cosmos.base.v1beta1.Coin reward_pool_coins = 6 [
(gogoproto.moretags) = "yaml:\"reward_pool_coins\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];

// global_last_epoch_time specifies the last executed epoch time of the plans
google.protobuf.Timestamp global_last_epoch_time = 7 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"global_last_epoch_time\""
];
}

// PlanRecord is used for import/export via genesis json.
Expand All @@ -36,33 +59,13 @@ message PlanRecord {
option (gogoproto.goproto_getters) = false;

// plan specifies the plan interface; it can be FixedAmountPlan or RatioPlan
google.protobuf.Any plan = 1 [(gogoproto.nullable) = false];

// last_epoch_time specifies the last distributed epoch time of the plan
google.protobuf.Timestamp last_epoch_time = 2
[(gogoproto.stdtime) = true, (gogoproto.nullable) = false, (gogoproto.moretags) = "yaml:\"last_epoch_time\""];
google.protobuf.Any plan = 1 [(gogoproto.nullable) = false, (cosmos_proto.accepts_interface) = "PlanI"];

// farming_pool_coins specifies balance of the farming pool for the plan
// this param is needed for import/export validation
repeated cosmos.base.v1beta1.Coin farming_pool_coins = 3 [
repeated cosmos.base.v1beta1.Coin farming_pool_coins = 2 [
(gogoproto.moretags) = "yaml:\"farming_pool_coins\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];

// reward_pool_coins specifies balance of the reward pool to be distributed in
// the plan this param is needed for import/export validation
repeated cosmos.base.v1beta1.Coin reward_pool_coins = 4 [
(gogoproto.moretags) = "yaml:\"reward_pool_coins\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];

// staking_reserve_coins specifies balance of the staking reserve pool staked
// in the plan this param is needed for import/export validation
repeated cosmos.base.v1beta1.Coin staking_reserve_coins = 5 [
(gogoproto.moretags) = "yaml:\"staking_reserve_coins\"",
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins",
(gogoproto.nullable) = false
];
}
100 changes: 42 additions & 58 deletions x/farming/keeper/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,76 +8,60 @@ import (

// InitGenesis initializes the farming module's state from a given genesis state.
func (k Keeper) InitGenesis(ctx sdk.Context, genState types.GenesisState) {
if err := k.ValidateGenesis(ctx, genState); err != nil {
panic(err)
}

ctx, applyCache := ctx.CacheContext()
k.SetParams(ctx, genState.Params)
moduleAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleName)
k.accountKeeper.SetModuleAccount(ctx, moduleAcc)

// TODO: decision making is needed
// feeCollectorAcc, err := sdk.AccAddressFromBech32(types.DefaultFarmingFeeCollector)
// if err != nil {
// panic(err)
// }

// k.accountKeeper.SetModuleAccount(ctx, feeCollectorAcc)

// TODO: unimplemented
//for _, record := range genState.PlanRecords {
// k.SetPlanRecord(ctx, record)
//}
//for _, staking := range genState.Stakings {
// k.SetStaking(ctx, staking)
//}
//for _, reward := range genState.Rewards {
// k.SetReward(ctx, reword)
//}
_, err := sdk.AccAddressFromBech32(genState.Params.FarmingFeeCollector)
if err != nil {
panic(err)
}

for _, record := range genState.PlanRecords {
plan, err := types.UnpackPlan(&record.Plan)
if err != nil {
panic(err)
}
k.SetPlan(ctx, plan)
k.SetGlobalPlanId(ctx, plan.GetId())
}
for _, staking := range genState.Stakings {
k.SetStaking(ctx, staking)
k.SetStakingIndex(ctx, staking)
}
for _, reward := range genState.Rewards {
k.SetReward(ctx, reward.StakingCoinDenom, reward.GetFarmer(), reward.RewardCoins)
}
if err := k.ValidateRemainingRewardsAmount(ctx); err != nil {
panic(err)
}
if err := k.ValidateStakingReservedAmount(ctx); err != nil {
panic(err)
}
applyCache()
}

// ExportGenesis returns the farming module's genesis state.
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
params := k.GetParams(ctx)

// TODO: unimplemented
var planRecords []types.PlanRecord

//plans := k.GetAllPlans(ctx)
//stakings := k.GetAllStakings(ctx)
//rewards := k.GetAllRewards(ctx)

//for _, plan := range plans {
// record, found := k.GetPlanRecord(ctx, plan)
// if found {
// planRecords = append(planRecords, record)
// }
//}
//
//if len(planRecords) == 0 {
// planRecords = []types.PlanRecord{}
//}

return types.NewGenesisState(params, planRecords, nil, nil)
}

// ValidateGenesis validates the farming module's genesis state.
func (k Keeper) ValidateGenesis(ctx sdk.Context, genState types.GenesisState) error {
if err := genState.Params.Validate(); err != nil {
return err
plans := k.GetAllPlans(ctx)
stakings := k.GetAllStakings(ctx)
rewards := k.GetAllRewards(ctx)

for _, plan := range plans {
any, err := types.PackPlan(plan)
if err != nil {
panic(err)
}
planRecords = append(planRecords, types.PlanRecord{
Plan: *any,
FarmingPoolCoins: k.bankKeeper.GetAllBalances(ctx, plan.GetFarmingPoolAddress()),
})
}

cc, _ := ctx.CacheContext()
k.SetParams(cc, genState.Params)

// TODO: unimplemented
//for _, record := range genState.PlanRecords {
// record = k.SetPlanRecord(cc, record)
// if err := k.ValidatePlanRecord(cc, record); err != nil {
// return err
// }
//}

return nil
epochTime, _ := k.GetLastEpochTime(ctx)
return types.NewGenesisState(params, planRecords, stakings, rewards, k.bankKeeper.GetAllBalances(ctx, types.StakingReserveAcc), k.bankKeeper.GetAllBalances(ctx, types.RewardsReserveAcc), epochTime)
}
68 changes: 68 additions & 0 deletions x/farming/keeper/genesis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package keeper_test

import (
sdk "github.com/cosmos/cosmos-sdk/types"

"github.com/tendermint/farming/x/farming/types"
)

func (suite *KeeperTestSuite) TestInitGenesis() {
plans := []types.PlanI{
types.NewFixedAmountPlan(
types.NewBasePlan(
1,
"",
types.PlanTypePrivate,
suite.addrs[0].String(),
suite.addrs[0].String(),
sdk.NewDecCoins(
sdk.NewDecCoinFromDec(denom1, sdk.NewDecWithPrec(3, 1)),
sdk.NewDecCoinFromDec(denom2, sdk.NewDecWithPrec(7, 1))),
mustParseRFC3339("2021-07-30T00:00:00Z"),
mustParseRFC3339("2021-08-30T00:00:00Z"),
),
sdk.NewCoins(sdk.NewInt64Coin(denom3, 1_000_000))),
types.NewRatioPlan(
types.NewBasePlan(
2,
"",
types.PlanTypePublic,
suite.addrs[0].String(),
suite.addrs[0].String(),
sdk.NewDecCoins(
sdk.NewDecCoinFromDec(denom1, sdk.NewDecWithPrec(3, 1)),
sdk.NewDecCoinFromDec(denom2, sdk.NewDecWithPrec(7, 1))),
mustParseRFC3339("2021-07-30T00:00:00Z"),
mustParseRFC3339("2021-08-30T00:00:00Z"),
),
sdk.MustNewDecFromStr("0.01")),
}
//for _, plan := range plans {
// suite.keeper.SetPlan(suite.ctx, plan)
//}
suite.keeper.SetPlan(suite.ctx, plans[1])
suite.keeper.SetPlan(suite.ctx, plans[0])

suite.Stake(suite.addrs[1], sdk.NewCoins(
sdk.NewInt64Coin(denom1, 1_000_000),
sdk.NewInt64Coin(denom2, 1_000_000)))
suite.keeper.ProcessQueuedCoins(suite.ctx)

suite.ctx = suite.ctx.WithBlockTime(mustParseRFC3339("2021-07-31T00:00:00Z"))
err := suite.keeper.DistributeRewards(suite.ctx)
suite.Require().NoError(err)

var genState *types.GenesisState
suite.Require().NotPanics(func() {
genState = suite.keeper.ExportGenesis(suite.ctx)
},
)
err = types.ValidateGenesis(*genState)
suite.Require().NoError(err)

suite.Require().NotPanics(func() {
suite.keeper.InitGenesis(suite.ctx, *genState)
},
)
suite.Require().Equal(genState, suite.keeper.ExportGenesis(suite.ctx))
}
27 changes: 4 additions & 23 deletions x/farming/keeper/invariants.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,8 @@ func AllInvariants(k Keeper) sdk.Invariant {
// StakingReservedAmountInvariant checks that the balance of StakingReserveAcc greater than the amount of staked, Queued coins in all staking objects.
func StakingReservedAmountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var totalStakingAmt sdk.Coins
balanceStakingReserveAcc := k.bankKeeper.GetAllBalances(ctx, types.StakingReserveAcc)

k.IterateAllStakings(ctx, func(staking types.Staking) (stop bool) {
totalStakingAmt = totalStakingAmt.Add(staking.StakedCoins...).Add(staking.QueuedCoins...)
return false
})

broken := !balanceStakingReserveAcc.IsAllGTE(totalStakingAmt)
err := k.ValidateStakingReservedAmount(ctx)
broken := err != nil
return sdk.FormatInvariant(types.ModuleName, "staking reserved amount invariant broken",
"the balance of StakingReserveAcc less than the amount of staked, Queued coins in all staking objects"), broken
}
Expand All @@ -45,20 +38,8 @@ func StakingReservedAmountInvariant(k Keeper) sdk.Invariant {
// RemainingRewardsAmountInvariant checks that the balance of the RewardPoolAddresses of all plans greater than the total amount of unwithdrawn reward coins in all reward objects
func RemainingRewardsAmountInvariant(k Keeper) sdk.Invariant {
return func(ctx sdk.Context) (string, bool) {
var totalRemainingRewards sdk.Coins
//var totalBalancesRewardPools sdk.Coins
//k.IterateAllPlans(ctx, func(plan types.PlanI) (stop bool) {
// totalBalancesRewardPools = totalBalancesRewardPools.Add(k.bankKeeper.GetAllBalances(ctx, plan.GetRewardPoolAddress())...)
// return false
//})
totalBalancesRewardPool := k.bankKeeper.GetAllBalances(ctx, k.GetRewardsReservePoolAcc(ctx))
k.IterateAllRewards(ctx, func(reward types.Reward) (stop bool) {
totalRemainingRewards = totalRemainingRewards.Add(reward.RewardCoins...)
return false
})

//broken := !totalBalancesRewardPools.IsAllGTE(totalRemainingRewards)
broken := !totalBalancesRewardPool.IsAllGTE(totalRemainingRewards)
err := k.ValidateRemainingRewardsAmount(ctx)
broken := err != nil
return sdk.FormatInvariant(types.ModuleName, "remaining rewards amount invariant broken",
"the balance of the RewardPoolAddresses of all plans less than the total amount of unwithdrawn reward coins in all reward objects"), broken
}
Expand Down
40 changes: 38 additions & 2 deletions x/farming/keeper/plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,16 @@ func (k Keeper) SetPlanIdByFarmerAddrIndex(ctx sdk.Context, farmerAcc sdk.AccAdd
// GetNextPlanIdWithUpdate returns and increments the global Plan ID counter.
// If the global plan number is not set, it initializes it with value 0.
func (k Keeper) GetNextPlanIdWithUpdate(ctx sdk.Context) uint64 {
store := ctx.KVStore(k.storeKey)
id := k.GetGlobalPlanId(ctx) + 1
k.SetGlobalPlanId(ctx, id)
return id
}

// SetGlobalPlanId set the global Plan ID counter.
func (k Keeper) SetGlobalPlanId(ctx sdk.Context, id uint64) {
store := ctx.KVStore(k.storeKey)
bz := k.cdc.MustMarshal(&gogotypes.UInt64Value{Value: id})
store.Set(types.GlobalPlanIdKey, bz)
return id
}

// GetGlobalPlanId returns the global Plan ID counter.
Expand Down Expand Up @@ -307,3 +312,34 @@ func (k Keeper) GeneratePrivatePlanFarmingPoolAddress(ctx sdk.Context, name stri
}
return poolAcc, nil
}

// ValidateStakingReservedAmount checks that the balance of StakingReserveAcc greater than the amount of staked, Queued coins in all staking objects.
func (k Keeper) ValidateStakingReservedAmount(ctx sdk.Context) error {
var totalStakingAmt sdk.Coins
balanceStakingReserveAcc := k.bankKeeper.GetAllBalances(ctx, types.StakingReserveAcc)

k.IterateAllStakings(ctx, func(staking types.Staking) (stop bool) {
totalStakingAmt = totalStakingAmt.Add(staking.StakedCoins...).Add(staking.QueuedCoins...)
return false
})

if !balanceStakingReserveAcc.IsAllGTE(totalStakingAmt) {
return types.ErrInvalidStakingReservedAmount
}
return nil
}

// ValidateRemainingRewardsAmount checks that the balance of the RewardPoolAddresses of all plans greater than the total amount of unwithdrawn reward coins in all reward objects
func (k Keeper) ValidateRemainingRewardsAmount(ctx sdk.Context) error {
var totalRemainingRewards sdk.Coins
totalBalancesRewardPool := k.bankKeeper.GetAllBalances(ctx, k.GetRewardsReservePoolAcc(ctx))
k.IterateAllRewards(ctx, func(reward types.Reward) (stop bool) {
totalRemainingRewards = totalRemainingRewards.Add(reward.RewardCoins...)
return false
})

if !totalBalancesRewardPool.IsAllGTE(totalRemainingRewards) {
return types.ErrInvalidRemainingRewardsAmount
}
return nil
}
5 changes: 5 additions & 0 deletions x/farming/spec/02_state.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ type PlanI interface {

GetId() uint64
SetId(uint64) error

GetName() string
SetName(name string) error

GetType() int32
SetType(int32) error
Expand Down Expand Up @@ -44,6 +47,8 @@ type PlanI interface {
SetDistributedCoins(sdk.Coins) error

String() string

Validate() error
}
```

Expand Down
2 changes: 2 additions & 0 deletions x/farming/types/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ var (
ErrDuplicatePlanName = sdkerrors.Register(ModuleName, 9, "duplicate plan name")
ErrInvalidPlanName = sdkerrors.Register(ModuleName, 10, "invalid plan name")
ErrConflictPrivatePlanFarmingPool = sdkerrors.Register(ModuleName, 11, "the address is already in use, please use a different plan name")
ErrInvalidStakingReservedAmount = sdkerrors.Register(ModuleName, 12, "staking reserved amount invariant broken")
ErrInvalidRemainingRewardsAmount = sdkerrors.Register(ModuleName, 13, "remaining rewards amount invariant broken")
)
Loading

0 comments on commit 6dea151

Please sign in to comment.