USDX incentives implementation (#399)

* USDX incentives implementation (#399)
* feat: upgrade to cosmos-sdk v0.38

Co-authored-by: Denali Marsh <denali@kava.io>
Co-authored-by: John Maheswaran <jmaheswaran@users.noreply.github.com>
Co-authored-by: John Maheswaran <john@kava.io>
This commit is contained in:
Kevin Davis 2020-04-24 11:20:34 -04:00 committed by GitHub
parent 5737f4fa19
commit 1ef9bd331b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 2250 additions and 4 deletions

View File

@ -7,6 +7,7 @@ import (
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -69,6 +70,7 @@ var (
pricefeed.AppModuleBasic{},
bep3.AppModuleBasic{},
kavadist.AppModuleBasic{},
incentive.AppModuleBasic{},
)
// module account permissions
@ -121,6 +123,7 @@ type App struct {
pricefeedKeeper pricefeed.Keeper
bep3Keeper bep3.Keeper
kavadistKeeper kavadist.Keeper
incentiveKeeper incentive.Keeper
// the module manager
mm *module.Manager
@ -145,7 +148,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
supply.StoreKey, mint.StoreKey, distr.StoreKey, slashing.StoreKey,
gov.StoreKey, params.StoreKey, evidence.StoreKey, validatorvesting.StoreKey,
auction.StoreKey, cdp.StoreKey, pricefeed.StoreKey, bep3.StoreKey,
kavadist.StoreKey,
kavadist.StoreKey, incentive.StoreKey,
)
tkeys := sdk.NewTransientStoreKeys(params.TStoreKey)
@ -173,6 +176,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
pricefeedSubspace := app.paramsKeeper.Subspace(pricefeed.DefaultParamspace)
bep3Subspace := app.paramsKeeper.Subspace(bep3.DefaultParamspace)
kavadistSubspace := app.paramsKeeper.Subspace(kavadist.DefaultParamspace)
incentiveSubspace := app.paramsKeeper.Subspace(incentive.DefaultParamspace)
// add keepers
app.accountKeeper = auth.NewAccountKeeper(
@ -295,6 +299,14 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
kavadistSubspace,
app.supplyKeeper,
)
app.incentiveKeeper = incentive.NewKeeper(
app.cdc,
keys[incentive.StoreKey],
incentiveSubspace,
app.supplyKeeper,
app.cdpKeeper,
app.accountKeeper,
)
// register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -321,12 +333,13 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
pricefeed.NewAppModule(app.pricefeedKeeper, app.accountKeeper),
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.supplyKeeper),
)
// During begin block slashing happens after distr.BeginBlocker so that
// there is nothing left over in the validator fee pool, so as to keep the
// CanWithdrawInvariant invariant.
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName)
app.mm.SetOrderBeginBlockers(mint.ModuleName, distr.ModuleName, slashing.ModuleName, validatorvesting.ModuleName, kavadist.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, incentive.ModuleName)
app.mm.SetOrderEndBlockers(crisis.ModuleName, gov.ModuleName, staking.ModuleName, pricefeed.ModuleName)
@ -335,7 +348,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
validatorvesting.ModuleName, distr.ModuleName,
staking.ModuleName, bank.ModuleName, slashing.ModuleName,
gov.ModuleName, mint.ModuleName, evidence.ModuleName,
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, // TODO is this order ok?
pricefeed.ModuleName, cdp.ModuleName, auction.ModuleName, bep3.ModuleName, kavadist.ModuleName, incentive.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.
@ -363,6 +376,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
auction.NewAppModule(app.auctionKeeper, app.accountKeeper, app.supplyKeeper),
bep3.NewAppModule(app.bep3Keeper, app.accountKeeper, app.supplyKeeper),
kavadist.NewAppModule(app.kavadistKeeper, app.supplyKeeper),
incentive.NewAppModule(app.incentiveKeeper, app.supplyKeeper),
)
app.sm.RegisterStoreDecoders()

View File

@ -30,6 +30,7 @@ import (
"github.com/kava-labs/kava/x/auction"
"github.com/kava-labs/kava/x/bep3"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/kavadist"
"github.com/kava-labs/kava/x/pricefeed"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
@ -75,6 +76,7 @@ func (tApp TestApp) GetCDPKeeper() cdp.Keeper { return tApp.cdpKeepe
func (tApp TestApp) GetPriceFeedKeeper() pricefeed.Keeper { return tApp.pricefeedKeeper }
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 }
// This calls InitChain on the app using the default genesis state, overwitten with any passed in genesis states
func (tApp TestApp) InitializeFromGenesisStates(genesisStates ...GenesisState) TestApp {

1
go.sum
View File

@ -416,6 +416,7 @@ github.com/tendermint/iavl v0.13.2/go.mod h1:vE1u0XAGXYjHykd4BLp8p/yivrw2PF1Tuol
github.com/tendermint/tendermint v0.33.2/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
github.com/tendermint/tendermint v0.33.3 h1:6lMqjEoCGejCzAghbvfQgmw87snGSqEhDTo/jw+W8CI=
github.com/tendermint/tendermint v0.33.3/go.mod h1:25DqB7YvV1tN3tHsjWoc2vFtlwICfrub9XO6UBO+4xk=
github.com/tendermint/tendermint v0.33.4 h1:NM3G9618yC5PaaxGrcAySc5ylc1PAANeIx42u2Re/jo=
github.com/tendermint/tm-db v0.4.1/go.mod h1:JsJ6qzYkCGiGwm5GHl/H5GLI9XLb6qZX7PRe425dHAY=
github.com/tendermint/tm-db v0.5.0 h1:qtM5UTr1dlRnHtDY6y7MZO5Di8XAE2j3lc/pCnKJ5hQ=
github.com/tendermint/tm-db v0.5.0/go.mod h1:lSq7q5WRR/njf1LnhiZ/lIJHk2S8Y1Zyq5oP/3o9C2U=

View File

@ -37,7 +37,6 @@ func queryAtomicSwapHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
swapID, err := types.HexToBytes(vars[restSwapID])
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())

13
x/incentive/abci.go Normal file
View File

@ -0,0 +1,13 @@
package incentive
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper"
)
// BeginBlocker runs at the start of every block
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
k.DeleteExpiredClaimsAndClaimPeriods(ctx)
k.ApplyRewardsToCdps(ctx)
k.CreateAndDeleteRewardPeriods(ctx)
}

92
x/incentive/alias.go Normal file
View File

@ -0,0 +1,92 @@
// nolint
// autogenerated code using github.com/rigelrozanski/multitool
// aliases generated for the following subdirectories:
// ALIASGEN: github.com/kava-labs/kava/x/incentive/keeper
// ALIASGEN: github.com/kava-labs/kava/x/incentive/types
package incentive
import (
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
const (
EventTypeClaim = types.EventTypeClaim
AttributeValueCategory = types.AttributeValueCategory
AttributeKeySender = types.AttributeKeySender
ModuleName = types.ModuleName
StoreKey = types.StoreKey
RouterKey = types.RouterKey
DefaultParamspace = types.DefaultParamspace
QuerierRoute = types.QuerierRoute
QueryGetClaims = types.QueryGetClaims
RestClaimOwner = types.RestClaimOwner
RestClaimDenom = types.RestClaimDenom
QueryGetParams = types.QueryGetParams
)
var (
// functions aliases
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
RegisterCodec = types.RegisterCodec
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState
BytesToUint64 = types.BytesToUint64
GetClaimPeriodPrefix = types.GetClaimPeriodPrefix
GetClaimPrefix = types.GetClaimPrefix
NewMsgClaimReward = types.NewMsgClaimReward
NewParams = types.NewParams
DefaultParams = types.DefaultParams
ParamKeyTable = types.ParamKeyTable
NewReward = types.NewReward
NewPeriod = types.NewPeriod
NewQueryClaimsParams = types.NewQueryClaimsParams
NewRewardPeriod = types.NewRewardPeriod
NewClaimPeriod = types.NewClaimPeriod
NewClaim = types.NewClaim
// variable aliases
ModuleCdc = types.ModuleCdc
ErrClaimNotFound = types.ErrClaimNotFound
ErrClaimPeriodNotFound = types.ErrClaimPeriodNotFound
ErrInvalidAccountType = types.ErrInvalidAccountType
ErrNoClaimsFound = types.ErrNoClaimsFound
ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix
ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix
ClaimKeyPrefix = types.ClaimKeyPrefix
NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
KeyActive = types.KeyActive
KeyRewards = types.KeyRewards
DefaultActive = types.DefaultActive
DefaultRewards = types.DefaultRewards
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
GovDenom = types.GovDenom
PrincipalDenom = types.PrincipalDenom
IncentiveMacc = types.IncentiveMacc
)
type (
Keeper = keeper.Keeper
SupplyKeeper = types.SupplyKeeper
CdpKeeper = types.CdpKeeper
AccountKeeper = types.AccountKeeper
GenesisClaimPeriodID = types.GenesisClaimPeriodID
GenesisClaimPeriodIDs = types.GenesisClaimPeriodIDs
GenesisState = types.GenesisState
MsgClaimReward = types.MsgClaimReward
Params = types.Params
Reward = types.Reward
Rewards = types.Rewards
QueryClaimsParams = types.QueryClaimsParams
PostClaimReq = types.PostClaimReq
RewardPeriod = types.RewardPeriod
RewardPeriods = types.RewardPeriods
ClaimPeriod = types.ClaimPeriod
ClaimPeriods = types.ClaimPeriods
Claim = types.Claim
Claims = types.Claims
)

View File

@ -0,0 +1,98 @@
package cli
import (
"fmt"
"strings"
"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/kava-labs/kava/x/incentive/types"
"github.com/spf13/cobra"
)
// GetQueryCmd returns the cli query commands for the incentive module
func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
incentiveQueryCmd := &cobra.Command{
Use: types.ModuleName,
Short: "Querying commands for the incentive module",
}
incentiveQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc),
)...)
return incentiveQueryCmd
}
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claims [owner-addr] [denom]",
Short: "get claims by onwer and denom",
Long: strings.TrimSpace(
fmt.Sprintf(`Get all claims owned by the owner address for the particular collateral type.
Example:
$ %s query %s claims kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
bz, err := cdc.MarshalJSON(types.QueryClaimsParams{
Owner: ownerAddress,
Denom: args[1],
})
if err != nil {
return err
}
// Query
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetClaims)
res, _, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
var claims types.Claims
if err := cdc.UnmarshalJSON(res, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims: %w", err)
}
return cliCtx.PrintOutput(claims)
},
}
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "params",
Short: "get the incentive module parameters",
Long: "Get the current global incentive 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, _, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
// 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)
},
}
}

View File

@ -0,0 +1,63 @@
package cli
import (
"bufio"
"fmt"
"strings"
"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/incentive/types"
"github.com/spf13/cobra"
)
// GetTxCmd returns the transaction cli commands for the incentive module
func GetTxCmd(cdc *codec.Codec) *cobra.Command {
incentiveTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "transaction commands for the incentive module",
}
incentiveTxCmd.AddCommand(flags.PostCommands(
getCmdClaim(cdc),
)...)
return incentiveTxCmd
}
func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claim [owner] [denom]",
Short: "claim rewards for owner and denom",
Long: strings.TrimSpace(
fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input denom,
Example:
$ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb
`, version.ClientName, types.ModuleName),
),
Args: cobra.ExactArgs(2),
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))
owner, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgClaimReward(owner, args[1])
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -0,0 +1,70 @@
package rest
import (
"fmt"
"net/http"
"github.com/cosmos/cosmos-sdk/client/context"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/types"
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/claims", types.ModuleName), queryClaimsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/parameters", types.ModuleName), queryParamsHandlerFn(cliCtx)).Methods("GET")
}
func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
ownerBech32 := vars[types.RestClaimOwner]
denom := vars[types.RestClaimDenom]
owner, err := sdk.AccAddressFromBech32(ownerBech32)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
queryParams := types.NewQueryClaimsParams(owner, denom)
bz, err := cliCtx.Codec.MarshalJSON(queryParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/incentive/%s", types.QueryGetClaims), bz)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
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)
}
}

View File

@ -0,0 +1,13 @@
package rest
import (
"github.com/gorilla/mux"
"github.com/cosmos/cosmos-sdk/client/context"
)
// RegisterRoutes registers incentive-related REST handlers to a router
func RegisterRoutes(cliCtx context.CLIContext, r *mux.Router) {
registerQueryRoutes(cliCtx, r)
registerTxRoutes(cliCtx, r)
}

View File

@ -0,0 +1,36 @@
package rest
import (
"net/http"
"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/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/types"
)
func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/incentive/claim", postClaimHandlerFn(cliCtx)).Methods("POST")
}
func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody types.PostClaimReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.Denom)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}

40
x/incentive/doc.go Normal file
View File

@ -0,0 +1,40 @@
/*
Package incentive implements a Cosmos SDK module, per ADR 009, that provides governance-controlled,
on-chain incentives for users who open cdps and mint stablecoins (USDX).
For the background and motivation of this module, see the governance proposal that was voted on by
KAVA token holders: https://ipfs.io/ipfs/QmSYedssC3nyQacDJmNcREtgmTPyaMx2JX7RNkMdAVkdkr/user-growth-fund-proposal.pdf
The 'Reward' parameter is used to control how much incentives are given.
For example, the following reward:
Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", 1000000000),
Duration: time.Hour*7*24,
TimeLock: time.Hour*24*365,
ClaimDuration: time.Hour*7*24,
}
will distribute 1000 KAVA each week (Duration) to users who mint USDX using collateral bnb.
That KAVA can be claimed by the user for one week (ClaimDuration) after the reward period expires,
and all KAVA rewards will be timelocked for 1 year (TimeLock). If a user does not claim them during
the claim duration period, they are forgone.
Rewards are accumulated by users continuously and proportionally - ie. if a user holds a CDP that has
minted 10% of all USDX backed by bnb for the entire reward period, they will be eligible to claim 10%
of rewards for that period.
Once a reward period ends, but not before, users can claim the rewards they have accumulated. Users claim rewards
using a MsgClaimReward transaction. The following msg:
MsgClaimReward {
"kava1..."
"bnb"
}
will claim all outstanding rewards for minting USDX backed by bnb for the input user.
*/
package incentive

73
x/incentive/genesis.go Normal file
View File

@ -0,0 +1,73 @@
package incentive
import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
// InitGenesis initializes the store state from a genesis state.
func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeeper, gs types.GenesisState) {
// check if the module account exists
moduleAcc := supplyKeeper.GetModuleAccount(ctx, types.IncentiveMacc)
if moduleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", types.IncentiveMacc))
}
if err := gs.Validate(); err != nil {
panic(fmt.Sprintf("failed to validate %s genesis state: %s", types.ModuleName, err))
}
k.SetParams(ctx, gs.Params)
for _, r := range gs.Params.Rewards {
k.SetNextClaimPeriodID(ctx, r.Denom, 1)
}
// only set the previous block time if it's different than default
if !gs.PreviousBlockTime.Equal(types.DefaultPreviousBlockTime) {
k.SetPreviousBlockTime(ctx, gs.PreviousBlockTime)
}
// set store objects
for _, rp := range gs.RewardPeriods {
k.SetRewardPeriod(ctx, rp)
}
for _, cp := range gs.ClaimPeriods {
k.SetClaimPeriod(ctx, cp)
}
for _, c := range gs.Claims {
k.SetClaim(ctx, c)
}
for _, id := range gs.NextClaimPeriodIDs {
k.SetNextClaimPeriodID(ctx, id.Denom, id.ID)
}
}
// ExportGenesis export genesis state for incentive module
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
// get all objects out of the store
params := k.GetParams(ctx)
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
// since it is not set in genesis, if somehow the chain got started and was exported
// immediately after InitGenesis, there would be no previousBlockTime value.
if !found {
previousBlockTime = types.DefaultPreviousBlockTime
}
// Get all objects from the store
rewardPeriods := k.GetAllRewardPeriods(ctx)
claimPeriods := k.GetAllClaimPeriods(ctx)
claims := k.GetAllClaims(ctx)
claimPeriodIDs := k.GetAllClaimPeriodIDPairs(ctx)
return types.NewGenesisState(params, previousBlockTime, rewardPeriods, claimPeriods, claims, claimPeriodIDs)
}

48
x/incentive/handler.go Normal file
View File

@ -0,0 +1,48 @@
package incentive
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
// NewHandler creates an sdk.Handler for incentive module messages
func NewHandler(k keeper.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)
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) {
claims, found := k.GetClaimsByAddressAndDenom(ctx, msg.Sender, msg.Denom)
if !found {
return nil, sdkerrors.Wrapf(types.ErrNoClaimsFound, "address: %s, denom: %s", msg.Sender, msg.Denom)
}
for _, claim := range claims {
err := k.PayoutClaim(ctx, claim.Owner, claim.Denom, claim.ClaimPeriodID)
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
}

View File

@ -0,0 +1,243 @@
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/incentive/types"
)
// Keeper keeper for the incentive module
type Keeper struct {
accountKeeper types.AccountKeeper
cdc *codec.Codec
cdpKeeper types.CdpKeeper
key sdk.StoreKey
paramSubspace subspace.Subspace
supplyKeeper types.SupplyKeeper
}
// NewKeeper creates a new keeper
func NewKeeper(
cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, sk types.SupplyKeeper,
cdpk types.CdpKeeper, ak types.AccountKeeper,
) Keeper {
return Keeper{
accountKeeper: ak,
cdc: cdc,
cdpKeeper: cdpk,
key: key,
paramSubspace: paramstore.WithKeyTable(types.ParamKeyTable()),
supplyKeeper: sk,
}
}
// GetRewardPeriod returns the reward period from the store for the input denom and a boolean for if it was found
func (k Keeper) GetRewardPeriod(ctx sdk.Context, denom string) (types.RewardPeriod, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
bz := store.Get([]byte(denom))
if bz == nil {
return types.RewardPeriod{}, false
}
var rp types.RewardPeriod
k.cdc.MustUnmarshalBinaryBare(bz, &rp)
return rp, true
}
// SetRewardPeriod sets the reward period in the store for the input deno,
func (k Keeper) SetRewardPeriod(ctx sdk.Context, rp types.RewardPeriod) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(rp)
store.Set([]byte(rp.Denom), bz)
}
// DeleteRewardPeriod deletes the reward period in the store for the input denom,
func (k Keeper) DeleteRewardPeriod(ctx sdk.Context, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(denom))
}
// IterateRewardPeriods iterates over all reward period objects in the store and preforms a callback function
func (k Keeper) IterateRewardPeriods(ctx sdk.Context, cb func(rp types.RewardPeriod) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var rp types.RewardPeriod
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &rp)
if cb(rp) {
break
}
}
}
// GetAllRewardPeriods returns all reward periods in the store
func (k Keeper) GetAllRewardPeriods(ctx sdk.Context) types.RewardPeriods {
rps := types.RewardPeriods{}
k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) (stop bool) {
rps = append(rps, rp)
return false
})
return rps
}
// GetNextClaimPeriodID returns the highest claim period id in the store for the input denom
func (k Keeper) GetNextClaimPeriodID(ctx sdk.Context, denom string) uint64 {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
bz := store.Get([]byte(denom))
return types.BytesToUint64(bz)
}
// SetNextClaimPeriodID sets the highest claim period id in the store for the input denom
func (k Keeper) SetNextClaimPeriodID(ctx sdk.Context, denom string, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
store.Set([]byte(denom), sdk.Uint64ToBigEndian(id))
}
// IterateClaimPeriodIDKeysAndValues iterates over the claim period id (value) and denom (key) of each claim period id in the store and performs a callback function
func (k Keeper) IterateClaimPeriodIDKeysAndValues(ctx sdk.Context, cb func(denom string, id uint64) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
id := types.BytesToUint64(iterator.Value())
denom := string(iterator.Key())
if cb(denom, id) {
break
}
}
}
// GetAllClaimPeriodIDPairs returns all denom:nextClaimPeriodID pairs in the store
func (k Keeper) GetAllClaimPeriodIDPairs(ctx sdk.Context) types.GenesisClaimPeriodIDs {
ids := types.GenesisClaimPeriodIDs{}
k.IterateClaimPeriodIDKeysAndValues(ctx, func(denom string, id uint64) (stop bool) {
genID := types.GenesisClaimPeriodID{
Denom: denom,
ID: id,
}
ids = append(ids, genID)
return false
})
return ids
}
// GetClaimPeriod returns claim period in the store for the input ID and denom and a boolean for if it was found
func (k Keeper) GetClaimPeriod(ctx sdk.Context, id uint64, denom string) (types.ClaimPeriod, bool) {
var cp types.ClaimPeriod
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
bz := store.Get(types.GetClaimPeriodPrefix(denom, id))
if bz == nil {
return types.ClaimPeriod{}, false
}
k.cdc.MustUnmarshalBinaryBare(bz, &cp)
return cp, true
}
// SetClaimPeriod sets the claim period in the store for the input ID and denom
func (k Keeper) SetClaimPeriod(ctx sdk.Context, cp types.ClaimPeriod) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(cp)
store.Set(types.GetClaimPeriodPrefix(cp.Denom, cp.ID), bz)
}
// DeleteClaimPeriod deletes the claim period in the store for the input ID and denom
func (k Keeper) DeleteClaimPeriod(ctx sdk.Context, id uint64, denom string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
store.Delete(types.GetClaimPeriodPrefix(denom, id))
}
// IterateClaimPeriods iterates over all claim period objects in the store and preforms a callback function
func (k Keeper) IterateClaimPeriods(ctx sdk.Context, cb func(cp types.ClaimPeriod) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var cp types.ClaimPeriod
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &cp)
if cb(cp) {
break
}
}
}
// GetAllClaimPeriods returns all ClaimPeriod objects in the store
func (k Keeper) GetAllClaimPeriods(ctx sdk.Context) types.ClaimPeriods {
cps := types.ClaimPeriods{}
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
cps = append(cps, cp)
return false
})
return cps
}
// GetClaim returns the claim in the store corresponding the the input address denom and id and a boolean for if the claim was found
func (k Keeper) GetClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) (types.Claim, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := store.Get(types.GetClaimPrefix(addr, denom, id))
if bz == nil {
return types.Claim{}, false
}
var c types.Claim
k.cdc.MustUnmarshalBinaryBare(bz, &c)
return c, true
}
// SetClaim sets the claim in the store corresponding to the input address, denom, and id
func (k Keeper) SetClaim(ctx sdk.Context, c types.Claim) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(c)
store.Set(types.GetClaimPrefix(c.Owner, c.Denom, c.ClaimPeriodID), bz)
}
// DeleteClaim deletes the claim in the store corresponding to the input address, denom, and id
func (k Keeper) DeleteClaim(ctx sdk.Context, owner sdk.AccAddress, denom string, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
store.Delete(types.GetClaimPrefix(owner, denom, id))
}
// IterateClaims iterates over all claim objects in the store and preforms a callback function
func (k Keeper) IterateClaims(ctx sdk.Context, cb func(c types.Claim) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var c types.Claim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c)
if cb(c) {
break
}
}
}
// GetAllClaims returns all Claim objects in the store
func (k Keeper) GetAllClaims(ctx sdk.Context) types.Claims {
cs := types.Claims{}
k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
cs = append(cs, c)
return false
})
return cs
}
// 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))
}

View File

@ -0,0 +1,18 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/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)
}

View File

@ -0,0 +1,204 @@
package keeper
import (
"fmt"
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/incentive/types"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
)
// PayoutClaim sends the timelocked claim coins to the input address
func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64) error {
claim, found := k.GetClaim(ctx, addr, denom, id)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, denom %s, address: %s", id, denom, addr)
}
claimPeriod, found := k.GetClaimPeriod(ctx, id, denom)
if !found {
return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, denom: %s", id, denom)
}
err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(claim.Reward), int64(claimPeriod.TimeLock.Seconds()))
if err != nil {
return err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeySender, fmt.Sprintf("%s", addr)),
),
)
return nil
}
// SendTimeLockedCoinsToAccount sends time-locked coins from the input module account to the recipient. If the recipients account is not a vesting account, it 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)
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
}
// DeleteExpiredClaimsAndClaimPeriods deletes expired claim periods and their associated claims
func (k Keeper) DeleteExpiredClaimsAndClaimPeriods(ctx sdk.Context) {
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if !cp.End.Before(ctx.BlockTime()) {
return false
}
k.IterateClaims(ctx, func(c types.Claim) (stop bool) {
if !(c.Denom == cp.Denom && c.ClaimPeriodID == cp.ID) {
return false
}
k.DeleteClaim(ctx, c.Owner, c.Denom, c.ClaimPeriodID)
return false
})
k.DeleteClaimPeriod(ctx, cp.ID, cp.Denom)
return false
})
}
// GetClaimsByAddressAndDenom returns all claims for a specific user and address and a bool for if any were found
func (k Keeper) GetClaimsByAddressAndDenom(ctx sdk.Context, addr sdk.AccAddress, denom string) (claims types.Claims, found bool) {
found = false
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if cp.Denom != denom {
return false
}
c, hasClaim := k.GetClaim(ctx, addr, cp.Denom, cp.ID)
if !hasClaim {
return false
}
found = true
claims = append(claims, c)
return false
})
return claims, found
}
// 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...)
// update vesting periods
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)
}
updatedPeriods = append(updatedPeriods, updatedPeriod)
}
vacc.VestingPeriods = updatedPeriods
vacc.StartTime = ctx.BlockTime().Unix()
}
// logic for inserting a new vesting period into the existing vesting schedule
totalPeriodLength := types.GetTotalVestingPeriodLength(vacc.VestingPeriods)
proposedEndTime := ctx.BlockTime().Unix() + length
if totalPeriodLength < length {
// in the case that the proposed length is longer than the sum of all previous period lengths, create a new period with length equal to the difference between the proposed length and the previous total length
newPeriodLength := length - totalPeriodLength
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.
// EXAMPLE (l is length, a is amount)
// Original Periods: {[l: 1 a: 1], [l: 2, a: 1], [l:8, a:3], [l: 5, a: 3]}
// Period we want to insert [l: 5, a: x]
// Expected result:
// {[l: 1, a: 1], [l:2, a: 1], [l:2, a:x], [l:6, a:3], [l:5, a:3]}
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 < length {
newPeriods = append(newPeriods, period)
} else if lengthCounter == length {
newPeriod := types.NewPeriod(period.Amount.Add(amt...), period.Length)
newPeriods = append(newPeriods, newPeriod)
appendRemaining = true
} else {
newPeriod := types.NewPeriod(amt, 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,51 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/incentive/types"
abci "github.com/tendermint/tendermint/abci/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.QueryGetClaims:
return queryGetClaims(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
}
}
// query params in the store
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 queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var requestParams types.QueryClaimsParams
err := k.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
claims, _ := k.GetClaimsByAddressAndDenom(ctx, requestParams.Owner, requestParams.Denom)
bz, err := codec.MarshalJSONIndent(k.cdc, claims)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -0,0 +1,110 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/types"
)
// HandleRewardPeriodExpiry deletes expired RewardPeriods from the store and creates a ClaimPeriod in the store for each expired RewardPeriod
func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) {
k.CreateUniqueClaimPeriod(ctx, rp.Denom, rp.ClaimEnd, rp.ClaimTimeLock)
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(rp.Denom))
return
}
// CreateNewRewardPeriod creates a new reward period from the input reward
func (k Keeper) CreateNewRewardPeriod(ctx sdk.Context, reward types.Reward) {
// reward periods store the amount of rewards payed PER SECOND
rewardsPerSecond := sdk.NewDecFromInt(reward.AvailableRewards.Amount).Quo(sdk.NewDecFromInt(sdk.NewInt(int64(reward.Duration.Seconds())))).TruncateInt()
rewardCoinPerSecond := sdk.NewCoin(reward.AvailableRewards.Denom, rewardsPerSecond)
rp := types.RewardPeriod{
Denom: reward.Denom,
Start: ctx.BlockTime(),
End: ctx.BlockTime().Add(reward.Duration),
Reward: rewardCoinPerSecond,
ClaimEnd: ctx.BlockTime().Add(reward.Duration).Add(reward.ClaimDuration),
ClaimTimeLock: reward.TimeLock,
}
k.SetRewardPeriod(ctx, rp)
}
// CreateAndDeleteRewardPeriods creates reward periods for active rewards that don't already have a reward period and deletes reward periods for inactive rewards that currently have a reward period
func (k Keeper) CreateAndDeleteRewardPeriods(ctx sdk.Context) {
params := k.GetParams(ctx)
for _, r := range params.Rewards {
_, found := k.GetRewardPeriod(ctx, r.Denom)
// if governance has made a reward inactive, delete the current period
if found && !r.Active {
k.DeleteRewardPeriod(ctx, r.Denom)
}
// if a reward period for an active reward is not found, create one
if !found && r.Active {
k.CreateNewRewardPeriod(ctx, r)
}
}
}
// ApplyRewardsToCdps iterates over the reward periods and creates a claim for each cdp owner that created usdx with the collateral specified in the reward period
func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) {
previousBlockTime, found := k.GetPreviousBlockTime(ctx)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
return
}
k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) bool {
expired := false
// the total amount of usdx created with the collateral type being incentivized
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rp.Denom, types.PrincipalDenom)
// the number of seconds since last payout
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix())
if rp.End.Before(ctx.BlockTime()) {
timeElapsed = sdk.NewInt(rp.End.Unix() - previousBlockTime.Unix())
expired = true
}
// the amount of rewards to pay (rewardAmount * timeElapsed)
rewardsThisPeriod := rp.Reward.Amount.Mul(timeElapsed)
id := k.GetNextClaimPeriodID(ctx, rp.Denom)
k.cdpKeeper.IterateCdpsByDenom(ctx, rp.Denom, func(cdp cdptypes.CDP) bool {
rewardsShare := sdk.NewDecFromInt(cdp.Principal.AmountOf(types.PrincipalDenom).Add(cdp.AccumulatedFees.AmountOf(types.PrincipalDenom))).Quo(sdk.NewDecFromInt(totalPrincipal))
// sanity check - don't create zero claims
if rewardsShare.IsZero() {
return false
}
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsThisPeriod)).RoundInt()
k.AddToClaim(ctx, cdp.Owner, rp.Denom, id, sdk.NewCoin(types.GovDenom, rewardsEarned))
return false
})
if !expired {
return false
}
k.HandleRewardPeriodExpiry(ctx, rp)
return false
})
k.SetPreviousBlockTime(ctx, ctx.BlockTime())
}
// CreateUniqueClaimPeriod creates a new claim period in the store and updates the highest claim period id
func (k Keeper) CreateUniqueClaimPeriod(ctx sdk.Context, denom string, end time.Time, timeLock time.Duration) {
id := k.GetNextClaimPeriodID(ctx, denom)
claimPeriod := types.NewClaimPeriod(denom, id, end, timeLock)
k.SetClaimPeriod(ctx, claimPeriod)
k.SetNextClaimPeriodID(ctx, denom, id+1)
}
// AddToClaim adds the amount to an existing claim or creates a new one for that amount
func (k Keeper) AddToClaim(ctx sdk.Context, addr sdk.AccAddress, denom string, id uint64, amount sdk.Coin) {
claim, found := k.GetClaim(ctx, addr, denom, id)
if found {
claim.Reward = claim.Reward.Add(amount)
} else {
claim = types.NewClaim(addr, amount, denom, id)
}
k.SetClaim(ctx, claim)
}

164
x/incentive/module.go Normal file
View File

@ -0,0 +1,164 @@
package incentive
import (
"encoding/json"
"math/rand"
"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"
"github.com/gorilla/mux"
"github.com/kava-labs/kava/x/incentive/client/cli"
"github.com/kava-labs/kava/x/incentive/client/rest"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/simulation"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
_ module.AppModuleSimulation = AppModule{}
)
// AppModuleBasic defines the basic application module used by the incentive module.
type AppModuleBasic struct{}
// Name returns the incentive module's name.
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterCodec registers the incentive module's types for the given codec.
func (AppModuleBasic) RegisterCodec(cdc *codec.Codec) {
types.RegisterCodec(cdc)
}
// DefaultGenesis returns default genesis state as raw bytes for the incentive
// module.
func (AppModuleBasic) DefaultGenesis() json.RawMessage {
return types.ModuleCdc.MustMarshalJSON(types.DefaultGenesisState())
}
// ValidateGenesis performs genesis state validation for the incentive module.
func (AppModuleBasic) ValidateGenesis(bz json.RawMessage) error {
var gs types.GenesisState
err := types.ModuleCdc.UnmarshalJSON(bz, &gs)
if err != nil {
return err
}
return gs.Validate()
}
// RegisterRESTRoutes registers the REST routes for the incentive module.
func (AppModuleBasic) RegisterRESTRoutes(ctx context.CLIContext, rtr *mux.Router) {
rest.RegisterRoutes(ctx, rtr)
}
// GetTxCmd returns the root tx command for the incentive module.
func (AppModuleBasic) GetTxCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetTxCmd(cdc)
}
// GetQueryCmd returns no root query command for the crisis module.
func (AppModuleBasic) GetQueryCmd(cdc *codec.Codec) *cobra.Command {
return cli.GetQueryCmd(types.StoreKey, cdc)
}
// RegisterStoreDecoder registers a decoder for cdp module's types
func (AppModuleBasic) RegisterStoreDecoder(sdr sdk.StoreDecoderRegistry) {
sdr[StoreKey] = simulation.DecodeStore
}
// GenerateGenesisState creates a randomized GenState of the cdp module
func (AppModuleBasic) GenerateGenesisState(simState *module.SimulationState) {
simulation.RandomizedGenState(simState)
}
// RandomizedParams creates randomized cdp param changes for the simulator.
func (AppModuleBasic) RandomizedParams(r *rand.Rand) []sim.ParamChange {
return simulation.ParamChanges(r)
}
// ProposalContents doesn't return any content functions for governance proposals.
func (AppModuleBasic) ProposalContents(_ module.SimulationState) []sim.WeightedProposalContent {
return nil
}
// WeightedOperations returns the all the bep3 module operations with their respective weights.
func (am AppModule) WeightedOperations(_ module.SimulationState) []sim.WeightedOperation {
return nil
}
// AppModule implements the sdk.AppModule interface.
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
supplyKeeper types.SupplyKeeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper keeper.Keeper, supplyKeeper types.SupplyKeeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
supplyKeeper: supplyKeeper,
}
}
// Name returns the incentive module's name.
func (AppModule) Name() string {
return types.ModuleName
}
// RegisterInvariants registers the incentive module invariants.
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route returns the message routing key for the incentive module.
func (AppModule) Route() string {
return types.RouterKey
}
// NewHandler returns an sdk.Handler for the incentive module.
func (am AppModule) NewHandler() sdk.Handler {
return NewHandler(am.keeper)
}
// QuerierRoute returns the incentive module's querier route name.
func (AppModule) QuerierRoute() string {
return types.QuerierRoute
}
// NewQuerierHandler returns the incentive module sdk.Querier.
func (am AppModule) NewQuerierHandler() sdk.Querier {
return keeper.NewQuerier(am.keeper)
}
// InitGenesis performs genesis initialization for the incentive module. It returns no validator updates.
func (am AppModule) InitGenesis(ctx sdk.Context, data json.RawMessage) []abci.ValidatorUpdate {
var gs types.GenesisState
types.ModuleCdc.MustUnmarshalJSON(data, &gs)
InitGenesis(ctx, am.keeper, am.supplyKeeper, gs)
return []abci.ValidatorUpdate{}
}
// ExportGenesis returns the exported genesis state as raw bytes for the incentive module
func (am AppModule) ExportGenesis(ctx sdk.Context) json.RawMessage {
gs := ExportGenesis(ctx, am.keeper)
return types.ModuleCdc.MustMarshalJSON(gs)
}
// BeginBlock returns the begin blocker for the incentive module.
func (am AppModule) BeginBlock(ctx sdk.Context, req abci.RequestBeginBlock) {
BeginBlocker(ctx, am.keeper)
}
// EndBlock returns the end blocker for the incentive module. It returns no validator updates.
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

View File

@ -0,0 +1,12 @@
package simulation
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/tendermint/tendermint/libs/kv"
)
// DecodeStore unmarshals the KVPair's Value to the corresponding incentive type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
// TODO implement this
return ""
}

View File

@ -0,0 +1,14 @@
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 {
// TODO implement this
return []simulation.ParamChange{}
}

View File

@ -0,0 +1,22 @@
package simulation
import (
"fmt"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/kava-labs/kava/x/incentive/types"
)
// RandomizedGenState generates a random GenesisState for cdp
func RandomizedGenState(simState *module.SimulationState) {
// TODO implement this fully
// - randomly generating the genesis params
// - overwriting with genesis provided to simulation
genesis := types.DefaultGenesisState()
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, genesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(genesis)
}

View File

@ -0,0 +1,14 @@
package types
import (
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
)
// 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
}

View File

@ -0,0 +1,51 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
)
type accountTest struct {
periods vesting.Periods
expectedVal int64
}
type AccountTestSuite struct {
suite.Suite
tests []accountTest
}
func (suite *AccountTestSuite) SetupTest() {
tests := []accountTest{
accountTest{
periods: vesting.Periods{
vesting.Period{
Length: int64(100),
Amount: sdk.Coins{},
},
vesting.Period{
Length: int64(200),
Amount: sdk.Coins{},
},
},
expectedVal: int64(300),
},
}
suite.tests = tests
}
func (suite *AccountTestSuite) TestGetTotalPeriodLength() {
for _, t := range suite.tests {
length := types.GetTotalVestingPeriodLength(t.periods)
suite.Equal(t.expectedVal, length)
}
}
func TestAccountTestSuite(t *testing.T) {
suite.Run(t, new(AccountTestSuite))
}

View File

@ -0,0 +1,22 @@
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 incentive module
func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgClaimReward{}, "incentive/MsgClaimReward", nil)
cdc.RegisterConcrete(GenesisClaimPeriodID{}, "incentive/GenesisClaimPeriodID", nil)
cdc.RegisterConcrete(RewardPeriod{}, "incentive/RewardPeriod", nil)
cdc.RegisterConcrete(ClaimPeriod{}, "incentive/ClaimPeriod", nil)
cdc.RegisterConcrete(Claim{}, "incentive/Claim", nil)
}

View File

@ -0,0 +1,15 @@
package types
import (
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// DONTCOVER
var (
ErrClaimNotFound = sdkerrors.Register(ModuleName, 1, "no claim with input id found for owner and denom")
ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 2, "no claim period found for id and denom")
ErrInvalidAccountType = sdkerrors.Register(ModuleName, 3, "account type not supported")
ErrNoClaimsFound = sdkerrors.Register(ModuleName, 4, "no claims with denom found for address")
ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 5, "module account has insufficient balance to pay claim")
)

View File

@ -0,0 +1,8 @@
package types
const (
EventTypeClaim = "claim_reward"
AttributeValueCategory = ModuleName
AttributeKeySender = "sender"
)

View File

@ -0,0 +1,27 @@
package types
import (
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"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
)
// SupplyKeeper defines the expected supply keeper for module accounts
type SupplyKeeper interface {
GetModuleAccount(ctx sdk.Context, name string) supplyexported.ModuleAccountI
SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error
}
// CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface {
IterateCdpsByDenom(ctx sdk.Context, denom string, cb func(cdp cdptypes.CDP) (stop bool))
GetTotalPrincipal(ctx sdk.Context, collateralDenom string, principalDenom string) (total sdk.Int)
}
// 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)
}

View File

@ -0,0 +1,75 @@
package types
import (
"bytes"
"fmt"
"time"
)
// GenesisClaimPeriodID stores the next claim id and its corresponding denom
type GenesisClaimPeriodID struct {
Denom string `json:"denom" yaml:"denom"`
ID uint64 `json:"id" yaml:"id"`
}
// GenesisClaimPeriodIDs array of GenesisClaimPeriodID
type GenesisClaimPeriodIDs []GenesisClaimPeriodID
// 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"`
RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"`
ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"`
Claims Claims `json:"claims" yaml:"claims"`
NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, previousBlockTime time.Time, rp RewardPeriods, cp ClaimPeriods, c Claims, ids GenesisClaimPeriodIDs) GenesisState {
return GenesisState{
Params: params,
PreviousBlockTime: previousBlockTime,
RewardPeriods: rp,
ClaimPeriods: cp,
Claims: c,
NextClaimPeriodIDs: ids,
}
}
// DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
PreviousBlockTime: DefaultPreviousBlockTime,
RewardPeriods: RewardPeriods{},
ClaimPeriods: ClaimPeriods{},
Claims: Claims{},
NextClaimPeriodIDs: GenesisClaimPeriodIDs{},
}
}
// 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")
}
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{})
}

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

@ -0,0 +1,61 @@
package types
import (
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "incentive"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
// QuerierRoute route used for abci queries
QuerierRoute = ModuleName
)
// Key Prefixes
var (
RewardPeriodKeyPrefix = []byte{0x01} // prefix for keys that store reward periods
ClaimPeriodKeyPrefix = []byte{0x02} // prefix for keys that store claim periods
ClaimKeyPrefix = []byte{0x03} // prefix for keys that store claims
NextClaimPeriodIDPrefix = []byte{0x04} // prefix for keys that store the next ID for claims periods
PreviousBlockTimeKey = []byte{0x05} // prefix for key that stores the previous blocktime
)
// Keys
// 0x00:Denom <> RewardPeriod the current active reward period (max 1 reward period per denom)
// 0x01:Denom:ID <> ClaimPeriod object for that ID, indexed by denom and ID
// 0x02:Denom:ID:Owner <> Claim object, indexed by Denom, ID and owner
// 0x03:Denom <> NextClaimPeriodIDPrefix the ID of the next claim period, indexed by denom
// BytesToUint64 returns uint64 format from a byte array
func BytesToUint64(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}
// GetClaimPeriodPrefix returns the key (denom + id) for a claim prefix
func GetClaimPeriodPrefix(denom string, id uint64) []byte {
return createKey([]byte(denom), sdk.Uint64ToBigEndian(id))
}
// GetClaimPrefix returns the key (denom + id + address) for a claim
func GetClaimPrefix(addr sdk.AccAddress, denom string, id uint64) []byte {
return createKey([]byte(denom), sdk.Uint64ToBigEndian(id), addr)
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

54
x/incentive/types/msg.go Normal file
View File

@ -0,0 +1,54 @@
package types
import (
"errors"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgClaimReward{}
// MsgClaimReward message type used to claim rewards
type MsgClaimReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
}
// NewMsgClaimReward returns a new MsgClaimReward.
func NewMsgClaimReward(sender sdk.AccAddress, denom string) MsgClaimReward {
return MsgClaimReward{
Sender: sender,
Denom: denom,
}
}
// 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_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 strings.TrimSpace(msg.Denom) == "" {
return errors.New("invalid (empty) denom")
}
return nil
}
// 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}
}

View File

@ -0,0 +1,59 @@
package types_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
"github.com/tendermint/tendermint/crypto"
)
type msgTest struct {
from sdk.AccAddress
denom string
expectPass bool
}
type MsgTestSuite struct {
suite.Suite
tests []msgTest
}
func (suite *MsgTestSuite) SetupTest() {
tests := []msgTest{
msgTest{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
denom: "bnb",
expectPass: true,
},
msgTest{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
denom: "",
expectPass: false,
},
msgTest{
from: sdk.AccAddress{},
denom: "bnb",
expectPass: false,
},
}
suite.tests = tests
}
func (suite *MsgTestSuite) TestMsgValidation() {
for _, t := range suite.tests {
msg := types.NewMsgClaimReward(t.from, t.denom)
err := msg.ValidateBasic()
if t.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
}
}
}
func TestMsgTestSuite(t *testing.T) {
suite.Run(t, new(MsgTestSuite))
}

165
x/incentive/types/params.go Normal file
View File

@ -0,0 +1,165 @@
package types
import (
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/params"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
kavadistTypes "github.com/kava-labs/kava/x/kavadist/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Parameter keys and default values
var (
KeyActive = []byte("Active")
KeyRewards = []byte("Rewards")
DefaultActive = false
DefaultRewards = Rewards{}
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
GovDenom = cdptypes.DefaultGovDenom
PrincipalDenom = "usdx"
IncentiveMacc = kavadistTypes.ModuleName
)
// Params governance parameters for the incentive module
type Params struct {
Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards
Rewards Rewards `json:"rewards" yaml:"rewards"`
}
// NewParams returns a new params object
func NewParams(active bool, rewards Rewards) Params {
return Params{
Active: active,
Rewards: rewards,
}
}
// DefaultParams returns default params for kavadist module
func DefaultParams() Params {
return NewParams(DefaultActive, DefaultRewards)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Active: %t
Rewards: %s`, p.Active, p.Rewards)
}
// 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(KeyRewards, &p.Rewards, validateRewardsParam),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateActiveParam(p.Active); err != nil {
return err
}
if err := validateRewardsParam(p.Rewards); err != nil {
return err
}
return nil
}
func validateActiveParam(i interface{}) error {
_, ok := i.(bool)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return nil
}
func validateRewardsParam(i interface{}) error {
rewards, ok := i.(Rewards)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
rewardDenoms := make(map[string]bool)
for _, reward := range rewards {
if strings.TrimSpace(reward.Denom) == "" {
return fmt.Errorf("cannot have empty reward denom: %s", reward)
}
if rewardDenoms[reward.Denom] {
return fmt.Errorf("cannot have duplicate reward denoms: %s", reward.Denom)
}
rewardDenoms[reward.Denom] = true
if !reward.AvailableRewards.IsValid() {
return fmt.Errorf("invalid reward coins %s for %s", reward.AvailableRewards, reward.Denom)
}
if !reward.AvailableRewards.IsPositive() {
return fmt.Errorf("reward amount must be positive, is %s for %s", reward.AvailableRewards, reward.Denom)
}
if int(reward.Duration.Seconds()) <= 0 {
return fmt.Errorf("reward duration must be positive, is %s for %s", reward.Duration.String(), reward.Denom)
}
if int(reward.TimeLock.Seconds()) < 0 {
return fmt.Errorf("reward timelock must be non-negative, is %s for %s", reward.TimeLock.String(), reward.Denom)
}
if int(reward.ClaimDuration.Seconds()) <= 0 {
return fmt.Errorf("reward timelock must be positive, is %s for %s", reward.ClaimDuration.String(), reward.Denom)
}
}
return nil
}
// Reward stores the specified state for a single reward period.
type Reward struct {
Active bool `json:"active" yaml:"active"` // governance switch to disable a period
Denom string `json:"denom" yaml:"denom"` // the collateral denom rewards apply to, must be found in the cdp collaterals
AvailableRewards sdk.Coin `json:"available_rewards" yaml:"available_rewards"` // the total amount of coins distributed per period
Duration time.Duration `json:"duration" yaml:"duration"` // the duration of the period
TimeLock time.Duration `json:"time_lock" yaml:"time_lock"` // how long rewards for this period are timelocked
ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards
}
// NewReward returns a new Reward
func NewReward(active bool, denom string, reward sdk.Coin, duration time.Duration, timelock time.Duration, claimDuration time.Duration) Reward {
return Reward{
Active: active,
Denom: denom,
AvailableRewards: reward,
Duration: duration,
TimeLock: timelock,
ClaimDuration: claimDuration,
}
}
// String implements fmt.Stringer
func (r Reward) String() string {
return fmt.Sprintf(`Reward:
Active: %t,
Denom: %s,
Available Rewards: %s,
Duration: %s,
Time Lock: %s,
Claim Duration: %s`,
r.Active, r.Denom, r.AvailableRewards, r.Duration, r.TimeLock, r.ClaimDuration)
}
// Rewards array of Reward
type Rewards []Reward
// String implements fmt.Stringer
func (rs Rewards) String() string {
out := "Rewards\n"
for _, r := range rs {
out += fmt.Sprintf("%s\n", r)
}
return out
}

View File

@ -0,0 +1,152 @@
package types_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/stretchr/testify/suite"
)
type paramTest struct {
params types.Params
expectPass bool
}
type ParamTestSuite struct {
suite.Suite
tests []paramTest
}
func (suite *ParamTestSuite) SetupTest() {
p1 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
}
p2 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
}
p3 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * -24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
}
p4 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * -8766,
ClaimDuration: time.Hour * 24 * 14,
},
},
}
p5 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 0,
},
},
}
p6 := types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
Denom: "bnb",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
Duration: time.Hour * 24 * 7,
TimeLock: time.Hour * 8766,
ClaimDuration: time.Hour * 0,
},
},
}
suite.tests = []paramTest{
paramTest{
params: p1,
expectPass: true,
},
paramTest{
params: p2,
expectPass: false,
},
paramTest{
params: p3,
expectPass: false,
},
paramTest{
params: p4,
expectPass: false,
},
paramTest{
params: p5,
expectPass: false,
},
paramTest{
params: p6,
expectPass: false,
},
}
}
func (suite *ParamTestSuite) TestParamValidation() {
for _, t := range suite.tests {
err := t.params.Validate()
if t.expectPass {
suite.NoError(err)
} else {
suite.Error(err)
}
}
}
func TestParamTestSuite(t *testing.T) {
suite.Run(t, new(ParamTestSuite))
}

View File

@ -0,0 +1,11 @@
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}
}

View File

@ -0,0 +1,35 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
)
// Querier routes for the incentive module
const (
QueryGetClaims = "claims"
RestClaimOwner = "owner"
RestClaimDenom = "denom"
QueryGetParams = "parameters"
)
// QueryClaimsParams params for query /incentive/claims
type QueryClaimsParams struct {
Owner sdk.AccAddress
Denom string
}
// NewQueryClaimsParams returns QueryClaimsParams
func NewQueryClaimsParams(owner sdk.AccAddress, denom string) QueryClaimsParams {
return QueryClaimsParams{
Owner: owner,
Denom: denom,
}
}
// PostClaimReq defines the properties of claim transaction's request body.
type PostClaimReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Denom string `json:"denom" yaml:"denom"`
}

View File

@ -0,0 +1,97 @@
package types
import (
"fmt"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// RewardPeriod stores the state of an ongoing reward
type RewardPeriod struct {
Denom string `json:"denom" yaml:"denom"`
Start time.Time `json:"start" yaml:"start"`
End time.Time `json:"end" yaml:"end"`
Reward sdk.Coin `json:"reward" yaml:"reward"` // per second reward payouts
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
ClaimTimeLock time.Duration `json:"claim_time_lock" yaml:"claim_time_lock"` // the amount of time rewards are timelocked once they are sent to users
}
// String implements fmt.Stringer
func (rp RewardPeriod) String() string {
return fmt.Sprintf(`Reward Period:
Denom: %s,
Start: %s,
End: %s,
Reward: %s,
Claim End: %s,
Claim Time Lock: %s`,
rp.Denom, rp.Start, rp.End, rp.Reward, rp.ClaimEnd, rp.ClaimTimeLock)
}
// NewRewardPeriod returns a new RewardPeriod
func NewRewardPeriod(denom string, start time.Time, end time.Time, reward sdk.Coin, claimEnd time.Time, claimTimeLock time.Duration) RewardPeriod {
return RewardPeriod{
Denom: denom,
Start: start,
End: end,
Reward: reward,
ClaimEnd: claimEnd,
ClaimTimeLock: claimTimeLock,
}
}
// RewardPeriods array of RewardPeriod
type RewardPeriods []RewardPeriod
// ClaimPeriod stores the state of an ongoing claim period
type ClaimPeriod struct {
Denom string `json:"denom" yaml:"denom"`
ID uint64 `json:"id" yaml:"id"`
End time.Time `json:"end" yaml:"end"`
TimeLock time.Duration `json:"time_lock" yaml:"time_lock"`
}
// NewClaimPeriod returns a new ClaimPeriod
func NewClaimPeriod(denom string, id uint64, end time.Time, timeLock time.Duration) ClaimPeriod {
return ClaimPeriod{
Denom: denom,
ID: id,
End: end,
TimeLock: timeLock,
}
}
// ClaimPeriods array of ClaimPeriod
type ClaimPeriods []ClaimPeriod
// Claim stores the rewards that can be claimed by owner
type Claim struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Reward sdk.Coin `json:"reward" yaml:"reward"`
Denom string `json:"denom" yaml:"denom"`
ClaimPeriodID uint64 `json:"claim_period_id" yaml:"claim_period_id"`
}
// NewClaim returns a new Claim
func NewClaim(owner sdk.AccAddress, reward sdk.Coin, denom string, claimPeriodID uint64) Claim {
return Claim{
Owner: owner,
Reward: reward,
Denom: denom,
ClaimPeriodID: claimPeriodID,
}
}
// String implements fmt.Stringer
func (c Claim) String() string {
return fmt.Sprintf(`Claim:
Owner: %s,
Denom: %s,
Reward: %s,
Claim Period ID: %d,`,
c.Owner, c.Denom, c.Reward, c.ClaimPeriodID)
}
// Claims array of Claim
type Claims []Claim