mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-23 23:05:18 +00:00
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:
parent
5737f4fa19
commit
1ef9bd331b
20
app/app.go
20
app/app.go
@ -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()
|
||||
|
@ -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
1
go.sum
@ -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=
|
||||
|
@ -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
13
x/incentive/abci.go
Normal 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
92
x/incentive/alias.go
Normal 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
|
||||
)
|
98
x/incentive/client/cli/query.go
Normal file
98
x/incentive/client/cli/query.go
Normal 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, ¶ms); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal params: %w", err)
|
||||
}
|
||||
return cliCtx.PrintOutput(params)
|
||||
},
|
||||
}
|
||||
}
|
63
x/incentive/client/cli/tx.go
Normal file
63
x/incentive/client/cli/tx.go
Normal 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})
|
||||
},
|
||||
}
|
||||
}
|
70
x/incentive/client/rest/query.go
Normal file
70
x/incentive/client/rest/query.go
Normal 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)
|
||||
}
|
||||
}
|
13
x/incentive/client/rest/rest.go
Normal file
13
x/incentive/client/rest/rest.go
Normal 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)
|
||||
}
|
36
x/incentive/client/rest/tx.go
Normal file
36
x/incentive/client/rest/tx.go
Normal 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
40
x/incentive/doc.go
Normal 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
73
x/incentive/genesis.go
Normal 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
48
x/incentive/handler.go
Normal 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
|
||||
}
|
243
x/incentive/keeper/keeper.go
Normal file
243
x/incentive/keeper/keeper.go
Normal 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))
|
||||
}
|
18
x/incentive/keeper/params.go
Normal file
18
x/incentive/keeper/params.go
Normal 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, ¶ms)
|
||||
}
|
204
x/incentive/keeper/payout.go
Normal file
204
x/incentive/keeper/payout.go
Normal 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
|
||||
}
|
51
x/incentive/keeper/querier.go
Normal file
51
x/incentive/keeper/querier.go
Normal 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
|
||||
}
|
110
x/incentive/keeper/rewards.go
Normal file
110
x/incentive/keeper/rewards.go
Normal 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
164
x/incentive/module.go
Normal 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{}
|
||||
}
|
12
x/incentive/simulation/decoder.go
Normal file
12
x/incentive/simulation/decoder.go
Normal 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 ""
|
||||
}
|
14
x/incentive/simulation/params.go
Normal file
14
x/incentive/simulation/params.go
Normal 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{}
|
||||
}
|
22
x/incentive/simulation/simulation.go
Normal file
22
x/incentive/simulation/simulation.go
Normal 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)
|
||||
}
|
14
x/incentive/types/account.go
Normal file
14
x/incentive/types/account.go
Normal 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
|
||||
}
|
51
x/incentive/types/account_test.go
Normal file
51
x/incentive/types/account_test.go
Normal 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))
|
||||
}
|
22
x/incentive/types/codec.go
Normal file
22
x/incentive/types/codec.go
Normal 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)
|
||||
}
|
15
x/incentive/types/errors.go
Normal file
15
x/incentive/types/errors.go
Normal 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")
|
||||
)
|
8
x/incentive/types/events.go
Normal file
8
x/incentive/types/events.go
Normal file
@ -0,0 +1,8 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
EventTypeClaim = "claim_reward"
|
||||
|
||||
AttributeValueCategory = ModuleName
|
||||
AttributeKeySender = "sender"
|
||||
)
|
27
x/incentive/types/expected_keepers.go
Normal file
27
x/incentive/types/expected_keepers.go
Normal 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)
|
||||
}
|
75
x/incentive/types/genesis.go
Normal file
75
x/incentive/types/genesis.go
Normal 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
61
x/incentive/types/keys.go
Normal 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
54
x/incentive/types/msg.go
Normal 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}
|
||||
}
|
59
x/incentive/types/msg_test.go
Normal file
59
x/incentive/types/msg_test.go
Normal 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
165
x/incentive/types/params.go
Normal 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
|
||||
}
|
152
x/incentive/types/params_test.go
Normal file
152
x/incentive/types/params_test.go
Normal 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))
|
||||
}
|
11
x/incentive/types/period.go
Normal file
11
x/incentive/types/period.go
Normal 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}
|
||||
}
|
35
x/incentive/types/querier.go
Normal file
35
x/incentive/types/querier.go
Normal 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"`
|
||||
}
|
97
x/incentive/types/rewards.go
Normal file
97
x/incentive/types/rewards.go
Normal 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
|
Loading…
Reference in New Issue
Block a user