harvest v1 (#658)

* wip: and types and keeper methods

* add keeper tests

* add client

* add spec and events

* respond to review comments

* apply suggestions from review

* feat: add test for validator vesting case

* use int64 for multiplier type

* remove incentive changes
This commit is contained in:
Kevin Davis 2020-09-21 17:08:43 -04:00 committed by GitHub
parent 7292b8843a
commit fe38c4aa43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 5538 additions and 3 deletions

View File

@ -37,6 +37,7 @@ import (
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/hvt"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
@ -82,6 +83,7 @@ var (
kavadist.AppModuleBasic{},
incentive.AppModuleBasic{},
issuance.AppModuleBasic{},
hvt.AppModuleBasic{},
)
// module account permissions
@ -100,6 +102,9 @@ var (
bep3.ModuleName: {supply.Minter, supply.Burner},
kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
hvt.LPAccount: {supply.Minter, supply.Burner},
hvt.DelegatorAccount: {supply.Minter, supply.Burner},
hvt.ModuleAccountName: {supply.Minter, supply.Burner},
}
// module accounts that are allowed to receive tokens
@ -144,6 +149,7 @@ type App struct {
kavadistKeeper kavadist.Keeper
incentiveKeeper incentive.Keeper
issuanceKeeper issuance.Keeper
harvestKeeper hvt.Keeper
// the module manager
mm *module.Manager
@ -169,6 +175,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
gov.StoreKey, params.StoreKey, upgrade.StoreKey, evidence.StoreKey,
validatorvesting.StoreKey, auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey,
bep3.StoreKey, kavadist.StoreKey, incentive.StoreKey, issuance.StoreKey, committee.StoreKey,
hvt.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -198,6 +205,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
issuanceSubspace := app.paramsKeeper.Subspace(issuance.DefaultParamspace)
harvestSubspace := app.paramsKeeper.Subspace(hvt.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -362,6 +370,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.accountKeeper,
app.supplyKeeper,
)
app.harvestKeeper = hvt.NewKeeper(
app.cdc,
keys[hvt.StoreKey],
harvestSubspace,
app.accountKeeper,
app.supplyKeeper,
&stakingKeeper)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -392,6 +407,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
hvt.NewAppModule(app.harvestKeeper, app.supplyKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
@ -402,7 +418,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
app.mm.SetOrderBeginBlockers(
upgrade.ModuleName, mint.ModuleName, distr.ModuleName, slashing.ModuleName,
validatorvesting.ModuleName, kavadist.ModuleName, auction.ModuleName, cdp.ModuleName,
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
bep3.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName, hvt.ModuleName,
)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
@ -413,7 +429,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName,
bep3.ModuleName, kavadist.ModuleName, incentive.ModuleName, committee.ModuleName, issuance.ModuleName, hvt.ModuleName,
supply.ModuleName, // calculates the total supply from account - should run after modules that modify accounts in genesis
crisis.ModuleName, // runs the invariants at genesis - should run after other modules
genutil.ModuleName, // genutils must occur after staking so that pools are properly initialized with tokens from genesis accounts.
@ -444,6 +460,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
hvt.NewAppModule(app.harvestKeeper, app.supplyKeeper),
)
app.sm.RegisterStoreDecoders()

View File

@ -34,6 +34,7 @@ import (
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/committee"
"github.com/kava-labs/kava/x/hvt"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/issuance"
"github.com/kava-labs/kava/x/kavadist"
@ -84,10 +85,11 @@ func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefee
func (tApp TestApp) GetBep3Keeper() bep3.Keeper { return tApp.bep3Keeper }
func (tApp TestApp) GetKavadistKeeper() kavadist.Keeper { return tApp.kavadistKeeper }
func (tApp TestApp) GetIncentiveKeeper() incentive.Keeper { return tApp.incentiveKeeper }
func (tApp TestApp) GetHarvestKeeper() hvt.Keeper { return tApp.harvestKeeper }
func (tApp TestApp) GetCommitteeKeeper() committee.Keeper { return tApp.committeeKeeper }
func (tApp TestApp) GetIssuanceKeeper() issuance.Keeper { return tApp.issuanceKeeper }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
// InitializeFromGenesisStates calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {
// Create a default genesis state and overwrite with provided values
genesisState := NewDefaultGenesisState()

15
x/hvt/abci.go Normal file
View File

@ -0,0 +1,15 @@
package hvt
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// BeginBlocker applies rewards to liquidity providers and delegators according to params
func BeginBlocker(ctx sdk.Context, k Keeper) {
k.ApplyDepositRewards(ctx)
if k.ShouldDistributeValidatorRewards(ctx, k.BondDenom(ctx)) {
k.ApplyDelegationRewards(ctx, k.BondDenom(ctx))
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), k.BondDenom(ctx))
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}

134
x/hvt/alias.go Normal file
View File

@ -0,0 +1,134 @@
package hvt
// DO NOT EDIT - generated by aliasgen tool (github.com/rhuairahrighairidh/aliasgen)
import (
"github.com/kava-labs/kava/x/hvt/keeper"
"github.com/kava-labs/kava/x/hvt/types"
)
const (
BeginningOfMonth = keeper.BeginningOfMonth
MidMonth = keeper.MidMonth
PaymentHour = keeper.PaymentHour
AttributeKeyBlockHeight = types.AttributeKeyBlockHeight
AttributeKeyClaimAmount = types.AttributeKeyClaimAmount
AttributeKeyClaimHolder = types.AttributeKeyClaimHolder
AttributeKeyClaimMultiplier = types.AttributeKeyClaimMultiplier
AttributeKeyDeposit = types.AttributeKeyDeposit
AttributeKeyDepositDenom = types.AttributeKeyDepositDenom
AttributeKeyDepositType = types.AttributeKeyDepositType
AttributeKeyDepositor = types.AttributeKeyDepositor
AttributeKeyRewardsDistribution = types.AttributeKeyRewardsDistribution
AttributeValueCategory = types.AttributeValueCategory
DefaultParamspace = types.DefaultParamspace
DelegatorAccount = types.DelegatorAccount
EventTypeClaimHarvestReward = types.EventTypeClaimHarvestReward
EventTypeDeleteHarvestDeposit = types.EventTypeDeleteHarvestDeposit
EventTypeHarvestDelegatorDistribution = types.EventTypeHarvestDelegatorDistribution
EventTypeHarvestDeposit = types.EventTypeHarvestDeposit
EventTypeHarvestLPDistribution = types.EventTypeHarvestLPDistribution
EventTypeHarvestWithdrawal = types.EventTypeHarvestWithdrawal
LP = types.LP
LPAccount = types.LPAccount
Large = types.Large
Medium = types.Medium
ModuleAccountName = types.ModuleAccountName
ModuleName = types.ModuleName
QuerierRoute = types.QuerierRoute
QueryGetClaims = types.QueryGetClaims
QueryGetDeposits = types.QueryGetDeposits
QueryGetModuleAccounts = types.QueryGetModuleAccounts
QueryGetParams = types.QueryGetParams
RouterKey = types.RouterKey
Small = types.Small
Stake = types.Stake
StoreKey = types.StoreKey
)
var (
// function aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
ClaimKey = types.ClaimKey
DefaultGenesisState = types.DefaultGenesisState
DefaultParams = types.DefaultParams
DepositKey = types.DepositKey
DepositTypeIteratorKey = types.DepositTypeIteratorKey
GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
NewClaim = types.NewClaim
NewDelegatorDistributionSchedule = types.NewDelegatorDistributionSchedule
NewDeposit = types.NewDeposit
NewDistributionSchedule = types.NewDistributionSchedule
NewGenesisState = types.NewGenesisState
NewMsgClaimReward = types.NewMsgClaimReward
NewMsgDeposit = types.NewMsgDeposit
NewMsgWithdraw = types.NewMsgWithdraw
NewMultiplier = types.NewMultiplier
NewParams = types.NewParams
NewPeriod = types.NewPeriod
NewQueryAccountParams = types.NewQueryAccountParams
NewQueryClaimParams = types.NewQueryClaimParams
NewQueryDepositParams = types.NewQueryDepositParams
ParamKeyTable = types.ParamKeyTable
RegisterCodec = types.RegisterCodec
// variable aliases
ClaimsKeyPrefix = types.ClaimsKeyPrefix
DefaultActive = types.DefaultActive
DefaultDelegatorSchedules = types.DefaultDelegatorSchedules
DefaultDistributionTimes = types.DefaultDistributionTimes
DefaultGovSchedules = types.DefaultGovSchedules
DefaultLPSchedules = types.DefaultLPSchedules
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
DepositTypesClaimQuery = types.DepositTypesClaimQuery
DepositTypesDepositQuery = types.DepositTypesDepositQuery
DepositsKeyPrefix = types.DepositsKeyPrefix
ErrAccountNotFound = types.ErrAccountNotFound
ErrClaimExpired = types.ErrClaimExpired
ErrClaimNotFound = types.ErrClaimNotFound
ErrDepositNotFound = types.ErrDepositNotFound
ErrGovScheduleNotFound = types.ErrGovScheduleNotFound
ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
ErrInvaliWithdrawAmount = types.ErrInvaliWithdrawAmount
ErrInvalidAccountType = types.ErrInvalidAccountType
ErrInvalidDepositDenom = types.ErrInvalidDepositDenom
ErrInvalidDepositType = types.ErrInvalidDepositType
ErrInvalidMultiplier = types.ErrInvalidMultiplier
ErrLPScheduleNotFound = types.ErrLPScheduleNotFound
ErrZeroClaim = types.ErrZeroClaim
GovDenom = types.GovDenom
KeyActive = types.KeyActive
KeyDelegatorSchedule = types.KeyDelegatorSchedule
KeyLPSchedules = types.KeyLPSchedules
ModuleCdc = types.ModuleCdc
PreviousBlockTimeKey = types.PreviousBlockTimeKey
PreviousDelegationDistributionKey = types.PreviousDelegationDistributionKey
)
type (
Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper
Claim = types.Claim
DelegatorDistributionSchedule = types.DelegatorDistributionSchedule
DelegatorDistributionSchedules = types.DelegatorDistributionSchedules
Deposit = types.Deposit
DepositType = types.DepositType
DistributionSchedule = types.DistributionSchedule
DistributionSchedules = types.DistributionSchedules
GenesisDistributionTime = types.GenesisDistributionTime
GenesisDistributionTimes = types.GenesisDistributionTimes
GenesisState = types.GenesisState
MsgClaimReward = types.MsgClaimReward
MsgDeposit = types.MsgDeposit
MsgWithdraw = types.MsgWithdraw
Multiplier = types.Multiplier
MultiplierName = types.MultiplierName
Multipliers = types.Multipliers
Params = types.Params
QueryAccountParams = types.QueryAccountParams
QueryClaimParams = types.QueryClaimParams
QueryDepositParams = types.QueryDepositParams
StakingKeeper = types.StakingKeeper
SupplyKeeper = types.SupplyKeeper
)

240
x/hvt/client/cli/query.go Normal file
View File

@ -0,0 +1,240 @@
package cli
import (
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/hvt/types"
)
// flags for cli queries
const (
flagName = "name"
flagDepositDenom = "deposit-denom"
flagOwner = "owner"
flagDepositType = "deposit-type"
)
// GetQueryCmd returns the cli query commands for the harvest module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
harvestQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the harvest module",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
harvestQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
queryModAccountsCmd(queryRoute, cdc),
queryDepositsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc),
)...)
return harvestQueryCmd
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the harvest module parameters",
Long: "Get the current global harvest module parameters.",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetParams)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var params types.Params
if err := cdc.UnmarshalJSON(res, &params); err != nil {
return fmt.Errorf("failed to unmarshal params: %w", err)
}
return cliCtx.PrintOutput(params)
},
}
}
func queryModAccountsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "accounts",
Short: "query harvest module accounts with optional filter",
Long: strings.TrimSpace(`Query for all harvest module accounts or a specific account using the name flag:
Example:
$ kvcli q harvest accounts
$ kvcli q harvest accounts --name harvest|harvest_delegator_distribution|harvest_lp_distribution`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
name := viper.GetString(flagName)
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
params := types.NewQueryAccountParams(page, limit, name)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetModuleAccounts)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var modAccounts []supplyexported.ModuleAccountI
if err := cdc.UnmarshalJSON(res, &modAccounts); err != nil {
return fmt.Errorf("failed to unmarshal module accounts: %w", err)
}
return cliCtx.PrintOutput(modAccounts)
},
}
}
func queryDepositsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposits",
Short: "query harvest module deposits with optional filters",
Long: strings.TrimSpace(`query for all harvest module deposits or a specific deposit using flags:
Example:
$ kvcli q harvest deposits
$ kvcli q harvest deposits --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny --deposit-type lp --deposit-denom bnb
$ kvcli q harvest deposits --deposit-type stake --deposit-denom ukava
$ kvcli q harvest deposits --deposit-denom btcb`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
var owner sdk.AccAddress
var depositType types.DepositType
ownerBech := viper.GetString(flagOwner)
depositDenom := viper.GetString(flagDepositDenom)
depositTypeStr := viper.GetString(flagDepositType)
if len(ownerBech) != 0 {
depositOwner, err := sdk.AccAddressFromBech32(ownerBech)
if err != nil {
return err
}
owner = depositOwner
}
if len(depositTypeStr) != 0 {
if err := types.DepositType(depositTypeStr).IsValid(); err != nil {
return err
}
depositType = types.DepositType(depositTypeStr)
}
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
params := types.NewQueryDepositParams(page, limit, depositDenom, owner, depositType)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetDeposits)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var deposits []types.Deposit
if err := cdc.UnmarshalJSON(res, &deposits); err != nil {
return fmt.Errorf("failed to unmarshal deposits: %w", err)
}
return cliCtx.PrintOutput(deposits)
},
}
}
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claims",
Short: "query harvest module claims with optional filters",
Long: strings.TrimSpace(`query for all harvest module claims or a specific claim using flags:
Example:
$ kvcli q harvest claims
$ kvcli q harvest claims --owner kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny --deposit-type lp --deposit-denom bnb
$ kvcli q harvest claims --deposit-type stake --deposit-denom ukava
$ kvcli q harvest claims --deposit-denom btcb`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
var owner sdk.AccAddress
var depositType types.DepositType
ownerBech := viper.GetString(flagOwner)
depositDenom := viper.GetString(flagDepositDenom)
depositTypeStr := viper.GetString(flagDepositType)
if len(ownerBech) != 0 {
claimOwner, err := sdk.AccAddressFromBech32(ownerBech)
if err != nil {
return err
}
owner = claimOwner
}
if len(depositTypeStr) != 0 {
if err := types.DepositType(depositTypeStr).IsValid(); err != nil {
return err
}
depositType = types.DepositType(depositTypeStr)
}
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
params := types.NewQueryDepositParams(page, limit, depositDenom, owner, depositType)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var claims []types.Claim
if err := cdc.UnmarshalJSON(res, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims: %w", err)
}
return cliCtx.PrintOutput(claims)
},
}
}

118
x/hvt/client/cli/tx.go Normal file
View File

@ -0,0 +1,118 @@
package cli
import (
"bufio"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/hvt/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
harvestTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: fmt.Sprintf("%s transactions subcommands", types.ModuleName),
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
harvestTxCmd.AddCommand(flags.PostCommands(
getCmdDeposit(cdc),
getCmdWithdraw(cdc),
getCmdClaimReward(cdc),
)...)
return harvestTxCmd
}
func getCmdDeposit(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposit [amount] [deposit-type]",
Short: "deposit coins to harvest",
Args: cobra.ExactArgs(3),
Example: fmt.Sprintf(
`%s tx %s deposit 10000000bnb lp --from <key>`, version.ClientName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
amount, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
msg := types.NewMsgDeposit(cliCtx.GetFromAddress(), amount, args[1])
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdWithdraw(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "withdraw [amount] [deposit-type]",
Short: "withdraw coins from harvest",
Args: cobra.ExactArgs(3),
Example: fmt.Sprintf(
`%s tx %s withdraw 10000000bnb lp --from <key>`, version.ClientName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
amount, err := sdk.ParseCoin(args[0])
if err != nil {
return err
}
msg := types.NewMsgWithdraw(cliCtx.GetFromAddress(), amount, args[1])
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}
func getCmdClaimReward(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claim [receiver-addr] [deposit-denom] [deposit-type] [multiplier]",
Short: "claim HARD tokens to receiver address",
Long: strings.TrimSpace(
`sends accumulated HARD tokens from the harvest module account to the receiver address.
Note that receiver address should match the sender address,
unless the sender is a validator-vesting account`),
Args: cobra.ExactArgs(4),
Example: fmt.Sprintf(
`%s tx %s claim kava1hgcfsuwc889wtdmt8pjy7qffua9dd2tralu64j bnb lp large --from <key>`, version.ClientName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
receiver, err := sdk.AccAddressFromBech32(args[1])
if err != nil {
return err
}
msg := types.NewMsgClaimReward(cliCtx.GetFromAddress(), receiver, args[2], args[3], args[4])
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

196
x/hvt/client/rest/query.go Normal file
View File

@ -0,0 +1,196 @@
package rest
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/kava-labs/kava/x/hvt/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/deposits", types.ModuleName), queryDepositsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/accounts", types.ModuleName), queryModAccountsHandlerFn(cliCtx)).Methods("GET")
}
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
route := fmt.Sprintf("custom/%s/parameters", types.QuerierRoute)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryDepositsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
var depositDenom string
var depositOwner sdk.AccAddress
var depositType types.DepositType
if x := r.URL.Query().Get(RestDenom); len(x) != 0 {
depositDenom = strings.TrimSpace(x)
}
if x := r.URL.Query().Get(RestOwner); len(x) != 0 {
depositOwnerStr := strings.ToLower(strings.TrimSpace(x))
depositOwner, err = sdk.AccAddressFromBech32(depositOwnerStr)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from deposit owner %s", depositOwnerStr))
}
}
if x := r.URL.Query().Get(RestType); len(x) != 0 {
depositTypeStr := strings.ToLower(strings.TrimSpace(x))
err := types.DepositType(depositTypeStr).IsValid()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
depositType = types.DepositType(depositTypeStr)
}
params := types.NewQueryDepositParams(page, limit, depositDenom, depositOwner, depositType)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetDeposits)
res, height, err := cliCtx.QueryWithData(route, bz)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
var depositDenom string
var claimOwner sdk.AccAddress
var depositType types.DepositType
if x := r.URL.Query().Get(RestDenom); len(x) != 0 {
depositDenom = strings.TrimSpace(x)
}
if x := r.URL.Query().Get(RestOwner); len(x) != 0 {
claimOwnerStr := strings.ToLower(strings.TrimSpace(x))
claimOwner, err = sdk.AccAddressFromBech32(claimOwnerStr)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from claim owner %s", claimOwnerStr))
}
}
if x := r.URL.Query().Get(RestType); len(x) != 0 {
depositTypeStr := strings.ToLower(strings.TrimSpace(x))
err := types.DepositType(depositTypeStr).IsValid()
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
depositType = types.DepositType(depositTypeStr)
}
params := types.NewQueryDepositParams(page, limit, depositDenom, claimOwner, depositType)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetClaims)
res, height, err := cliCtx.QueryWithData(route, bz)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryModAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
// Parse the query height
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
var name string
if x := r.URL.Query().Get(RestName); len(x) != 0 {
name = strings.TrimSpace(x)
}
params := types.NewQueryAccountParams(page, limit, name)
bz, err := cliCtx.Codec.MarshalJSON(params)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
route := fmt.Sprintf("custom/%s/%s", types.ModuleName, types.QueryGetModuleAccounts)
res, height, err := cliCtx.QueryWithData(route, bz)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}

50
x/hvt/client/rest/rest.go Normal file
View File

@ -0,0 +1,50 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// REST variable names
// nolint
const (
RestOwner = "owner"
RestDenom = "deposit-denom"
RestType = "deposit-type"
RestName = "name"
)
// RegisterRoutes registers harvest-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}
// PostCreateDepositReq defines the properties of a deposit create request's body
type PostCreateDepositReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// PostCreateWithdrawReq defines the properties of a deposit withdraw request's body
type PostCreateWithdrawReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// PostClaimReq defines the properties of a claim reward request's body
type PostClaimReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
Multiplier string `json:"multiplier" yaml:"multiplier"`
}

85
x/hvt/client/rest/tx.go Normal file
View File

@ -0,0 +1,85 @@
package rest
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/cosmos/cosmos-sdk/x/auth/client/utils"
"github.com/kava-labs/kava/x/hvt/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/deposit", types.ModuleName), postDepositHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/withdraw", types.ModuleName), postWithdrawHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc(fmt.Sprintf("/%s/claim", types.ModuleName), postClaimHandlerFn(cliCtx)).Methods("POST")
}
func postDepositHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode POST request body
var req PostCreateDepositReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgDeposit(req.From, req.Amount, strings.ToLower(req.DepositType))
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
func postWithdrawHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode POST request body
var req PostCreateWithdrawReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgWithdraw(req.From, req.Amount, strings.ToLower(req.DepositType))
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}
func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Decode POST request body
var req PostClaimReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgClaimReward(req.From, req.Receiver, req.DepositDenom, strings.ToLower(req.DepositType), req.Multiplier)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, req.BaseReq, []sdk.Msg{msg})
}
}

65
x/hvt/genesis.go Normal file
View File

@ -0,0 +1,65 @@
package hvt
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hvt/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, k Keeper, supplyKeeper types.SupplyKeeper, gs GenesisState) {
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", ModuleName, err))
}
k.SetParams(ctx, gs.Params)
// only set the previous block time if it's different than default
if !gs.PreviousBlockTime.Equal(DefaultPreviousBlockTime) {
k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
}
for _, pdt := range gs.PreviousDistributionTimes {
if !pdt.PreviousDistributionTime.Equal(DefaultPreviousBlockTime) {
k.SetPreviousDelegationDistribution(ctx, pdt.PreviousDistributionTime, pdt.Denom)
}
}
// check if the module account exists
LPModuleAcc := supplyKeeper.GetModuleAccount(ctx, LPAccount)
if LPModuleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", LPAccount))
}
// check if the module account exists
DelegatorModuleAcc := supplyKeeper.GetModuleAccount(ctx, DelegatorAccount)
if DelegatorModuleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", DelegatorAccount))
}
// check if the module account exists
DepositModuleAccount := supplyKeeper.GetModuleAccount(ctx, ModuleAccountName)
if DepositModuleAccount == nil {
panic(fmt.Sprintf("%s module account has not been set", DepositModuleAccount))
}
}
// ExportGenesis export genesis state for harvest module
func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = DefaultPreviousBlockTime
}
previousDistTimes := GenesisDistributionTimes{}
for _, dds := range params.DelegatorDistributionSchedules {
previousDistTime, found := k.GetPreviousDelegatorDistribution(ctx, dds.DistributionSchedule.DepositDenom)
if found {
previousDistTimes = append(previousDistTimes, GenesisDistributionTime{PreviousDistributionTime: previousDistTime, Denom: dds.DistributionSchedule.DepositDenom})
}
}
return NewGenesisState(params, previousBlockTime, previousDistTimes)
}

82
x/hvt/handler.go Normal file
View File

@ -0,0 +1,82 @@
package hvt
import (
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/hvt/keeper"
"github.com/kava-labs/kava/x/hvt/types"
)
// NewHandler creates an sdk.Handler for harvest messages
func NewHandler(k Keeper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) (*sdk.Result, error) {
ctx = ctx.WithEventManager(sdk.NewEventManager())
switch msg := msg.(type) {
case types.MsgClaimReward:
return handleMsgClaimReward(ctx, k, msg)
case types.MsgDeposit:
return handleMsgDeposit(ctx, k, msg)
case types.MsgWithdraw:
return handleMsgWithdraw(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
}
}
func handleMsgClaimReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimReward) (*sdk.Result, error) {
err := k.ClaimReward(ctx, msg.Sender, msg.Receiver, msg.DepositDenom, types.DepositType(strings.ToLower(msg.DepositType)), types.MultiplierName(strings.ToLower(msg.MultiplierName)))
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgDeposit(ctx sdk.Context, k keeper.Keeper, msg types.MsgDeposit) (*sdk.Result, error) {
err := k.Deposit(ctx, msg.Depositor, msg.Amount, types.DepositType(strings.ToLower(msg.DepositType)))
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}
func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw) (*sdk.Result, error) {
err := k.Withdraw(ctx, msg.Depositor, msg.Amount, types.DepositType(strings.ToLower(msg.DepositType)))
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Depositor.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}

148
x/hvt/keeper/claim.go Normal file
View File

@ -0,0 +1,148 @@
package keeper
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/hvt/types"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
const (
// BeginningOfMonth harvest rewards that are claimed after the 15th at 14:00UTC of the month always vest on the first of the month
BeginningOfMonth = 1
// MidMonth harvest rewards that are claimed before the 15th at 14:00UTC of the month always vest on the 15 of the month
MidMonth = 15
// PaymentHour harvest rewards always vest at 14:00UTC
PaymentHour = 14
)
// ClaimReward sends the reward amount to the reward owner and deletes the claim from the store
func (k Keeper) ClaimReward(ctx sdk.Context, claimHolder sdk.AccAddress, receiver sdk.AccAddress, depositDenom string, depositType types.DepositType, multiplier types.MultiplierName) error {
claim, found := k.GetClaim(ctx, claimHolder, depositDenom, depositType)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "no %s %s claim found for %s", depositDenom, depositType, claimHolder)
}
err := k.validateSenderReceiver(ctx, claimHolder, receiver)
if err != nil {
return err
}
switch depositType {
case types.LP:
err = k.claimLPReward(ctx, claim, receiver, multiplier)
case types.Stake:
err = k.claimDelegatorReward(ctx, claim, receiver, multiplier)
default:
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
}
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaimHarvestReward,
sdk.NewAttribute(sdk.AttributeKeyAmount, claim.Amount.String()),
sdk.NewAttribute(types.AttributeKeyClaimHolder, claimHolder.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, depositDenom),
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
sdk.NewAttribute(types.AttributeKeyClaimMultiplier, string(multiplier)),
),
)
k.DeleteClaim(ctx, claim)
return nil
}
// GetPeriodLength returns the length of the period based on the input blocktime and multiplier
// note that pay dates are always the 1st or 15th of the month at 14:00UTC.
func (k Keeper) GetPeriodLength(ctx sdk.Context, multiplier types.Multiplier) (int64, error) {
switch multiplier.Name {
case types.Small:
return 0, nil
case types.Medium, types.Large:
currentDay := ctx.BlockTime().Day()
payDay := BeginningOfMonth
monthOffset := int64(1)
if currentDay < MidMonth || (currentDay == MidMonth && ctx.BlockTime().Hour() < PaymentHour) {
payDay = MidMonth
monthOffset = int64(0)
}
periodEndDate := time.Date(ctx.BlockTime().Year(), ctx.BlockTime().Month(), payDay, PaymentHour, 0, 0, 0, time.UTC).AddDate(0, int(multiplier.MonthsLockup+monthOffset), 0)
return periodEndDate.Unix() - ctx.BlockTime().Unix(), nil
}
return 0, types.ErrInvalidMultiplier
}
func (k Keeper) claimLPReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
lps, found := k.GetLPSchedule(ctx, claim.DepositDenom)
if !found {
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
}
multiplier, found := lps.GetMultiplier(multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
if ctx.BlockTime().After(lps.ClaimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), lps.ClaimEnd)
}
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
return k.SendTimeLockedCoinsToAccount(ctx, types.LPAccount, receiver, sdk.NewCoins(rewardCoin), length)
}
func (k Keeper) claimDelegatorReward(ctx sdk.Context, claim types.Claim, receiver sdk.AccAddress, multiplierName types.MultiplierName) error {
dss, found := k.GetDelegatorSchedule(ctx, claim.DepositDenom)
if !found {
return sdkerrors.Wrapf(types.ErrLPScheduleNotFound, claim.DepositDenom)
}
multiplier, found := dss.DistributionSchedule.GetMultiplier(multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
if ctx.BlockTime().After(dss.DistributionSchedule.ClaimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), dss.DistributionSchedule.ClaimEnd)
}
rewardAmount := sdk.NewDecFromInt(claim.Amount.Amount).Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Amount.Denom, rewardAmount)
length, err := k.GetPeriodLength(ctx, multiplier)
if err != nil {
return err
}
return k.SendTimeLockedCoinsToAccount(ctx, types.DelegatorAccount, receiver, sdk.NewCoins(rewardCoin), length)
}
func (k Keeper) validateSenderReceiver(ctx sdk.Context, sender, receiver sdk.AccAddress) error {
senderAcc := k.accountKeeper.GetAccount(ctx, sender)
if senderAcc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, sender.String())
}
switch senderAcc.(type) {
case *validatorvesting.ValidatorVestingAccount:
if sender.Equals(receiver) {
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", senderAcc)
}
default:
if !sender.Equals(receiver) {
return sdkerrors.Wrapf(types.ErrInvalidReceiver, "%s", sender)
}
}
return nil
}

470
x/hvt/keeper/claim_test.go Normal file
View File

@ -0,0 +1,470 @@
package keeper_test
import (
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hvt/types"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
func (suite *KeeperTestSuite) TestClaim() {
type args struct {
claimOwner sdk.AccAddress
receiver sdk.AccAddress
denom string
depositType types.DepositType
multiplier types.MultiplierName
blockTime time.Time
createClaim bool
claimAmount sdk.Coin
validatorVesting bool
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
expectedVestingAccount bool
expectedVestingLength int64
}
type errArgs struct {
expectPass bool
contains string
}
type claimTest struct {
name string
args args
errArgs errArgs
}
testCases := []claimTest{
{
"valid liquid claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(33)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(967))),
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid liquid delegator claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.Stake,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(33)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(967))),
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid medium vesting claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(50)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(950))),
expectedVestingAccount: true,
expectedVestingLength: 16848000,
multiplier: types.Medium,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid large vesting claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedVestingAccount: true,
expectedVestingLength: 64281600,
multiplier: types.Large,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid validator vesting",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test2"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedVestingAccount: true,
expectedVestingLength: 64281600,
multiplier: types.Large,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid validator vesting",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedVestingAccount: true,
expectedVestingLength: 64281600,
multiplier: types.Large,
},
errArgs{
expectPass: false,
contains: "receiver account type not supported",
},
},
{
"claim not found",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: false,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: false,
contains: "claim not found",
},
},
{
"claim expired",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2022, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: false,
contains: "claim period expired",
},
},
{
"different receiver address",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
receiver: sdk.AccAddress(crypto.AddressHash([]byte("test2"))),
denom: "bnb",
depositType: types.LP,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
createClaim: true,
claimAmount: sdk.NewCoin("hard", sdk.NewInt(100)),
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedVestingAccount: true,
expectedVestingLength: 64281600,
multiplier: types.Large,
},
errArgs{
expectPass: false,
contains: "receiver account must match sender account",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.claimOwner, tc.args.receiver},
[]sdk.Coins{
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
if tc.args.validatorVesting {
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, tc.args.claimOwner)
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err := vesting.NewBaseVestingAccount(
bacc,
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(20))), time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix()+100)
suite.Require().NoError(err)
vva := validatorvesting.NewValidatorVestingAccountRaw(
bva,
time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix(),
vesting.Periods{
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}},
sdk.ConsAddress(crypto.AddressHash([]byte("test"))),
sdk.AccAddress{},
95,
)
err = vva.Validate()
suite.Require().NoError(err)
ak.SetAccount(ctx, vva)
}
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.LPAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
supplyKeeper.MintCoins(ctx, types.DelegatorAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
if tc.args.createClaim {
claim := types.NewClaim(tc.args.claimOwner, tc.args.denom, tc.args.claimAmount, tc.args.depositType)
suite.Require().NotPanics(func() { suite.keeper.SetClaim(suite.ctx, claim) })
}
err := suite.keeper.ClaimReward(suite.ctx, tc.args.claimOwner, tc.args.receiver, tc.args.denom, tc.args.depositType, tc.args.multiplier)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.receiver)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.LPAccount)
if tc.args.depositType == types.Stake {
mAcc = suite.getModuleAccount(types.DelegatorAccount)
}
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
if tc.args.expectedVestingAccount {
suite.Require().True(ok)
suite.Require().Equal(tc.args.expectedVestingLength, vacc.VestingPeriods[0].Length)
} else {
suite.Require().False(ok)
}
_, f := suite.keeper.GetClaim(ctx, tc.args.claimOwner, tc.args.denom, tc.args.depositType)
suite.Require().False(f)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestGetPeriodLength() {
type args struct {
blockTime time.Time
multiplier types.Multiplier
expectedLength int64
}
type errArgs struct {
expectPass bool
contains string
}
type periodTest struct {
name string
args args
errArgs errArgs
}
testCases := []periodTest{
{
name: "first half of month",
args: args{
blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2021, 5, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "first half of month long lockup",
args: args{
blockTime: time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2022, 11, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 11, 2, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "second half of month",
args: args{
blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "second half of month long lockup",
args: args{
blockTime: time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2023, 1, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 31, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "end of feb",
args: args{
blockTime: time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2021, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2021, 2, 28, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "leap year",
args: args{
blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2020, 9, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "leap year long lockup",
args: args{
blockTime: time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Large, 24, sdk.MustNewDecFromStr("1")),
expectedLength: time.Date(2022, 3, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 2, 29, 15, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "exactly half of month",
args: args{
blockTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2021, 7, 1, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "just before half of month",
args: args{
blockTime: time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC),
multiplier: types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.333333")),
expectedLength: time.Date(2021, 6, 15, 14, 0, 0, 0, time.UTC).Unix() - time.Date(2020, 12, 15, 13, 59, 59, 0, time.UTC).Unix(),
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
ctx := suite.ctx.WithBlockTime(tc.args.blockTime)
length, err := suite.keeper.GetPeriodLength(ctx, tc.args.multiplier)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
suite.Require().Equal(tc.args.expectedLength, length)
} else {
suite.Require().Error(err)
}
})
}
}

136
x/hvt/keeper/deposit.go Normal file
View File

@ -0,0 +1,136 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/hvt/types"
)
// Deposit deposit
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositType types.DepositType) error {
err := k.ValidateDeposit(ctx, amount, depositType)
if err != nil {
return err
}
switch depositType {
case types.LP:
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, depositor, types.ModuleAccountName, sdk.NewCoins(amount))
default:
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
}
if err != nil {
return err
}
deposit, found := k.GetDeposit(ctx, depositor, amount.Denom, depositType)
if !found {
deposit = types.NewDeposit(depositor, amount, depositType)
} else {
deposit.Amount = deposit.Amount.Add(amount)
}
k.SetDeposit(ctx, deposit)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHarvestDeposit,
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
sdk.NewAttribute(types.AttributeKeyDepositor, deposit.Depositor.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, deposit.Amount.Denom),
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
),
)
return nil
}
// ValidateDeposit validates a deposit
func (k Keeper) ValidateDeposit(ctx sdk.Context, amount sdk.Coin, depositType types.DepositType) error {
var err error
switch depositType {
case types.LP:
err = k.ValidateLPDeposit(ctx, amount, depositType)
default:
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
}
if err != nil {
return err
}
return nil
}
// ValidateLPDeposit validates that a liquidity provider deposit
func (k Keeper) ValidateLPDeposit(ctx sdk.Context, amount sdk.Coin, depositType types.DepositType) error {
params := k.GetParams(ctx)
for _, lps := range params.LiquidityProviderSchedules {
if lps.DepositDenom == amount.Denom {
return nil
}
}
return sdkerrors.Wrapf(types.ErrInvalidDepositDenom, "liquidity provider denom %s not found", amount.Denom)
}
// Withdraw returns some or all of a deposit back to original depositor
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositType types.DepositType) error {
deposit, found := k.GetDeposit(ctx, depositor, amount.Denom, depositType)
if !found {
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no %s %s deposit found for %s", amount.Denom, depositType, depositor)
}
if !deposit.Amount.IsGTE(amount) {
return sdkerrors.Wrapf(types.ErrInvaliWithdrawAmount, "%s>%s", amount, deposit.Amount)
}
var err error
switch depositType {
case types.LP:
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, sdk.NewCoins(amount))
default:
return sdkerrors.Wrap(types.ErrInvalidDepositType, string(depositType))
}
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHarvestWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, amount.Denom),
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
),
)
if deposit.Amount.IsEqual(amount) {
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeDeleteHarvestDeposit,
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, amount.Denom),
sdk.NewAttribute(types.AttributeKeyDepositType, string(depositType)),
),
)
k.DeleteDeposit(ctx, deposit)
return nil
}
deposit.Amount = deposit.Amount.Sub(amount)
k.SetDeposit(ctx, deposit)
return nil
}
// GetTotalDeposited returns the total amount deposited for the input deposit type and deposit denom
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositType types.DepositType, depositDenom string) (total sdk.Int) {
var macc supplyExported.ModuleAccountI
switch depositType {
case types.LP:
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
}
return macc.GetCoins().AmountOf(depositDenom)
}

View File

@ -0,0 +1,332 @@
package keeper_test
import (
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hvt/types"
)
func (suite *KeeperTestSuite) TestDeposit() {
type args struct {
depositor sdk.AccAddress
amount sdk.Coin
depositType types.DepositType
numberDeposits int
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
}
type errArgs struct {
expectPass bool
contains string
}
type depositTest struct {
name string
args args
errArgs errArgs
}
testCases := []depositTest{
{
"valid",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
depositType: types.LP,
numberDeposits: 1,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid multi deposit",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
depositType: types.LP,
numberDeposits: 2,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(800)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid deposit type",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
amount: sdk.NewCoin("bnb", sdk.NewInt(100)),
depositType: types.Stake,
numberDeposits: 1,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "invalid deposit type",
},
},
{
"invalid deposit denom",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
amount: sdk.NewCoin("btcb", sdk.NewInt(100)),
depositType: types.LP,
numberDeposits: 1,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "invalid deposit denom",
},
},
{
"insufficient funds",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
amount: sdk.NewCoin("bnb", sdk.NewInt(10000)),
depositType: types.LP,
numberDeposits: 1,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
},
errArgs{
expectPass: false,
contains: "insufficient funds",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
// run the test
var err error
for i := 0; i < tc.args.numberDeposits; i++ {
err = suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.amount, tc.args.depositType)
}
// verify results
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.depositor)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
_, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor, tc.args.amount.Denom, tc.args.depositType)
suite.Require().True(f)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *KeeperTestSuite) TestWithdraw() {
type args struct {
depositor sdk.AccAddress
depositAmount sdk.Coin
withdrawAmount sdk.Coin
depositType types.DepositType
withdrawType types.DepositType
createDeposit bool
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
depositExists bool
finalDepositAmount sdk.Coin
}
type errArgs struct {
expectPass bool
contains string
}
type withdrawTest struct {
name string
args args
errArgs errArgs
}
testCases := []withdrawTest{
{
"valid partial withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(100)),
depositType: types.LP,
withdrawType: types.LP,
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
depositExists: true,
finalDepositAmount: sdk.NewCoin("bnb", sdk.NewInt(100)),
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid full withdraw",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
depositType: types.LP,
withdrawType: types.LP,
createDeposit: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.Coins(nil),
depositExists: false,
finalDepositAmount: sdk.Coin{},
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"deposit not found invalid denom",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
withdrawAmount: sdk.NewCoin("btcb", sdk.NewInt(200)),
depositType: types.LP,
withdrawType: types.LP,
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coin{},
},
errArgs{
expectPass: false,
contains: "deposit not found",
},
},
{
"deposit not found invalid deposit type",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
depositType: types.LP,
withdrawType: types.Stake,
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coin{},
},
errArgs{
expectPass: false,
contains: "deposit not found",
},
},
{
"withdraw exceeds deposit",
args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositAmount: sdk.NewCoin("bnb", sdk.NewInt(200)),
withdrawAmount: sdk.NewCoin("bnb", sdk.NewInt(300)),
depositType: types.LP,
withdrawType: types.LP,
createDeposit: true,
expectedAccountBalance: sdk.Coins{},
expectedModAccountBalance: sdk.Coins{},
depositExists: false,
finalDepositAmount: sdk.Coin{},
},
errArgs{
expectPass: false,
contains: "withdrawal amount exceeds deposit amount",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
if tc.args.createDeposit {
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount, tc.args.depositType)
suite.Require().NoError(err)
}
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount, tc.args.withdrawType)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.depositor)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor, tc.args.depositAmount.Denom, tc.args.depositType)
if tc.args.depositExists {
suite.Require().True(f)
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
} else {
suite.Require().False(f)
}
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}

184
x/hvt/keeper/keeper.go Normal file
View File

@ -0,0 +1,184 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params/subspace"
"github.com/kava-labs/kava/x/hvt/types"
)
// Keeper keeper for the harvest module
type Keeper struct {
key sdk.StoreKey
cdc *codec.Codec
paramSubspace subspace.Subspace
accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper
stakingKeeper types.StakingKeeper
}
// NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper {
if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
}
return Keeper{
key: key,
cdc: cdc,
paramSubspace: paramstore,
accountKeeper: ak,
supplyKeeper: sk,
stakingKeeper: stk,
}
}
// GetPreviousBlockTime get the blocktime for the previous block
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
b := store.Get([]byte{})
if b == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryBare(b, &blockTime)
return blockTime, true
}
// SetPreviousBlockTime set the time of the previous block
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryBare(blockTime))
}
// GetPreviousDelegatorDistribution get the time of the previous delegator distribution
func (k Keeper) GetPreviousDelegatorDistribution(ctx sdk.Context, denom string) (distTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDelegationDistributionKey)
bz := store.Get([]byte(denom))
if bz == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryBare(bz, &distTime)
return distTime, true
}
// SetPreviousDelegationDistribution set the time of the previous delegator distribution
func (k Keeper) SetPreviousDelegationDistribution(ctx sdk.Context, distTime time.Time, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDelegationDistributionKey)
store.Set([]byte(denom), k.cdc.MustMarshalBinaryBare(distTime))
}
// GetDeposit returns a deposit from the store for a particular depositor address, deposit denom, and deposit type
func (k Keeper) GetDeposit(ctx sdk.Context, depositor sdk.AccAddress, denom string, depositType types.DepositType) (types.Deposit, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
bz := store.Get(types.DepositKey(depositType, denom, depositor))
if bz == nil {
return types.Deposit{}, false
}
var deposit types.Deposit
k.cdc.MustUnmarshalBinaryBare(bz, &deposit)
return deposit, true
}
// SetDeposit sets the input deposit in the store, prefixed by the deposit type, deposit denom, and depositor address, in that order
func (k Keeper) SetDeposit(ctx sdk.Context, deposit types.Deposit) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(deposit)
store.Set(types.DepositKey(deposit.Type, deposit.Amount.Denom, deposit.Depositor), bz)
}
// DeleteDeposit deletes a deposit from the store
func (k Keeper) DeleteDeposit(ctx sdk.Context, deposit types.Deposit) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
store.Delete(types.DepositKey(deposit.Type, deposit.Amount.Denom, deposit.Depositor))
}
// IterateDeposits iterates over all deposit objects in the store and performs a callback function
func (k Keeper) IterateDeposits(ctx sdk.Context, cb func(deposit types.Deposit) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var deposit types.Deposit
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &deposit)
if cb(deposit) {
break
}
}
}
// IterateDepositsByTypeAndDenom iterates over all deposit objects in the store with the matching deposit type and deposit denom and performs a callback function
func (k Keeper) IterateDepositsByTypeAndDenom(ctx sdk.Context, depositType types.DepositType, depositDenom string, cb func(deposit types.Deposit) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.DepositsKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, types.DepositTypeIteratorKey(depositType, depositDenom))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var deposit types.Deposit
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &deposit)
if cb(deposit) {
break
}
}
}
// GetClaim returns a claim from the store for a particular claim owner, deposit denom, and deposit type
func (k Keeper) GetClaim(ctx sdk.Context, owner sdk.AccAddress, depositDenom string, depositType types.DepositType) (types.Claim, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
bz := store.Get(types.ClaimKey(depositType, depositDenom, owner))
if bz == nil {
return types.Claim{}, false
}
var claim types.Claim
k.cdc.MustUnmarshalBinaryBare(bz, &claim)
return claim, true
}
// SetClaim stores the input claim in the store, prefixed by the deposit type, deposit denom, and owner address, in that order
func (k Keeper) SetClaim(ctx sdk.Context, claim types.Claim) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(claim)
store.Set(types.ClaimKey(claim.Type, claim.DepositDenom, claim.Owner), bz)
}
// DeleteClaim deletes a claim from the store
func (k Keeper) DeleteClaim(ctx sdk.Context, claim types.Claim) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
store.Delete(types.ClaimKey(claim.Type, claim.DepositDenom, claim.Owner))
}
// IterateClaims iterates over all claim objects in the store and performs a callback function
func (k Keeper) IterateClaims(ctx sdk.Context, cb func(claim types.Claim) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var claim types.Claim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &claim)
if cb(claim) {
break
}
}
}
// IterateClaimsByTypeAndDenom iterates over all claim objects in the store with the matching deposit type and deposit denom and performs a callback function
func (k Keeper) IterateClaimsByTypeAndDenom(ctx sdk.Context, depositType types.DepositType, depositDenom string, cb func(claim types.Claim) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimsKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, types.DepositTypeIteratorKey(depositType, depositDenom))
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var claim types.Claim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &claim)
if cb(claim) {
break
}
}
}
// BondDenom returns the bond denom from the staking keeper
func (k Keeper) BondDenom(ctx sdk.Context) string {
return k.stakingKeeper.BondDenom(ctx)
}

165
x/hvt/keeper/keeper_test.go Normal file
View File

@ -0,0 +1,165 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hvt/keeper"
"github.com/kava-labs/kava/x/hvt/types"
)
// Test suite used for all keeper tests
type KeeperTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
}
// The default state used by each test
func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(1)
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
suite.addrs = addrs
}
func (suite *KeeperTestSuite) TestGetSetPreviousBlockTime() {
now := tmtime.Now()
_, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.Require().False(f)
suite.NotPanics(func() { suite.keeper.SetPreviousBlockTime(suite.ctx, now) })
pbt, f := suite.keeper.GetPreviousBlockTime(suite.ctx)
suite.True(f)
suite.Equal(now, pbt)
}
func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
now := tmtime.Now()
_, f := suite.keeper.GetPreviousDelegatorDistribution(suite.ctx, suite.keeper.BondDenom(suite.ctx))
suite.Require().False(f)
suite.NotPanics(func() {
suite.keeper.SetPreviousDelegationDistribution(suite.ctx, now, suite.keeper.BondDenom(suite.ctx))
})
pdt, f := suite.keeper.GetPreviousDelegatorDistribution(suite.ctx, suite.keeper.BondDenom(suite.ctx))
suite.True(f)
suite.Equal(now, pdt)
}
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().False(f)
suite.keeper.SetDeposit(suite.ctx, dep)
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().True(f)
suite.Require().Equal(dep, testDeposit)
suite.Require().NotPanics(func() { suite.keeper.DeleteDeposit(suite.ctx, dep) })
_, f = suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().False(f)
}
func (suite *KeeperTestSuite) TestIterateDeposits() {
for i := 0; i < 5; i++ {
dep := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
}
var deposits []types.Deposit
suite.keeper.IterateDeposits(suite.ctx, func(d types.Deposit) bool {
deposits = append(deposits, d)
return false
})
suite.Require().Equal(5, len(deposits))
}
func (suite *KeeperTestSuite) TestIterateDepositsByTypeAndDenom() {
for i := 0; i < 5; i++ {
depA := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "lp")
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depA) })
depB := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("bnb", sdk.NewInt(100)), "gov")
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depB) })
depC := types.NewDeposit(sdk.AccAddress("test"+string(i)), sdk.NewCoin("btcb", sdk.NewInt(100)), "lp")
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, depC) })
}
var bnbLPDeposits []types.Deposit
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "lp", "bnb", func(d types.Deposit) bool {
bnbLPDeposits = append(bnbLPDeposits, d)
return false
})
suite.Require().Equal(5, len(bnbLPDeposits))
var bnbGovDeposits []types.Deposit
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "gov", "bnb", func(d types.Deposit) bool {
bnbGovDeposits = append(bnbGovDeposits, d)
return false
})
suite.Require().Equal(5, len(bnbGovDeposits))
var btcbLPDeposits []types.Deposit
suite.keeper.IterateDepositsByTypeAndDenom(suite.ctx, "lp", "btcb", func(d types.Deposit) bool {
btcbLPDeposits = append(btcbLPDeposits, d)
return false
})
suite.Require().Equal(5, len(btcbLPDeposits))
var deposits []types.Deposit
suite.keeper.IterateDeposits(suite.ctx, func(d types.Deposit) bool {
deposits = append(deposits, d)
return false
})
suite.Require().Equal(15, len(deposits))
}
func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "lp")
_, f := suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().False(f)
suite.Require().NotPanics(func() { suite.keeper.SetClaim(suite.ctx, claim) })
testClaim, f := suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().True(f)
suite.Require().Equal(claim, testClaim)
suite.Require().NotPanics(func() { suite.keeper.DeleteClaim(suite.ctx, claim) })
_, f = suite.keeper.GetClaim(suite.ctx, sdk.AccAddress("test"), "bnb", "lp")
suite.Require().False(f)
}
func (suite *KeeperTestSuite) getAccount(addr sdk.AccAddress) authexported.Account {
ak := suite.app.GetAccountKeeper()
return ak.GetAccount(suite.ctx, addr)
}
func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.ModuleAccountI {
sk := suite.app.GetSupplyKeeper()
return sk.GetModuleAccount(suite.ctx, name)
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}

39
x/hvt/keeper/params.go Normal file
View File

@ -0,0 +1,39 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hvt/types"
)
// GetParams returns the params from the store
func (k Keeper) GetParams(ctx sdk.Context) types.Params {
var p types.Params
k.paramSubspace.GetParamSet(ctx, &p)
return p
}
// SetParams sets params on the store
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.DistributionSchedule, bool) {
params := k.GetParams(ctx)
for _, lps := range params.LiquidityProviderSchedules {
if lps.DepositDenom == denom {
return lps, true
}
}
return types.DistributionSchedule{}, false
}
func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.DelegatorDistributionSchedule, bool) {
params := k.GetParams(ctx)
for _, dds := range params.DelegatorDistributionSchedules {
if dds.DistributionSchedule.DepositDenom == denom {
return dds, true
}
}
return types.DelegatorDistributionSchedule{}, false
}

280
x/hvt/keeper/querier.go Normal file
View File

@ -0,0 +1,280 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/hvt/types"
)
// NewQuerier is the module level router for state queries
func NewQuerier(k Keeper) sdk.Querier {
return func(ctx sdk.Context, path []string, req abci.RequestQuery) (res []byte, err error) {
switch path[0] {
case types.QueryGetParams:
return queryGetParams(ctx, req, k)
case types.QueryGetModuleAccounts:
return queryGetModAccounts(ctx, req, k)
case types.QueryGetDeposits:
return queryGetDeposits(ctx, req, k)
case types.QueryGetClaims:
return queryGetClaims(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
// Get params
params := k.GetParams(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(k.cdc, params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetModAccounts(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryAccountParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
var accs []supplyexported.ModuleAccountI
if len(params.Name) > 0 {
acc := k.supplyKeeper.GetModuleAccount(ctx, types.LPAccount)
accs = append(accs, acc)
} else {
acc := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
accs = append(accs, acc)
acc = k.supplyKeeper.GetModuleAccount(ctx, types.LPAccount)
accs = append(accs, acc)
acc = k.supplyKeeper.GetModuleAccount(ctx, types.DelegatorAccount)
accs = append(accs, acc)
}
bz, err := codec.MarshalJSONIndent(k.cdc, accs)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetDeposits(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryDepositParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
depositDenom := false
owner := false
depositType := false
if len(params.DepositDenom) > 0 {
depositDenom = true
}
if len(params.Owner) > 0 {
owner = true
}
if len(params.DepositType) > 0 {
depositType = true
}
var deposits []types.Deposit
if depositDenom && owner && depositType {
deposit, found := k.GetDeposit(ctx, params.Owner, params.DepositDenom, params.DepositType)
if found {
deposits = append(deposits, deposit)
}
} else if depositDenom && owner {
for _, dt := range types.DepositTypesDepositQuery {
deposit, found := k.GetDeposit(ctx, params.Owner, params.DepositDenom, dt)
if found {
deposits = append(deposits, deposit)
}
}
} else if depositDenom && depositType {
k.IterateDepositsByTypeAndDenom(ctx, params.DepositType, params.DepositDenom, func(deposit types.Deposit) (stop bool) {
deposits = append(deposits, deposit)
return false
})
} else if owner && depositType {
schedules := k.GetParams(ctx).LiquidityProviderSchedules
for _, lps := range schedules {
deposit, found := k.GetDeposit(ctx, params.Owner, lps.DepositDenom, params.DepositType)
if found {
deposits = append(deposits, deposit)
}
}
} else if depositDenom {
for _, dt := range types.DepositTypesDepositQuery {
k.IterateDepositsByTypeAndDenom(ctx, dt, params.DepositDenom, func(deposit types.Deposit) (stop bool) {
deposits = append(deposits, deposit)
return false
})
}
} else if owner {
schedules := k.GetParams(ctx).LiquidityProviderSchedules
for _, lps := range schedules {
for _, dt := range types.DepositTypesDepositQuery {
deposit, found := k.GetDeposit(ctx, params.Owner, lps.DepositDenom, dt)
if found {
deposits = append(deposits, deposit)
}
}
}
} else if depositType {
schedules := k.GetParams(ctx).LiquidityProviderSchedules
for _, lps := range schedules {
k.IterateDepositsByTypeAndDenom(ctx, params.DepositType, lps.DepositDenom, func(deposit types.Deposit) (stop bool) {
deposits = append(deposits, deposit)
return false
})
}
} else {
k.IterateDeposits(ctx, func(deposit types.Deposit) (stop bool) {
deposits = append(deposits, deposit)
return false
})
}
start, end := client.Paginate(len(deposits), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
deposits = []types.Deposit{}
} else {
deposits = deposits[start:end]
}
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, deposits)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryClaimParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
depositDenom := false
owner := false
depositType := false
if len(params.DepositDenom) > 0 {
depositDenom = true
}
if len(params.Owner) > 0 {
owner = true
}
if len(params.DepositType) > 0 {
depositType = true
}
var claims []types.Claim
if depositDenom && owner && depositType {
claim, found := k.GetClaim(ctx, params.Owner, params.DepositDenom, params.DepositType)
if found {
claims = append(claims, claim)
}
} else if depositDenom && owner {
for _, dt := range types.DepositTypesClaimQuery {
claim, found := k.GetClaim(ctx, params.Owner, params.DepositDenom, dt)
if found {
claims = append(claims, claim)
}
}
} else if depositDenom && depositType {
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, params.DepositDenom, func(claim types.Claim) (stop bool) {
claims = append(claims, claim)
return false
})
} else if owner && depositType {
harvestParams := k.GetParams(ctx)
for _, lps := range harvestParams.LiquidityProviderSchedules {
claim, found := k.GetClaim(ctx, params.Owner, lps.DepositDenom, params.DepositType)
if found {
claims = append(claims, claim)
}
}
for _, dss := range harvestParams.DelegatorDistributionSchedules {
claim, found := k.GetClaim(ctx, params.Owner, dss.DistributionSchedule.DepositDenom, params.DepositType)
if found {
claims = append(claims, claim)
}
}
} else if depositDenom {
for _, dt := range types.DepositTypesClaimQuery {
k.IterateClaimsByTypeAndDenom(ctx, dt, params.DepositDenom, func(claim types.Claim) (stop bool) {
claims = append(claims, claim)
return false
})
}
} else if owner {
harvestParams := k.GetParams(ctx)
for _, lps := range harvestParams.LiquidityProviderSchedules {
for _, dt := range types.DepositTypesClaimQuery {
claim, found := k.GetClaim(ctx, params.Owner, lps.DepositDenom, dt)
if found {
claims = append(claims, claim)
}
}
}
for _, dds := range harvestParams.DelegatorDistributionSchedules {
for _, dt := range types.DepositTypesClaimQuery {
claim, found := k.GetClaim(ctx, params.Owner, dds.DistributionSchedule.DepositDenom, dt)
if found {
claims = append(claims, claim)
}
}
}
} else if depositType {
harvestParams := k.GetParams(ctx)
for _, lps := range harvestParams.LiquidityProviderSchedules {
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, lps.DepositDenom, func(claim types.Claim) (stop bool) {
claims = append(claims, claim)
return false
})
}
for _, dds := range harvestParams.DelegatorDistributionSchedules {
k.IterateClaimsByTypeAndDenom(ctx, params.DepositType, dds.DistributionSchedule.DepositDenom, func(claim types.Claim) (stop bool) {
claims = append(claims, claim)
return false
})
}
} else {
k.IterateClaims(ctx, func(claim types.Claim) (stop bool) {
claims = append(claims, claim)
return false
})
}
start, end := client.Paginate(len(claims), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
claims = []types.Claim{}
} else {
claims = claims[start:end]
}
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, claims)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

166
x/hvt/keeper/rewards.go Normal file
View File

@ -0,0 +1,166 @@
package keeper
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/kava-labs/kava/x/hvt/types"
)
// ApplyDepositRewards iterates over lp and gov deposits and updates the amount of rewards for each depositor
func (k Keeper) ApplyDepositRewards(ctx sdk.Context) {
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
return
}
params := k.GetParams(ctx)
if !params.Active {
return
}
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
for _, lps := range params.LiquidityProviderSchedules {
if !lps.Active {
continue
}
if lps.End.Before(ctx.BlockTime()) {
continue
}
totalDeposited := k.GetTotalDeposited(ctx, types.LP, lps.DepositDenom)
if totalDeposited.IsZero() {
continue
}
rewardsToDistribute := lps.RewardsPerSecond.Amount.Mul(timeElapsed)
if rewardsToDistribute.IsZero() {
continue
}
rewardsDistributed := sdk.ZeroInt()
k.IterateDepositsByTypeAndDenom(ctx, types.LP, lps.DepositDenom, func(dep types.Deposit) (stop bool) {
rewardsShare := sdk.NewDecFromInt(dep.Amount.Amount).Quo(sdk.NewDecFromInt(totalDeposited))
if rewardsShare.IsZero() {
return false
}
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsToDistribute)).RoundInt()
if rewardsEarned.IsZero() {
return false
}
k.AddToClaim(ctx, dep.Depositor, dep.Amount.Denom, dep.Type, sdk.NewCoin(lps.RewardsPerSecond.Denom, rewardsEarned))
rewardsDistributed = rewardsDistributed.Add(rewardsEarned)
return false
})
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHarvestLPDistribution,
sdk.NewAttribute(types.AttributeKeyBlockHeight, fmt.Sprintf("%d", ctx.BlockHeight())),
sdk.NewAttribute(types.AttributeKeyRewardsDistribution, rewardsDistributed.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, lps.DepositDenom),
),
)
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}
// ShouldDistributeValidatorRewards returns true if enough time has elapsed such that rewards should be distributed to delegators
func (k Keeper) ShouldDistributeValidatorRewards(ctx sdk.Context, denom string) bool {
previousDistributionTime, found := k.GetPreviousDelegatorDistribution(ctx, denom)
if !found {
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), denom)
return false
}
params := k.GetParams(ctx)
if !params.Active {
return false
}
for _, dds := range params.DelegatorDistributionSchedules {
if denom != dds.DistributionSchedule.DepositDenom {
continue
}
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistributionTime.Unix())
if timeElapsed.GTE(sdk.NewInt(int64(dds.DistributionFrequency))) {
return true
}
}
return false
}
// ApplyDelegationRewards iterates over each delegation object in the staking store and applies rewards according to the input delegation distribution schedule
func (k Keeper) ApplyDelegationRewards(ctx sdk.Context, denom string) {
dds, found := k.GetDelegatorSchedule(ctx, denom)
if !found {
return
}
if !dds.DistributionSchedule.Active {
return
}
bondMacc := k.stakingKeeper.GetBondedPool(ctx)
bondedCoinAmount := bondMacc.GetCoins().AmountOf(dds.DistributionSchedule.DepositDenom)
if bondedCoinAmount.IsZero() {
return
}
previousDistributionTime, found := k.GetPreviousDelegatorDistribution(ctx, dds.DistributionSchedule.DepositDenom)
if !found {
return
}
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistributionTime.Unix())
rewardsToDistribute := dds.DistributionSchedule.RewardsPerSecond.Amount.Mul(timeElapsed)
// create a map that has each validator address (sdk.ValAddress) as a key and the coversion factor for going from delegator shares to tokens for delegations to that validator.
// If a validator has never been slashed, the conversion factor will be 1.0, if they have been, it will be < 1.0
sharesToTokens := make(map[string]sdk.Dec)
k.stakingKeeper.IterateValidators(ctx, func(index int64, validator stakingexported.ValidatorI) (stop bool) {
if validator.GetTokens().IsZero() {
return false
}
// don't include a validator if it's unbonded - ie delegators don't accumulate rewards when delegated to an unbonded validator
if validator.GetStatus() == sdk.Unbonded {
return false
}
sharesToTokens[validator.GetOperator().String()] = (validator.GetDelegatorShares()).Quo(sdk.NewDecFromInt(validator.GetTokens()))
return false
})
rewardsDistributed := sdk.ZeroInt()
k.stakingKeeper.IterateAllDelegations(ctx, func(delegation stakingtypes.Delegation) (stop bool) {
conversionFactor, ok := sharesToTokens[delegation.ValidatorAddress.String()]
if ok {
delegationTokens := conversionFactor.Mul(delegation.Shares)
delegationShare := delegationTokens.Quo(sdk.NewDecFromInt(bondedCoinAmount))
rewardsEarned := delegationShare.Mul(sdk.NewDecFromInt(rewardsToDistribute)).RoundInt()
if rewardsEarned.IsZero() {
return false
}
k.AddToClaim(
ctx, delegation.DelegatorAddress, dds.DistributionSchedule.DepositDenom,
types.Stake, sdk.NewCoin(dds.DistributionSchedule.RewardsPerSecond.Denom, rewardsEarned))
rewardsDistributed = rewardsDistributed.Add(rewardsEarned)
}
return false
})
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHarvestDelegatorDistribution,
sdk.NewAttribute(types.AttributeKeyBlockHeight, fmt.Sprintf("%d", ctx.BlockHeight())),
sdk.NewAttribute(types.AttributeKeyRewardsDistribution, rewardsDistributed.String()),
sdk.NewAttribute(types.AttributeKeyDepositDenom, denom),
),
)
}
// AddToClaim adds the input amount to an existing claim or creates a new one
func (k Keeper) AddToClaim(ctx sdk.Context, owner sdk.AccAddress, depositDenom string, depositType types.DepositType, amountToAdd sdk.Coin) {
claim, found := k.GetClaim(ctx, owner, depositDenom, depositType)
if !found {
claim = types.NewClaim(owner, depositDenom, amountToAdd, depositType)
} else {
claim.Amount = claim.Amount.Add(amountToAdd)
}
k.SetClaim(ctx, claim)
}

View File

@ -0,0 +1,194 @@
package keeper_test
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hvt/types"
)
func (suite *KeeperTestSuite) TestApplyDepositRewards() {
type args struct {
depositor sdk.AccAddress
denom string
depositAmount sdk.Coin
totalDeposits sdk.Coin
rewardRate sdk.Coin
depositType types.DepositType
previousBlockTime time.Time
blockTime time.Time
expectedClaimBalance sdk.Coin
}
type errArgs struct {
expectPanic bool
contains string
}
type testCase struct {
name string
args args
errArgs errArgs
}
testCases := []testCase{
{
name: "distribute rewards",
args: args{
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
denom: "bnb",
rewardRate: c("hard", 500),
depositAmount: c("bnb", 100),
totalDeposits: c("bnb", 1000),
depositType: types.LP,
previousBlockTime: time.Date(2020, 11, 1, 13, 59, 50, 0, time.UTC),
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
expectedClaimBalance: c("hard", 500),
},
errArgs: errArgs{
expectPanic: false,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), tc.args.previousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
keeper := tApp.GetHarvestKeeper()
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount, tc.args.depositType)
keeper.SetDeposit(ctx, deposit)
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
if tc.errArgs.expectPanic {
suite.Require().Panics(func() { suite.keeper.ApplyDepositRewards(suite.ctx) })
} else {
suite.Require().NotPanics(func() { suite.keeper.ApplyDepositRewards(suite.ctx) })
claim, f := suite.keeper.GetClaim(suite.ctx, tc.args.depositor, tc.args.denom, tc.args.depositType)
suite.Require().True(f)
suite.Require().Equal(tc.args.expectedClaimBalance, claim.Amount)
}
})
}
}
func (suite *KeeperTestSuite) TestApplyDelegatorRewards() {
type args struct {
delegator sdk.AccAddress
delegatorCoins sdk.Coins
delegationAmount sdk.Coin
totalBonded sdk.Coin
rewardRate sdk.Coin
depositType types.DepositType
previousDistributionTime time.Time
blockTime time.Time
expectedClaimBalance sdk.Coin
}
type errArgs struct {
expectPanic bool
contains string
}
type testCase struct {
name string
args args
errArgs errArgs
}
testCases := []testCase{
{
name: "distribute rewards",
args: args{
delegator: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
delegatorCoins: cs(c("ukava", 1000)),
rewardRate: c("hard", 500),
delegationAmount: c("ukava", 100),
totalBonded: c("ukava", 900),
depositType: types.Stake,
previousDistributionTime: time.Date(2020, 11, 1, 13, 59, 50, 0, time.UTC),
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
expectedClaimBalance: c("hard", 500),
},
errArgs: errArgs{
expectPanic: false,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.delegator, sdk.AccAddress(crypto.AddressHash([]byte("other_delegator")))}, []sdk.Coins{tc.args.delegatorCoins, cs(tc.args.totalBonded)})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), tc.args.rewardRate, time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper()
keeper.SetPreviousDelegationDistribution(ctx, tc.args.previousDistributionTime, "ukava")
stakingKeeper := tApp.GetStakingKeeper()
stakingParams := stakingKeeper.GetParams(ctx)
stakingParams.BondDenom = "ukava"
stakingKeeper.SetParams(ctx, stakingParams)
validatorPubKey := ed25519.GenPrivKey().PubKey()
validator := stakingtypes.NewValidator(sdk.ValAddress(validatorPubKey.Address()), validatorPubKey, stakingtypes.Description{})
validator.Status = sdk.Bonded
stakingKeeper.SetValidator(ctx, validator)
stakingKeeper.SetValidatorByConsAddr(ctx, validator)
stakingKeeper.SetNewValidatorByPowerIndex(ctx, validator)
// call the after-creation hook
stakingKeeper.AfterValidatorCreated(ctx, validator.OperatorAddress)
_, err := stakingKeeper.Delegate(ctx, tc.args.delegator, tc.args.delegationAmount.Amount, sdk.Unbonded, validator, true)
suite.Require().NoError(err)
stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx)
validator, f := stakingKeeper.GetValidator(ctx, validator.OperatorAddress)
suite.Require().True(f)
_, err = stakingKeeper.Delegate(ctx, sdk.AccAddress(crypto.AddressHash([]byte("other_delegator"))), tc.args.totalBonded.Amount, sdk.Unbonded, validator, true)
suite.Require().NoError(err)
stakingKeeper.ApplyAndReturnValidatorSetUpdates(ctx)
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
if tc.errArgs.expectPanic {
suite.Require().Panics(func() { suite.keeper.ApplyDelegationRewards(suite.ctx, suite.keeper.BondDenom(suite.ctx)) })
} else {
suite.Require().NotPanics(func() { suite.keeper.ApplyDelegationRewards(suite.ctx, suite.keeper.BondDenom(suite.ctx)) })
claim, f := suite.keeper.GetClaim(suite.ctx, tc.args.delegator, tc.args.delegationAmount.Denom, tc.args.depositType)
suite.Require().True(f)
suite.Require().Equal(tc.args.expectedClaimBalance, claim.Amount)
}
})
}
}

145
x/hvt/keeper/timelock.go Normal file
View File

@ -0,0 +1,145 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
supplyExported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/hvt/types"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient.
// If the recipients account is not a vesting account and the input length is greater than zero,
// the recipient account is converted to a periodic vesting account and the coins are added to the vesting balance as a vesting period with the input length.
func (k Keeper) SendTimeLockedCoinsToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
macc := k.supplyKeeper.GetModuleAccount(ctx, senderModule)
if !macc.GetCoins().IsAllGTE(amt) {
return sdkerrors.Wrapf(types.ErrInsufficientModAccountBalance, "%s", senderModule)
}
// 0. Get the account from the account keeper and do a type switch, error if it's a validator vesting account or module account (can make this work for validator vesting later if necessary)
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
if acc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, recipientAddr.String())
}
if length == 0 {
return k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
}
switch acc.(type) {
case *validatorvesting.ValidatorVestingAccount, supplyExported.ModuleAccountI:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
case *vesting.PeriodicVestingAccount:
return k.SendTimeLockedCoinsToPeriodicVestingAccount(ctx, senderModule, recipientAddr, amt, length)
case *auth.BaseAccount:
return k.SendTimeLockedCoinsToBaseAccount(ctx, senderModule, recipientAddr, amt, length)
default:
return sdkerrors.Wrapf(types.ErrInvalidAccountType, "%T", acc)
}
}
// SendTimeLockedCoinsToPeriodicVestingAccount sends time-locked coins from the input module account to the recipient
func (k Keeper) SendTimeLockedCoinsToPeriodicVestingAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
k.addCoinsToVestingSchedule(ctx, recipientAddr, amt, length)
return nil
}
// SendTimeLockedCoinsToBaseAccount sends time-locked coins from the input module account to the recipient, converting the recipient account to a vesting account
func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins, length int64) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, senderModule, recipientAddr, amt)
if err != nil {
return err
}
acc := k.accountKeeper.GetAccount(ctx, recipientAddr)
// transition the account to a periodic vesting account:
bacc := authtypes.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
newPeriods := vesting.Periods{types.NewPeriod(amt, length)}
bva, err := vesting.NewBaseVestingAccount(bacc, amt, ctx.BlockTime().Unix()+length)
if err != nil {
return err
}
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), newPeriods)
k.accountKeeper.SetAccount(ctx, pva)
return nil
}
// addCoinsToVestingSchedule adds coins to the input account's vesting schedule where length is the amount of time (from the current block time), in seconds, that the coins will be vesting for
// the input address must be a periodic vesting account
func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) {
acc := k.accountKeeper.GetAccount(ctx, addr)
vacc := acc.(*vesting.PeriodicVestingAccount)
// Add the new vesting coins to OriginalVesting
vacc.OriginalVesting = vacc.OriginalVesting.Add(amt...)
if vacc.EndTime < ctx.BlockTime().Unix() {
// edge case one - the vesting account's end time is in the past (ie, all previous vesting periods have completed)
// append a new period to the vesting account, update the end time, update the account in the store and return
newPeriodLength := (ctx.BlockTime().Unix() - vacc.EndTime) + length
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
vacc.EndTime = ctx.BlockTime().Unix() + length
k.accountKeeper.SetAccount(ctx, vacc)
return
}
if vacc.StartTime > ctx.BlockTime().Unix() {
// edge case two - the vesting account's start time is in the future (all periods have not started)
// update the start time to now and adjust the period lengths in place - a new period will be inserted in the next code block
updatedPeriods := vesting.Periods{}
for i, period := range vacc.VestingPeriods {
updatedPeriod := period
if i == 0 {
updatedPeriod = types.NewPeriod(period.Amount, (vacc.StartTime-ctx.BlockTime().Unix())+period.Length) // 110 - 100 + 6 = 16
}
updatedPeriods = append(updatedPeriods, updatedPeriod)
}
vacc.VestingPeriods = updatedPeriods
vacc.StartTime = ctx.BlockTime().Unix()
}
// logic for inserting a new vesting period into the existing vesting schedule
remainingLength := vacc.EndTime - ctx.BlockTime().Unix()
elapsedTime := ctx.BlockTime().Unix() - vacc.StartTime
proposedEndTime := ctx.BlockTime().Unix() + length
if remainingLength < length {
// in the case that the proposed length is longer than the remaining length of all vesting periods, create a new period with length equal to the difference between the proposed length and the previous total length
newPeriodLength := length - remainingLength
newPeriod := types.NewPeriod(amt, newPeriodLength)
vacc.VestingPeriods = append(vacc.VestingPeriods, newPeriod)
// update the end time so that the sum of all period lengths equals endTime - startTime
vacc.EndTime = proposedEndTime
} else {
// In the case that the proposed length is less than or equal to the sum of all previous period lengths, insert the period and update other periods as necessary.
newPeriods := vesting.Periods{}
lengthCounter := int64(0)
appendRemaining := false
for _, period := range vacc.VestingPeriods {
if appendRemaining {
newPeriods = append(newPeriods, period)
continue
}
lengthCounter += period.Length
if lengthCounter < elapsedTime+length {
newPeriods = append(newPeriods, period)
} else if lengthCounter == elapsedTime+length {
newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
newPeriods = append(newPeriods, newPeriod)
appendRemaining = true
} else {
newPeriod := types.NewPeriod(amt, elapsedTime+length-types.GetTotalVestingPeriodLength(newPeriods))
previousPeriod := types.NewPeriod(period.Amount, period.Length-newPeriod.Length)
newPeriods = append(newPeriods, newPeriod, previousPeriod)
appendRemaining = true
}
}
vacc.VestingPeriods = newPeriods
}
k.accountKeeper.SetAccount(ctx, vacc)
return
}

View File

@ -0,0 +1,334 @@
package keeper_test
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hvt/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
type accountArgs struct {
addr sdk.AccAddress
vestingAccountBefore bool
vestingAccountAfter bool
coins sdk.Coins
periods vesting.Periods
origVestingCoins sdk.Coins
startTime int64
endTime int64
}
type args struct {
accArgs accountArgs
period vesting.Period
blockTime time.Time
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
expectedPeriods vesting.Periods
expectedStartTime int64
expectedEndTime int64
}
type errArgs struct {
expectPass bool
contains string
}
type testCase struct {
name string
args args
errArgs errArgs
}
testCases := []testCase{
{
name: "send liquid coins to base account",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: false,
vestingAccountAfter: false,
coins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
},
period: vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedPeriods: vesting.Periods{},
expectedStartTime: 0,
expectedEndTime: 0,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "send liquid coins to vesting account",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
periods: vesting.Periods{
vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
},
origVestingCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
startTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
endTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
},
period: vesting.Period{Length: 0, Amount: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(100)))},
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)), sdk.NewCoin("hard", sdk.NewInt(100))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(900))),
expectedPeriods: vesting.Periods{
vesting.Period{Amount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), Length: 100},
},
expectedStartTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
expectedEndTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix() + 100,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "insert period at beginning of schedule",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: cs(c("bnb", 20)),
periods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
origVestingCoins: cs(c("bnb", 20)),
startTime: 100,
endTime: 120,
},
period: vesting.Period{Length: 2, Amount: cs(c("hard", 6))},
blockTime: time.Unix(101, 0),
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
expectedModAccountBalance: cs(c("hard", 994)),
expectedPeriods: vesting.Periods{
vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
expectedStartTime: 100,
expectedEndTime: 120,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "insert period at beginning with new start time",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: cs(c("bnb", 20)),
periods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
origVestingCoins: cs(c("bnb", 20)),
startTime: 100,
endTime: 120,
},
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
blockTime: time.Unix(80, 0),
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
expectedModAccountBalance: cs(c("hard", 994)),
expectedPeriods: vesting.Periods{
vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
vesting.Period{Length: 18, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
expectedStartTime: 80,
expectedEndTime: 120,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "insert period in middle of schedule",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: cs(c("bnb", 20)),
periods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
origVestingCoins: cs(c("bnb", 20)),
startTime: 100,
endTime: 120,
},
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
blockTime: time.Unix(101, 0),
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
expectedModAccountBalance: cs(c("hard", 994)),
expectedPeriods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 3, Amount: cs(c("hard", 6))},
vesting.Period{Length: 2, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
expectedStartTime: 100,
expectedEndTime: 120,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "append to end of schedule",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: cs(c("bnb", 20)),
periods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
origVestingCoins: cs(c("bnb", 20)),
startTime: 100,
endTime: 120,
},
period: vesting.Period{Length: 7, Amount: cs(c("hard", 6))},
blockTime: time.Unix(125, 0),
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
expectedModAccountBalance: cs(c("hard", 994)),
expectedPeriods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 12, Amount: cs(c("hard", 6))}},
expectedStartTime: 100,
expectedEndTime: 132,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
name: "add coins to existing period",
args: args{
accArgs: accountArgs{
addr: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
vestingAccountBefore: true,
vestingAccountAfter: true,
coins: cs(c("bnb", 20)),
periods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
origVestingCoins: cs(c("bnb", 20)),
startTime: 100,
endTime: 120,
},
period: vesting.Period{Length: 5, Amount: cs(c("hard", 6))},
blockTime: time.Unix(110, 0),
expectedAccountBalance: cs(c("bnb", 20), c("hard", 6)),
expectedModAccountBalance: cs(c("hard", 994)),
expectedPeriods: vesting.Periods{
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5), c("hard", 6))},
vesting.Period{Length: 5, Amount: cs(c("bnb", 5))}},
expectedStartTime: 100,
expectedEndTime: 120,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins})
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 24, sdk.OneDec())}),
time.Hour*24,
),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
if tc.args.accArgs.vestingAccountBefore {
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, tc.args.accArgs.addr)
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err := vesting.NewBaseVestingAccount(bacc, tc.args.accArgs.origVestingCoins, tc.args.accArgs.endTime)
suite.Require().NoError(err)
pva := vesting.NewPeriodicVestingAccountRaw(bva, tc.args.accArgs.startTime, tc.args.accArgs.periods)
ak.SetAccount(ctx, pva)
}
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.LPAccount, sdk.NewCoins(sdk.NewCoin("hard", sdk.NewInt(1000))))
keeper := tApp.GetHarvestKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, types.LPAccount, tc.args.accArgs.addr, tc.args.period.Amount, tc.args.period.Length)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.accArgs.addr)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.LPAccount)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
if tc.args.accArgs.vestingAccountAfter {
suite.Require().True(ok)
suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods)
suite.Require().Equal(tc.args.expectedStartTime, vacc.StartTime)
suite.Require().Equal(tc.args.expectedEndTime, vacc.EndTime)
} else {
suite.Require().False(ok)
}
}
})
}
}
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }

171
x/hvt/module.go Normal file
View File

@ -0,0 +1,171 @@
package hvt
import (
"encoding/json"
"math/rand"
"github.com/gorilla/mux"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
sim "github.com/cosmos/cosmos-sdk/x/simulation"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/hvt/client/cli"
"github.com/kava-labs/kava/x/hvt/client/rest"
"github.com/kava-labs/kava/x/hvt/keeper"
"github.com/kava-labs/kava/x/hvt/simulation"
"github.com/kava-labs/kava/x/hvt/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return ModuleName
}
// RegisterCodec register module codec
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
RegisterCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return ModuleCdc.MustMarshalJSON(DefaultGenesisState())
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs GenesisState
err := ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers REST routes for the harvest module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the harvest module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns no root query command for the harvest module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(types.StoreKey, cdc)
}
//____________________________________________________________________________
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper Keeper
supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
supplyKeeper: supplyKeeper,
}
}
// Name module name
func (AppModule) Name() string {
return ModuleName
}
// RegisterInvariants register module invariants
func (AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (AppModule) Route() string {
return ModuleName
}
// NewHandler module handler
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return QuerierRoute
}
// NewQuerierHandler returns no sdk.Querier.
func (am AppModule) NewQuerierHandler() sdk.Querier {
return keeper.NewQuerier(am.keeper)
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var genesisState GenesisState
ModuleCdc.MustUnmarshalJSON(data, &genesisState)
InitGenesis(ctx, am.keeper, am.supplyKeeper, genesisState)
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock module begin-block
func (am AppModule) BeginBlock(ctx sdk.Context, _ abci.RequestBeginBlock) {
BeginBlocker(ctx, am.keeper)
}
// EndBlock module end-block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
//____________________________________________________________________________
// GenerateGenesisState creates a randomized GenState of the harvest module
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// ProposalContents doesn't return any content functions for governance proposals.
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
}
// RandomizedParams returns nil because harvest has no params.
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return simulation.ParamChanges(r)
}
// RegisterStoreDecoder registers a decoder for harvest module's types
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// WeightedOperations returns the all the harvest module operations with their respective weights.
func (am AppModule) WeightedOperations(simState module.SimulationState) []sim.WeightedOperation {
return nil
}

View File

@ -0,0 +1,36 @@
package simulation
import (
"bytes"
"fmt"
"time"
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/hvt/types"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding harvest type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey), bytes.Equal(kvA.Key[:1], types.PreviousDelegationDistributionKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryBare(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
case bytes.Equal(kvA.Key[:1], types.DepositsKeyPrefix):
var depA, depB types.Deposit
cdc.MustUnmarshalBinaryBare(kvA.Value, &depA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &depB)
return fmt.Sprintf("%s\n%s", depA, depB)
case bytes.Equal(kvA.Key[:1], types.ClaimsKeyPrefix):
var claimA, claimB types.Claim
cdc.MustUnmarshalBinaryBare(kvA.Value, &claimA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &claimB)
return fmt.Sprintf("%s\n%s", claimA, claimB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}
}

View File

@ -0,0 +1,62 @@
package simulation
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/libs/kv"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hvt/types"
)
func makeTestCodec() (cdc *codec.Codec) {
cdc = codec.New()
sdk.RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
types.RegisterCodec(cdc)
return
}
func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec()
prevBlockTime := time.Now().UTC()
deposit := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoin("bnb", sdk.NewInt(1)), "lp")
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
kvPairs := kv.Pairs{
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
kv.Pair{Key: []byte(types.PreviousDelegationDistributionKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
kv.Pair{Key: []byte(types.DepositsKeyPrefix), Value: cdc.MustMarshalBinaryBare(deposit)},
kv.Pair{Key: []byte(types.ClaimsKeyPrefix), Value: cdc.MustMarshalBinaryBare(claim)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
tests := []struct {
name string
expectedLog string
}{
{"PreviousBlockTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
{"PreviousDistributionTime", fmt.Sprintf("%s\n%s", prevBlockTime, prevBlockTime)},
{"Deposit", fmt.Sprintf("%s\n%s", deposit, deposit)},
{"Claim", fmt.Sprintf("%s\n%s", claim, claim)},
{"other", ""},
}
for i, tt := range tests {
i, tt := i, tt
t.Run(tt.name, func(t *testing.T) {
switch i {
case len(tests) - 1:
require.Panics(t, func() { DecodeStore(cdc, kvPairs[i], kvPairs[i]) }, tt.name)
default:
require.Equal(t, tt.expectedLog, DecodeStore(cdc, kvPairs[i], kvPairs[i]), tt.name)
}
})
}
}

View File

@ -0,0 +1,79 @@
package simulation
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/kava-labs/kava/x/hvt/types"
)
// SecondsPerYear is the number of seconds in a year
const (
SecondsPerYear = 31536000
// BaseAprPadding sets the minimum inflation to the calculated SPR inflation rate from being 0.0
BaseAprPadding = "0.000000003022265980"
)
// RandomizedGenState generates a random GenesisState for harvest module
func RandomizedGenState(simState *module.SimulationState) {
// params := genRandomParams(simState)
// if err := params.Validate(); err != nil {
// panic(err)
// }
harvestGenesis := types.DefaultGenesisState()
if err := harvestGenesis.Validate(); err != nil {
panic(err)
}
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, harvestGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(harvestGenesis)
}
// func genRandomParams(simState *module.SimulationState) types.Params {
// periods := genRandomPeriods(simState.Rand, simState.GenTimestamp)
// params := types.NewParams(true, periods)
// return params
// }
// func genRandomPeriods(r *rand.Rand, timestamp time.Time) types.Periods {
// var periods types.Periods
// numPeriods := simulation.RandIntBetween(r, 1, 10)
// periodStart := timestamp
// for i := 0; i < numPeriods; i++ {
// // set periods to be between 1-3 days
// durationMultiplier := simulation.RandIntBetween(r, 1, 3)
// duration := time.Duration(int64(24*durationMultiplier)) * time.Hour
// periodEnd := periodStart.Add(duration)
// inflation := genRandomInflation(r)
// period := types.NewPeriod(periodStart, periodEnd, inflation)
// periods = append(periods, period)
// periodStart = periodEnd
// }
// return periods
// }
// func genRandomInflation(r *rand.Rand) sdk.Dec {
// // If sim.RandomDecAmount is less than base apr padding, add base apr padding
// aprPadding, _ := sdk.NewDecFromStr(BaseAprPadding)
// extraAprInflation := simulation.RandomDecAmount(r, sdk.MustNewDecFromStr("0.25"))
// for extraAprInflation.LT(aprPadding) {
// extraAprInflation = extraAprInflation.Add(aprPadding)
// }
// aprInflation := sdk.OneDec().Add(extraAprInflation)
// // convert APR inflation to SPR (inflation per second)
// inflationSpr, err := aprInflation.ApproxRoot(uint64(SecondsPerYear))
// if err != nil {
// panic(fmt.Sprintf("error generating random inflation %v", err))
// }
// return inflationSpr
// }
// func genRandomActive(r *rand.Rand) bool {
// threshold := 50
// value := simulation.RandIntBetween(r, 1, 100)
// return value > threshold
// }

View File

@ -0,0 +1,13 @@
package simulation
import (
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
)
// ParamChanges defines the parameters that can be modified by param change proposals
// on the simulation
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []simulation.ParamChange{}
}

22
x/hvt/spec/01_concepts.md Normal file
View File

@ -0,0 +1,22 @@
<!--
order: 1
-->
# Concepts
The harvest module introduces the hard token to the kava blockchain. This module distributes hard tokens to two types of ecosystem participants:
1. Kava stakers - any address that stakes (delegates) kava tokens will be eligible to claim hard tokens. For each delegator, hard tokens are accumulated ratably based on the total number of kava tokens staked. For example, if a user stakes 1 million KAVA tokens and there are 100 million staked KAVA, that user will accumulate 1% of hard tokens earmarked for stakers during the distribution period. Distribution periods are defined by a start date, an end date, and a number of hard tokens that are distributed per second.
2. Depositors - any address that deposits eligible tokens to the harvest module will be eligible to claim hard tokens. For each depositor, hard tokens are accumulated ratable based on the total number of tokens staked of that denomination. For example, if a user deposits 1 million "xyz" tokens and there are 100 million xyz deposited, that user will accumulate 1% of hard tokens earmarked for depositors of that denomination during the distribution period. Distribution periods are defined by a start date, an end date, and a number of hard tokens that are distributed per second.
Users are not air-dropped tokens, rather they accumulate `Claim` objects that they my submit a transaction in order to claim. In order to better align long term incentives, when users claim hard tokens, they have three options, called 'multipliers', for how tokens are distributed.
* Liquid - users can immediately receive hard tokens, but they will receive a smaller fraction of tokens than if they choose medium-term or long-term locked tokens.
* Medium-term locked - users can receive tokens that are medium-term transfer restricted. They will receive more tokens than users who choose liquid tokens, but fewer than those who choose long term locked tokens.
* Long-term locked - users can receive tokens that are long-term transfer restricted. Users choosing this option will receive more tokens than users who choose liquid or medium-term locked tokens.
The exact multipliers will be voted by governance and can be changed via a governance vote. An example multiplier schedule would be:
* Liquid - 10% multiplier and no lock up. Users receive 10% as many tokens as users who choose long-term locked tokens.
* Medium-term locked - 33% multiplier and 6 month transfer restriction. Users receive 33% as many tokens as users who choose long-term locked tokens.
* Long-term locked - 100% multiplier and 2 year transfer restriction. Users receive 10x as many tokens as users who choose liquid tokens and 3x as many tokens as users who choose medium-term locked tokens.

52
x/hvt/spec/02_state.md Normal file
View File

@ -0,0 +1,52 @@
<!--
order: 2
-->
# State
## Parameters and Genesis State
`Parameters` define the distribution schedule of hard tokens that will be distributed to delegators and depositors, respectively.
```go
// Params governance parameters for harvest module
type Params struct {
Active bool `json:"active" yaml:"active"`
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"`
}
// DistributionSchedule distribution schedule for liquidity providers
type DistributionSchedule struct {
Active bool `json:"active" yaml:"active"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
Start time.Time `json:"start" yaml:"start"`
End time.Time `json:"end" yaml:"end"`
RewardsPerSecond sdk.Coin `json:"rewards_per_second" yaml:"rewards_per_second"`
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
}
// DistributionSchedules slice of DistributionSchedule
type DistributionSchedules []DistributionSchedule
// DelegatorDistributionSchedule distribution schedule for delegators
type DelegatorDistributionSchedule struct {
DistributionSchedule DistributionSchedule `json:"distribution_schedule" yaml:"distribution_schedule"`
DistributionFrequency time.Duration `json:"distribution_frequency" yaml:"distribution_frequency"`
}
// DelegatorDistributionSchedules slice of DelegatorDistributionSchedule
type DelegatorDistributionSchedules []DelegatorDistributionSchedule
```
`GenesisState` defines the state that must be persisted when the blockchain stops/restarts in order for normal function of the harvest module to resume.
```go
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
}
```

31
x/hvt/spec/03_messages.md Normal file
View File

@ -0,0 +1,31 @@
<!--
order: 3
-->
# Messages
There are three messages in the harvest module. Deposit allows users to deposit assets to the harvest module. In version 2, depositors will be able to use their deposits as collateral to borrow froom harvest. Withdraw removes assets from the harvest module, returning them to the user. Claim allows users to claim earned HARD tokens.
```go
// MsgDeposit deposit asset to the harvest module.
type MsgDeposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// MsgWithdraw withdraw from the harvest module.
type MsgWithdraw struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// MsgClaimReward message type used to claim HARD tokens
type MsgClaimReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
RewardMultiplier string `json:"reward_multiplier" yaml:"reward_multiplier"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
```

57
x/hvt/spec/04_events.md Normal file
View File

@ -0,0 +1,57 @@
<!--
order: 4
-->
# Events
The harvest module emits the following events:
## Handlers
### MsgDeposit
| Type | Attribute Key | Attribute Value |
|----------------------|---------------------|-----------------------|
| message | module | harvest |
| message | sender | `{sender address}` |
| harvest_deposit | amount | `{amount}` |
| harvest_deposit | depositor | `{depositor address}` |
| harvest_deposit | deposit_denom | `{deposit denom}` |
| harvest_deposit | deposit_type | `{deposit type}` |
### MsgWithdraw
| Type | Attribute Key | Attribute Value |
|------------------------|---------------------|-----------------------|
| message | module | harvest |
| message | sender | `{sender address}` |
| harvest_deposit | amount | `{amount}` |
| harvest_deposit | depositor | `{depositor address}` |
| harvest_deposit | deposit_denom | `{deposit denom}` |
| harvest_deposit | deposit_type | `{deposit type}` |
| delete_harvest_deposit | depositor | `{depositor address}` |
| delete_harvest_deposit | deposit_denom | `{deposit denom}` |
| delete_harvest_deposit | deposit_type | `{deposit type}` |
### MsgClaimReward
| Type | Attribute Key | Attribute Value |
|------------------------|---------------------|--------------------------|
| message | module | harvest |
| message | sender | `{sender address}` |
| claim_harvest_reward | amount | `{amount}` |
| claim_harvest_reward | claim_holder | `{claim holder address}` |
| claim_harvest_reward | deposit_denom | `{deposit denom}` |
| claim_harvest_reward | deposit_type | `{deposit type}` |
| claim_harvest_reward | claim_multiplier | `{claim multiplier}` |
## BeginBlock
| Type | Attribute Key | Attribute Value |
|--------------------------------|---------------------|--------------------------|
| harvest_lp_distribution | block_height | `{block height}` |
| harvest_lp_distribution | rewards_distributed | `{rewards distributed}` |
| harvest_lp_distribution | deposit_denom | `{deposit denom}` |
| harvest_delegator_distribution | block_height | `{block height}` |
| harvest_delegator_distribution | rewards_distributed | `{rewards distributed}` |
| harvest_delegator_distribution | deposit_denom | `{deposit denom}` |

46
x/hvt/spec/05_params.md Normal file
View File

@ -0,0 +1,46 @@
<!--
order: 5
-->
# Parameters
The harvest module has the following parameters:
| Key | Type | Example | Description |
|-----------------------------------|---------------------------------------|---------------|--------------------------------------------------|
| Active | bool | "true" | boolean for if token distribution is active |
| LiquidityProviderSchedules | array (LiquidityProviderSchedule) | [{see below}] | array of params for each supported asset |
| DelegatorDistributionSchedules | array (DelegatorDistributionSchedule) | [{see below}] | array of params for staking incentive assets |
Each `LiquidityProviderSchedules` has the following parameters
| Key | Type | Example | Description |
|------------------|--------------------|--------------------------|----------------------------------------------------------------|
| Active | bool | "true" | boolean for if token distribution is active for this schedule |
| DepositDenom | string | "bnb" | coin denom of the asset which can be deposited |
| Start | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
| End | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
| RewardsPerSecond | Coin | "500hard" | HARD tokens per second that can be claimed by depositors |
| ClaimEnd | time.Time | "2022-06-01T15:20:00Z" | the time at which users can no longer claim HARD tokens |
| ClaimMultipliers | array (Multiplier) | [{see below}] | reward multipliers for users claiming HARD tokens |
Each `DelegatorDistributionSchedule` has the following parameters
| Key | Type | Example | Description |
|-----------------------|--------------------|--------------------------|----------------------------------------------------------------|
| Active | bool | "true" | boolean for if token distribution is active for this schedule |
| DepositDenom | string | "bnb" | coin denom of the asset which can be deposited |
| Start | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
| End | time.Time | "2020-06-01T15:20:00Z" | the time when the period will end |
| RewardsPerSecond | Coin | "500hard" | HARD tokens per second that can be claimed by depositors |
| ClaimEnd | time.Time | "2022-06-01T15:20:00Z" | the time at which users can no longer claim HARD tokens |
| ClaimMultipliers | array (Multiplier) | [{see below}] | reward multipliers for users claiming HARD tokens |
| DistributionFrequency | time.Duration | "24hr" | frequency at which delegation rewards are accumulated |
Each `ClaimMultiplier` has the following parameters
| Key | Type | Example | Description |
|-----------------------|--------------------|--------------------------|-----------------------------------------------------------------|
| Name | string | "large" | the unique name of the reward multiplier |
| MonthsLockup | int | "6" | number of months HARD tokens with this multiplier are locked |
| Factor | Dec | "0.5" | the scaling factor for HARD tokens claimed with this multiplier |

View File

@ -0,0 +1,19 @@
<!--
order: 6
-->
# Begin Block
At the start of each block, hard tokens are distributed (as claims) to liquidity providers and delegators, respectively.
```go
// BeginBlocker applies rewards to liquidity providers and delegators according to params
func BeginBlocker(ctx sdk.Context, k Keeper) {
k.ApplyDepositRewards(ctx)
if k.ShouldDistributeValidatorRewards(ctx, k.BondDenom(ctx)) {
k.ApplyDelegationRewards(ctx, k.BondDenom(ctx))
k.SetPreviousDelegationDistribution(ctx, ctx.BlockTime(), k.BondDenom(ctx))
}
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}
```

20
x/hvt/spec/README.md Normal file
View File

@ -0,0 +1,20 @@
<!--
order: 0
title: "Harvest Overview"
parent:
title: "harvest"
-->
# `harvest`
<!-- TOC -->
1. **[Concepts](01_concepts.md)**
2. **[State](02_state.md)**
3. **[Messages](03_messages.md)**
4. **[Events](04_events.md)**
5. **[Params](05_params.md)**
6. **[BeginBlock](06_begin_block.md)**
## Abstract
`x/hvt` is an implementation of a Cosmos SDK Module that will serve as the basis for a cross-chain money market platform. The current version of the module defines how HARD tokens are distributed, while future versions of this module will define lending, borrowing, distribution, incentives, and governance for the money market module.

23
x/hvt/types/claim.go Normal file
View File

@ -0,0 +1,23 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Claim defines an amount of coins that the owner can claim
type Claim struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
Type DepositType `json:"type" yaml:"type"`
}
// NewClaim returns a new claim
func NewClaim(owner sdk.AccAddress, denom string, amount sdk.Coin, dtype DepositType) Claim {
return Claim{
Owner: owner,
DepositDenom: denom,
Amount: amount,
Type: dtype,
}
}

21
x/hvt/types/codec.go Normal file
View File

@ -0,0 +1,21 @@
package types
import "github.com/cosmos/cosmos-sdk/codec"
// ModuleCdc generic sealed codec to be used throughout module
var ModuleCdc *codec.Codec
func init() {
cdc := codec.New()
RegisterCodec(cdc)
codec.RegisterCrypto(cdc)
ModuleCdc = cdc.Seal()
}
// RegisterCodec registers the necessary types for hvt module
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgClaimReward{}, "hvt/MsgClaimReward", nil)
cdc.RegisterConcrete(MsgDeposit{}, "hvt/MsgDeposit", nil)
cdc.RegisterConcrete(MsgWithdraw{}, "hvt/MsgWithdraw", nil)
cdc.RegisterConcrete(DistributionSchedule{}, "hvt/DistributionSchedule", nil)
}

21
x/hvt/types/deposit.go Normal file
View File

@ -0,0 +1,21 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Deposit defines an amount of coins deposited into a harvest module account
type Deposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
Type DepositType `json:"type" yaml:"type"`
}
// NewDeposit returns a new deposit
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coin, dtype DepositType) Deposit {
return Deposit{
Depositor: depositor,
Amount: amount,
Type: dtype,
}
}

38
x/hvt/types/errors.go Normal file
View File

@ -0,0 +1,38 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DONTCOVER
var (
// ErrInvalidDepositDenom error for invalid deposit denoms
ErrInvalidDepositDenom = sdkerrors.Register(ModuleName, 2, "invalid deposit denom")
// ErrDepositNotFound error for deposit not found
ErrDepositNotFound = sdkerrors.Register(ModuleName, 3, "deposit not found")
// ErrInvaliWithdrawAmount error for invalid withdrawal amount
ErrInvaliWithdrawAmount = sdkerrors.Register(ModuleName, 4, "withdrawal amount exceeds deposit amount")
// ErrInvalidDepositType error for invalid deposit type
ErrInvalidDepositType = sdkerrors.Register(ModuleName, 5, "invalid deposit type")
// ErrClaimNotFound error for claim not found
ErrClaimNotFound = sdkerrors.Register(ModuleName, 6, "claim not found")
// ErrZeroClaim error for claim amount rounded to zero
ErrZeroClaim = sdkerrors.Register(ModuleName, 7, "cannot claim - claim amount rounds to zero")
// ErrLPScheduleNotFound error for liquidity provider rewards schedule not found
ErrLPScheduleNotFound = sdkerrors.Register(ModuleName, 8, "no liquidity provider rewards schedule found")
// ErrGovScheduleNotFound error for governance distribution rewards schedule not found
ErrGovScheduleNotFound = sdkerrors.Register(ModuleName, 9, "no governance rewards schedule found")
// ErrInvalidMultiplier error for multiplier not found
ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 10, "invalid rewards multiplier")
// ErrInsufficientModAccountBalance error for module account with innsufficient balance
ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 11, "module account has insufficient balance to pay reward")
// ErrInvalidAccountType error for unsupported accounts
ErrInvalidAccountType = sdkerrors.Register(ModuleName, 12, "receiver account type not supported")
// ErrAccountNotFound error for accounts that are not found in state
ErrAccountNotFound = sdkerrors.Register(ModuleName, 13, "account not found")
// ErrClaimExpired error for expired claims
ErrClaimExpired = sdkerrors.Register(ModuleName, 14, "claim period expired")
// ErrInvalidReceiver error for when sending and receiving accounts don't match
ErrInvalidReceiver = sdkerrors.Register(ModuleName, 15, "receiver account must match sender account")
)

21
x/hvt/types/events.go Normal file
View File

@ -0,0 +1,21 @@
package types
// Event types for harvest module
const (
EventTypeHarvestDeposit = "harvest_deposit"
EventTypeHarvestDelegatorDistribution = "harvest_delegator_distribution"
EventTypeHarvestLPDistribution = "harvest_lp_distribution"
EventTypeDeleteHarvestDeposit = "delete_harvest_deposit"
EventTypeHarvestWithdrawal = "harvest_withdrawal"
EventTypeClaimHarvestReward = "claim_harvest_reward"
AttributeValueCategory = ModuleName
AttributeKeyBlockHeight = "block_height"
AttributeKeyRewardsDistribution = "rewards_distributed"
AttributeKeyDeposit = "deposit"
AttributeKeyDepositType = "deposit_type"
AttributeKeyDepositDenom = "deposit_denom"
AttributeKeyDepositor = "depositor"
AttributeKeyClaimHolder = "claim_holder"
AttributeKeyClaimAmount = "claim_amount"
AttributeKeyClaimMultiplier = "claim_multiplier"
)

View File

@ -0,0 +1,35 @@
package types // noalias
import (
sdk "github.com/cosmos/cosmos-sdk/types"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/cosmos/cosmos-sdk/x/supply/exported"
)
// SupplyKeeper defines the expected supply keeper
type SupplyKeeper interface {
GetModuleAddress(name string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, name string) exported.ModuleAccountI
GetSupply(ctx sdk.Context) (supply exported.SupplyI)
SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
MintCoins(ctx sdk.Context, name string, amt sdk.Coins) error
}
// AccountKeeper defines the expected keeper interface for interacting with account
type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
SetAccount(ctx sdk.Context, acc authexported.Account)
}
// StakingKeeper defines the expected keeper interface for the staking keeper
type StakingKeeper interface {
IterateLastValidators(ctx sdk.Context, fn func(index int64, validator stakingexported.ValidatorI) (stop bool))
IterateValidators(sdk.Context, func(index int64, validator stakingexported.ValidatorI) (stop bool))
IterateAllDelegations(ctx sdk.Context, cb func(delegation stakingtypes.Delegation) (stop bool))
GetBondedPool(ctx sdk.Context) (bondedPool exported.ModuleAccountI)
BondDenom(ctx sdk.Context) (res string)
}

84
x/hvt/types/genesis.go Normal file
View File

@ -0,0 +1,84 @@
package types
import (
"bytes"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
// GenesisState default values
var (
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
DefaultDistributionTimes = GenesisDistributionTimes{}
)
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
PreviousDistributionTimes GenesisDistributionTimes `json:"previous_distribution_times" yaml:"previous_distribution_times"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, previousBlockTime time.Time, previousDistTimes GenesisDistributionTimes) GenesisState {
return GenesisState{
Params: params,
PreviousBlockTime: previousBlockTime,
PreviousDistributionTimes: previousDistTimes,
}
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
PreviousBlockTime: DefaultPreviousBlockTime,
PreviousDistributionTimes: DefaultDistributionTimes,
}
}
// Validate performs basic validation of genesis data returning an
// error for any failed validation criteria.
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
if gs.PreviousBlockTime.Equal(time.Time{}) {
return fmt.Errorf("previous block time not set")
}
for _, gdt := range gs.PreviousDistributionTimes {
if gdt.PreviousDistributionTime.Equal(time.Time{}) {
return fmt.Errorf("previous distribution time not set for %s", gdt.Denom)
}
if err := sdk.ValidateDenom(gdt.Denom); err != nil {
return err
}
}
return nil
}
// Equal checks whether two gov GenesisState structs are equivalent
func (gs GenesisState) Equal(gs2 GenesisState) bool {
b1 := ModuleCdc.MustMarshalBinaryBare(gs)
b2 := ModuleCdc.MustMarshalBinaryBare(gs2)
return bytes.Equal(b1, b2)
}
// IsEmpty returns true if a GenesisState is empty
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}
// GenesisDistributionTime stores the previous distribution time and its corresponding denom
type GenesisDistributionTime struct {
Denom string `json:"denom" yaml:"denom"`
PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"`
}
// GenesisDistributionTimes slice of GenesisDistributionTime
type GenesisDistributionTimes []GenesisDistributionTime

124
x/hvt/types/genesis_test.go Normal file
View File

@ -0,0 +1,124 @@
package types_test
import (
"strings"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/hvt/types"
)
type GenesisTestSuite struct {
suite.Suite
}
func (suite *GenesisTestSuite) TestGenesisValidation() {
type args struct {
params types.Params
pbt time.Time
pdts types.GenesisDistributionTimes
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "default",
args: args{
params: types.DefaultParams(),
pbt: types.DefaultPreviousBlockTime,
pdts: types.DefaultDistributionTimes,
},
expectPass: true,
expectedErr: "",
},
{
name: "valid",
args: args{
params: types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
),
pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC),
pdts: types.GenesisDistributionTimes{
{PreviousDistributionTime: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), Denom: "bnb"},
},
},
expectPass: true,
expectedErr: "",
},
{
name: "invalid previous blocktime",
args: args{
params: types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
),
pbt: time.Time{},
pdts: types.GenesisDistributionTimes{
{PreviousDistributionTime: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC), Denom: "bnb"},
},
},
expectPass: false,
expectedErr: "previous block time not set",
},
{
name: "invalid previous distribution time",
args: args{
params: types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.OneDec()), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("1.5")), types.NewMultiplier(types.Medium, 24, sdk.MustNewDecFromStr("3"))}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
),
pbt: time.Date(2020, 10, 8, 12, 0, 0, 0, time.UTC),
pdts: types.GenesisDistributionTimes{
{PreviousDistributionTime: time.Time{}, Denom: "bnb"},
},
},
expectPass: false,
expectedErr: "previous distribution time not set",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
gs := types.NewGenesisState(tc.args.params, tc.args.pbt, tc.args.pdts)
err := gs.Validate()
if tc.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
}
})
}
}
func TestGenesisTestSuite(t *testing.T) {
suite.Run(t, new(GenesisTestSuite))
}

61
x/hvt/types/keys.go Normal file
View File

@ -0,0 +1,61 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName name that will be used throughout the module
ModuleName = "harvest"
// LPAccount LP distribution module account
LPAccount = "harvest_lp_distribution"
// DelegatorAccount delegator distribution module account
DelegatorAccount = "harvest_delegator_distribution"
// ModuleAccountName name of module account used to hold deposits
ModuleAccountName = "harvest"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// QuerierRoute Top level query string
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
)
var (
PreviousBlockTimeKey = []byte{0x01}
PreviousDelegationDistributionKey = []byte{0x02}
DepositsKeyPrefix = []byte{0x03}
ClaimsKeyPrefix = []byte{0x04}
sep = []byte(":")
)
// DepositKey key of a specific deposit in the store
func DepositKey(depositType DepositType, denom string, depositor sdk.AccAddress) []byte {
return createKey([]byte(depositType), sep, []byte(denom), sep, depositor)
}
// DepositTypeIteratorKey returns an interator prefix for interating over deposits by deposit type and denom
func DepositTypeIteratorKey(depositType DepositType, denom string) []byte {
return createKey([]byte(depositType), sep, []byte(denom))
}
// ClaimKey key of a specific deposit in the store
func ClaimKey(depositType DepositType, denom string, owner sdk.AccAddress) []byte {
return createKey([]byte(depositType), sep, []byte(denom), sep, owner)
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

216
x/hvt/types/msg.go Normal file
View File

@ -0,0 +1,216 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// MultiplierName name for valid multiplier
type MultiplierName string
// DepositType type for valid deposit type strings
type DepositType string
// Valid reward multipliers and reward types
const (
Small MultiplierName = "small"
Medium MultiplierName = "medium"
Large MultiplierName = "large"
LP DepositType = "lp"
Stake DepositType = "stake"
)
// Queryable deposit types
var (
DepositTypesDepositQuery = []DepositType{LP}
DepositTypesClaimQuery = []DepositType{LP, Stake}
)
// IsValid checks if the input is one of the expected strings
func (mn MultiplierName) IsValid() error {
switch mn {
case Small, Medium, Large:
return nil
}
return fmt.Errorf("invalid multiplier name: %s", mn)
}
// IsValid checks if the input is one of the expected strings
func (dt DepositType) IsValid() error {
switch dt {
case LP, Stake:
return nil
}
return fmt.Errorf("invalid deposit type: %s", dt)
}
// ensure Msg interface compliance at compile time
var (
_ sdk.Msg = &MsgClaimReward{}
_ sdk.Msg = &MsgDeposit{}
_ sdk.Msg = &MsgWithdraw{}
)
// MsgDeposit deposit collateral to the harvest module.
type MsgDeposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// NewMsgDeposit returns a new MsgDeposit
func NewMsgDeposit(depositor sdk.AccAddress, amount sdk.Coin, depositType string) MsgDeposit {
return MsgDeposit{
Depositor: depositor,
Amount: amount,
DepositType: depositType,
}
}
// Route return the message type used for routing the message.
func (msg MsgDeposit) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgDeposit) Type() string { return "harvest_deposit" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgDeposit) ValidateBasic() error {
if msg.Depositor.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "deposit amount %s", msg.Amount)
}
return DepositType(strings.ToLower(msg.DepositType)).IsValid()
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgDeposit) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgDeposit) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Depositor}
}
// String implements the Stringer interface
func (msg MsgDeposit) String() string {
return fmt.Sprintf(`Deposit Message:
Depositor: %s
Amount: %s
Deposit Type: %s
`, msg.Depositor, msg.Amount, msg.DepositType)
}
// MsgWithdraw withdraw from the harvest module.
type MsgWithdraw struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// NewMsgWithdraw returns a new MsgWithdraw
func NewMsgWithdraw(depositor sdk.AccAddress, amount sdk.Coin, depositType string) MsgDeposit {
return MsgDeposit{
Depositor: depositor,
Amount: amount,
DepositType: depositType,
}
}
// Route return the message type used for routing the message.
func (msg MsgWithdraw) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgWithdraw) Type() string { return "harvest_withdraw" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgWithdraw) ValidateBasic() error {
if msg.Depositor.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "deposit amount %s", msg.Amount)
}
return DepositType(strings.ToLower(msg.DepositType)).IsValid()
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgWithdraw) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgWithdraw) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Depositor}
}
// String implements the Stringer interface
func (msg MsgWithdraw) String() string {
return fmt.Sprintf(`Withdraw Message:
Depositor: %s
Amount: %s
Deposit Type: %s
`, msg.Depositor, msg.Amount, msg.DepositType)
}
// MsgClaimReward message type used to claim rewards
type MsgClaimReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Receiver sdk.AccAddress `json:"receiver" yaml:"receiver"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
DepositType string `json:"deposit_type" yaml:"deposit_type"`
}
// NewMsgClaimReward returns a new MsgClaimReward.
func NewMsgClaimReward(sender, receiver sdk.AccAddress, depositDenom, depositType, multiplier string) MsgClaimReward {
return MsgClaimReward{
Sender: sender,
Receiver: receiver,
DepositDenom: depositDenom,
MultiplierName: multiplier,
DepositType: depositType,
}
}
// Route return the message type used for routing the message.
func (msg MsgClaimReward) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgClaimReward) Type() string { return "claim_harvest_reward" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgClaimReward) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Receiver.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "receiver address cannot be empty")
}
if err := sdk.ValidateDenom(msg.DepositDenom); err != nil {
return fmt.Errorf("collateral type cannot be blank")
}
if err := DepositType(strings.ToLower(msg.DepositType)).IsValid(); err != nil {
return err
}
return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid()
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgClaimReward) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}

223
x/hvt/types/msg_test.go Normal file
View File

@ -0,0 +1,223 @@
package types_test
import (
"strings"
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hvt/types"
)
type MsgTestSuite struct {
suite.Suite
}
func (suite *MsgTestSuite) TestMsgDeposit() {
type args struct {
depositor sdk.AccAddress
amount sdk.Coin
depositType string
}
addrs := []sdk.AccAddress{
sdk.AccAddress("test1"),
sdk.AccAddress("test2"),
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "valid",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "lp",
},
expectPass: true,
expectedErr: "",
},
{
name: "valid2",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "LP",
},
expectPass: true,
expectedErr: "",
},
{
name: "invalid",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "cat",
},
expectPass: false,
expectedErr: "invalid deposit type",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
msg := types.NewMsgDeposit(tc.args.depositor, tc.args.amount, tc.args.depositType)
err := msg.ValidateBasic()
if tc.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
}
})
}
}
func (suite *MsgTestSuite) TestMsgWithdraw() {
type args struct {
depositor sdk.AccAddress
amount sdk.Coin
depositType string
}
addrs := []sdk.AccAddress{
sdk.AccAddress("test1"),
sdk.AccAddress("test2"),
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "valid",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "lp",
},
expectPass: true,
expectedErr: "",
},
{
name: "valid2",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "LP",
},
expectPass: true,
expectedErr: "",
},
{
name: "invalid",
args: args{
depositor: addrs[0],
amount: sdk.NewCoin("bnb", sdk.NewInt(10000000)),
depositType: "cat",
},
expectPass: false,
expectedErr: "invalid deposit type",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
msg := types.NewMsgWithdraw(tc.args.depositor, tc.args.amount, tc.args.depositType)
err := msg.ValidateBasic()
if tc.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
}
})
}
}
func (suite *MsgTestSuite) TestMsgClaim() {
type args struct {
sender sdk.AccAddress
receiver sdk.AccAddress
denom string
depositType string
multiplier string
}
addrs := []sdk.AccAddress{
sdk.AccAddress("test1"),
sdk.AccAddress("test2"),
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "valid",
args: args{
sender: addrs[0],
receiver: addrs[0],
denom: "bnb",
depositType: "lp",
multiplier: "large",
},
expectPass: true,
expectedErr: "",
},
{
name: "valid2",
args: args{
sender: addrs[0],
receiver: addrs[0],
denom: "bnb",
depositType: "stake",
multiplier: "small",
},
expectPass: true,
expectedErr: "",
},
{
name: "valid3",
args: args{
sender: addrs[0],
receiver: addrs[1],
denom: "bnb",
depositType: "lp",
multiplier: "Medium",
},
expectPass: true,
expectedErr: "",
},
{
name: "invalid",
args: args{
sender: addrs[0],
receiver: addrs[0],
denom: "bnb",
depositType: "lp",
multiplier: "huge",
},
expectPass: false,
expectedErr: "invalid multiplier name",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
msg := types.NewMsgClaimReward(tc.args.sender, tc.args.receiver, tc.args.denom, tc.args.depositType, tc.args.multiplier)
err := msg.ValidateBasic()
if tc.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
}
})
}
}
func TestMsgTestSuite(t *testing.T) {
suite.Run(t, new(MsgTestSuite))
}

299
x/hvt/types/params.go Normal file
View File

@ -0,0 +1,299 @@
package types
import (
"errors"
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
)
// Parameter keys and default values
var (
KeyActive = []byte("Active")
KeyLPSchedules = []byte("LPSchedules")
KeyDelegatorSchedule = []byte("DelegatorSchedule")
DefaultActive = true
DefaultGovSchedules = DistributionSchedules{}
DefaultLPSchedules = DistributionSchedules{}
DefaultDelegatorSchedules = DelegatorDistributionSchedules{}
GovDenom = cdptypes.DefaultGovDenom
)
// Params governance parameters for harvest module
type Params struct {
Active bool `json:"active" yaml:"active"`
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"`
}
// DistributionSchedule distribution schedule for liquidity providers
type DistributionSchedule struct {
Active bool `json:"active" yaml:"active"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
Start time.Time `json:"start" yaml:"start"`
End time.Time `json:"end" yaml:"end"`
RewardsPerSecond sdk.Coin `json:"rewards_per_second" yaml:"rewards_per_second"`
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
}
// NewDistributionSchedule returns a new DistributionSchedule
func NewDistributionSchedule(active bool, denom string, start, end time.Time, reward sdk.Coin, claimEnd time.Time, multipliers Multipliers) DistributionSchedule {
return DistributionSchedule{
Active: active,
DepositDenom: denom,
Start: start,
End: end,
RewardsPerSecond: reward,
ClaimEnd: claimEnd,
ClaimMultipliers: multipliers,
}
}
// String implements fmt.Stringer
func (ds DistributionSchedule) String() string {
return fmt.Sprintf(`Liquidity Provider Distribution Schedule:
Deposit Denom: %s,
Start: %s,
End: %s,
Rewards Per Second: %s,
Claim End: %s,
Active: %t
`, ds.DepositDenom, ds.Start, ds.End, ds.RewardsPerSecond, ds.ClaimEnd, ds.Active)
}
// Validate performs a basic check of a distribution schedule.
func (ds DistributionSchedule) Validate() error {
if !ds.RewardsPerSecond.IsValid() {
return fmt.Errorf("invalid reward coins %s for %s", ds.RewardsPerSecond, ds.DepositDenom)
}
if !ds.RewardsPerSecond.IsPositive() {
return fmt.Errorf("reward amount must be positive, is %s for %s", ds.RewardsPerSecond, ds.DepositDenom)
}
if ds.Start.IsZero() {
return errors.New("reward period start time cannot be 0")
}
if ds.End.IsZero() {
return errors.New("reward period end time cannot be 0")
}
if ds.Start.After(ds.End) {
return fmt.Errorf("end period time %s cannot be before start time %s", ds.End, ds.Start)
}
if ds.ClaimEnd.Before(ds.End) {
return fmt.Errorf("claim end time %s cannot be before end time %s", ds.ClaimEnd, ds.End)
}
for _, multiplier := range ds.ClaimMultipliers {
if err := multiplier.Validate(); err != nil {
return err
}
}
return nil
}
// DistributionSchedules slice of DistributionSchedule
type DistributionSchedules []DistributionSchedule
// Validate checks if all the LiquidityProviderSchedules are valid and there are no duplicated
// entries.
func (dss DistributionSchedules) Validate() error {
seenPeriods := make(map[string]bool)
for _, ds := range dss {
if seenPeriods[ds.DepositDenom] {
return fmt.Errorf("duplicated distribution provider schedule with deposit denom %s", ds.DepositDenom)
}
if err := ds.Validate(); err != nil {
return err
}
seenPeriods[ds.DepositDenom] = true
}
return nil
}
// String implements fmt.Stringer
func (dss DistributionSchedules) String() string {
out := "Distribution Schedules\n"
for _, ds := range dss {
out += fmt.Sprintf("%s\n", ds)
}
return out
}
// DelegatorDistributionSchedule distribution schedule for delegators
type DelegatorDistributionSchedule struct {
DistributionSchedule DistributionSchedule `json:"distribution_schedule" yaml:"distribution_schedule"`
DistributionFrequency time.Duration `json:"distribution_frequency" yaml:"distribution_frequency"`
}
// NewDelegatorDistributionSchedule returns a new DelegatorDistributionSchedule
func NewDelegatorDistributionSchedule(ds DistributionSchedule, frequency time.Duration) DelegatorDistributionSchedule {
return DelegatorDistributionSchedule{
DistributionSchedule: ds,
DistributionFrequency: frequency,
}
}
// Validate performs a basic check of a reward fields.
func (dds DelegatorDistributionSchedule) Validate() error {
if err := dds.DistributionSchedule.Validate(); err != nil {
return err
}
if dds.DistributionFrequency <= 0 {
return fmt.Errorf("distribution frequency should be positive, got %d", dds.DistributionFrequency)
}
return nil
}
// DelegatorDistributionSchedules slice of DelegatorDistributionSchedule
type DelegatorDistributionSchedules []DelegatorDistributionSchedule
// Validate checks if all the LiquidityProviderSchedules are valid and there are no duplicated
// entries.
func (dds DelegatorDistributionSchedules) Validate() error {
seenPeriods := make(map[string]bool)
for _, ds := range dds {
if seenPeriods[ds.DistributionSchedule.DepositDenom] {
return fmt.Errorf("duplicated liquidity provider schedule with deposit denom %s", ds.DistributionSchedule.DepositDenom)
}
if err := ds.Validate(); err != nil {
return err
}
seenPeriods[ds.DistributionSchedule.DepositDenom] = true
}
return nil
}
// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked
type Multiplier struct {
Name MultiplierName `json:"name" yaml:"name"`
MonthsLockup int64 `json:"months_lockup" yaml:"months_lockup"`
Factor sdk.Dec `json:"factor" yaml:"factor"`
}
// NewMultiplier returns a new Multiplier
func NewMultiplier(name MultiplierName, lockup int64, factor sdk.Dec) Multiplier {
return Multiplier{
Name: name,
MonthsLockup: lockup,
Factor: factor,
}
}
// Validate multiplier param
func (m Multiplier) Validate() error {
if err := m.Name.IsValid(); err != nil {
return err
}
if m.MonthsLockup < 0 {
return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup)
}
if m.Factor.IsNegative() {
return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String())
}
return nil
}
// GetMultiplier returns the named multiplier from the input distribution schedule
func (ds DistributionSchedule) GetMultiplier(name MultiplierName) (Multiplier, bool) {
for _, multiplier := range ds.ClaimMultipliers {
if multiplier.Name == name {
return multiplier, true
}
}
return Multiplier{}, false
}
// Multipliers slice of Multiplier
type Multipliers []Multiplier
// NewParams returns a new params object
func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules) Params {
return Params{
Active: active,
LiquidityProviderSchedules: lps,
DelegatorDistributionSchedules: dds,
}
}
// DefaultParams returns default params for harvest module
func DefaultParams() Params {
return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Active: %t
Liquidity Provider Distribution Schedules %s
Delegator Distribution Schedule %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules)
}
// ParamKeyTable Key declaration for parameters
func ParamKeyTable() params.KeyTable {
return params.NewKeyTable().RegisterParamSet(&Params{})
}
// ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams),
params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateActiveParam(p.Active); err != nil {
return err
}
if err := validateDelegatorParams(p.DelegatorDistributionSchedules); err != nil {
return err
}
return validateLPParams(p.LiquidityProviderSchedules)
}
func validateActiveParam(i interface{}) error {
_, ok := i.(bool)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return nil
}
func validateLPParams(i interface{}) error {
dss, ok := i.(DistributionSchedules)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
for _, ds := range dss {
err := ds.Validate()
if err != nil {
return err
}
}
return nil
}
func validateDelegatorParams(i interface{}) error {
dds, ok := i.(DelegatorDistributionSchedules)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return dds.Validate()
}

View File

@ -0,0 +1,75 @@
package types_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/hvt/types"
)
type ParamTestSuite struct {
suite.Suite
}
func (suite *ParamTestSuite) TestParamValidation() {
type args struct {
lps types.DistributionSchedules
gds types.DistributionSchedules
dds types.DelegatorDistributionSchedules
active bool
}
testCases := []struct {
name string
args args
expectPass bool
expectedErr string
}{
{
name: "default",
args: args{
lps: types.DefaultLPSchedules,
dds: types.DefaultDelegatorSchedules,
active: types.DefaultActive,
},
expectPass: true,
expectedErr: "",
},
{
name: "valid",
args: args{
lps: types.DistributionSchedules{
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
dds: types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
active: true,
},
expectPass: true,
expectedErr: "",
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(tc.args.active, tc.args.lps, tc.args.dds)
err := params.Validate()
if tc.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
}
})
}
}
func TestParamTestSuite(t *testing.T) {
suite.Run(t, new(ParamTestSuite))
}

20
x/hvt/types/period.go Normal file
View File

@ -0,0 +1,20 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// NewPeriod returns a new vesting period
func NewPeriod(amount sdk.Coins, length int64) vesting.Period {
return vesting.Period{Amount: amount, Length: length}
}
// GetTotalVestingPeriodLength returns the summed length of all vesting periods
func GetTotalVestingPeriodLength(periods vesting.Periods) int64 {
length := int64(0)
for _, period := range periods {
length += period.Length
}
return length
}

69
x/hvt/types/querier.go Normal file
View File

@ -0,0 +1,69 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Querier routes for the harvest module
const (
QueryGetParams = "params"
QueryGetModuleAccounts = "accounts"
QueryGetDeposits = "deposits"
QueryGetClaims = "claims"
)
// QueryDepositParams is the params for a filtered deposit query
type QueryDepositParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
DepositType DepositType `json:"deposit_type" yaml:"deposit_type"`
}
// NewQueryDepositParams creates a new QueryDepositParams
func NewQueryDepositParams(page, limit int, depositDenom string, owner sdk.AccAddress, depositType DepositType) QueryDepositParams {
return QueryDepositParams{
Page: page,
Limit: limit,
DepositDenom: depositDenom,
Owner: owner,
DepositType: depositType,
}
}
// QueryClaimParams is the params for a filtered claim query
type QueryClaimParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
DepositDenom string `json:"deposit_denom" yaml:"deposit_denom"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
DepositType DepositType `json:"deposit_type" yaml:"deposit_type"`
}
// NewQueryClaimParams creates a new QueryClaimParams
func NewQueryClaimParams(page, limit int, depositDenom string, owner sdk.AccAddress, depositType DepositType) QueryClaimParams {
return QueryClaimParams{
Page: page,
Limit: limit,
DepositDenom: depositDenom,
Owner: owner,
DepositType: depositType,
}
}
// QueryAccountParams is the params for a filtered module account query
type QueryAccountParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
Name string `json:"name" yaml:"name"`
}
// NewQueryAccountParams returns QueryAccountParams
func NewQueryAccountParams(page, limit int, name string) QueryAccountParams {
return QueryAccountParams{
Page: page,
Limit: limit,
Name: name,
}
}