Harvest basic borrow functionality (#702)

* basic borrow types

* borrow keeper scaffolding

* borrow limits param

* integrate pricefeed keeper

* msg handling and querier

* borrow user validation

* update migration scripts for compile

* borrows querier, fixes

* add money market param

* add spot market ID to params, refactor pricefeed

* working bnb -> ukava borrows

* refactor to getAssetPrice

* conversion_factor param, refactor validateBorrow()

* address misc revisions

* remove validation code

* add borrow test

* update test params

* single borrow with sdk.Coins per user

* fix harvest test

* removed legacy commented out code

* address minor revisions
This commit is contained in:
Denali Marsh 2020-10-30 10:59:47 +01:00 committed by GitHub
parent 35a82acbd0
commit 1442deb3dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 640 additions and 55 deletions

View File

@ -376,7 +376,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
harvestSubspace, harvestSubspace,
app.accountKeeper, app.accountKeeper,
app.supplyKeeper, app.supplyKeeper,
&stakingKeeper) &stakingKeeper,
app.pricefeedKeeper)
// register the staking hooks // register the staking hooks
// NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks // NOTE: stakingKeeper above is passed by reference, so that it will contain these hooks
@ -407,7 +408,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper), committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper), issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper), harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper, app.pricefeedKeeper),
) )
// During begin block slashing happens after distr.BeginBlocker so that // During begin block slashing happens after distr.BeginBlocker so that
@ -460,7 +461,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool,
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper), incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.supplyKeeper),
committee.NewAppModule(app.committeeKeeper, app.accountKeeper), committee.NewAppModule(app.committeeKeeper, app.accountKeeper),
issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper), issuance.NewAppModule(app.issuanceKeeper, app.accountKeeper, app.supplyKeeper),
harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper), harvest.NewAppModule(app.harvestKeeper, app.supplyKeeper, app.pricefeedKeeper),
) )
app.sm.RegisterStoreDecoders() app.sm.RegisterStoreDecoders()

View File

@ -129,7 +129,7 @@ func MigrateAppState(v0_9AppState v39_genutil.AppMap) v39_genutil.AppMap {
delete(v0_9AppState, v0_9pricefeed.ModuleName) delete(v0_9AppState, v0_9pricefeed.ModuleName)
v0_11AppState[v0_9pricefeed.ModuleName] = v0_11Codec.MustMarshalJSON(MigratePricefeed(pricefeedGenState)) v0_11AppState[v0_9pricefeed.ModuleName] = v0_11Codec.MustMarshalJSON(MigratePricefeed(pricefeedGenState))
} }
v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest()) // v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest())
v0_11AppState[v0_11issuance.ModuleName] = v0_11Codec.MustMarshalJSON(v0_11issuance.DefaultGenesisState()) v0_11AppState[v0_11issuance.ModuleName] = v0_11Codec.MustMarshalJSON(v0_11issuance.DefaultGenesisState())
return v0_11AppState return v0_11AppState
} }
@ -633,29 +633,34 @@ func MigrateGov(oldGenState v39_1gov.GenesisState) v39_1gov.GenesisState {
return oldGenState return oldGenState
} }
// MigrateHarvest initializes the harvest genesis state for kava-4 // // MigrateHarvest initializes the harvest genesis state for kava-4
func MigrateHarvest() v0_11harvest.GenesisState { // func MigrateHarvest() v0_11harvest.GenesisState {
// total HARD per second for lps (week one): 633761 // // total HARD per second for lps (week one): 633761
// HARD per second for delegators (week one): 1267522 // // HARD per second for delegators (week one): 1267522
incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC) // incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC)
incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC) // incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC)
claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC) // claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC)
harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams( // harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams(
true, // true,
v0_11harvest.DistributionSchedules{ // v0_11harvest.DistributionSchedules{
v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), // v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), // v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), // v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), // v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
}, // },
v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule( // v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule(
v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), // v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
time.Hour*24, // time.Hour*24,
), // ),
}, // },
), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes) // v0_11harvest.BlockLimits{
return harvestGS // v0_11harvest.NewBlockLimit("usdx", sdk.Dec(0.9)),
} // v0_11harvest.NewBlockLimit("ukava", sdk.Dec(0.6)),
// v0_11harvest.NewBlockLimit("bnb", sdk.Dec(0.9)),
// },
// ), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes)
// return harvestGS
// }
// MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state // MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state
func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState { func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState {

View File

@ -74,6 +74,7 @@ var (
RegisterCodec = types.RegisterCodec RegisterCodec = types.RegisterCodec
// variable aliases // variable aliases
BorrowsKeyPrefix = types.BorrowsKeyPrefix
ClaimsKeyPrefix = types.ClaimsKeyPrefix ClaimsKeyPrefix = types.ClaimsKeyPrefix
DefaultActive = types.DefaultActive DefaultActive = types.DefaultActive
DefaultDelegatorSchedules = types.DefaultDelegatorSchedules DefaultDelegatorSchedules = types.DefaultDelegatorSchedules
@ -109,7 +110,9 @@ var (
type ( type (
Keeper = keeper.Keeper Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper AccountKeeper = types.AccountKeeper
Claim = types.Claim Borrow = types.Borrow
MoneyMarket = types.MoneyMarket
MoneyMarkets = types.MoneyMarkets
DelegatorDistributionSchedule = types.DelegatorDistributionSchedule DelegatorDistributionSchedule = types.DelegatorDistributionSchedule
DelegatorDistributionSchedules = types.DelegatorDistributionSchedules DelegatorDistributionSchedules = types.DelegatorDistributionSchedules
Deposit = types.Deposit Deposit = types.Deposit

View File

@ -40,6 +40,7 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
queryModAccountsCmd(queryRoute, cdc), queryModAccountsCmd(queryRoute, cdc),
queryDepositsCmd(queryRoute, cdc), queryDepositsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc), queryClaimsCmd(queryRoute, cdc),
queryBorrowsCmd(queryRoute, cdc),
)...) )...)
return harvestQueryCmd return harvestQueryCmd
@ -250,3 +251,60 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd.Flags().String(flagDepositType, "", "(optional) filter for claims by type (lp or staking)") cmd.Flags().String(flagDepositType, "", "(optional) filter for claims by type (lp or staking)")
return cmd return cmd
} }
func queryBorrowsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
cmd := &cobra.Command{
Use: "borrows",
Short: "query harvest module borrows with optional filters",
Long: strings.TrimSpace(`query for all harvest module borrows or a specific borrow using flags:
Example:
$ kvcli q harvest borrows
$ kvcli q harvest borrows --borrower kava1l0xsq2z7gqd7yly0g40y5836g0appumark77ny
$ kvcli q harvest borrows --borrow-denom bnb`,
),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
var owner sdk.AccAddress
ownerBech := viper.GetString(flagOwner)
depositDenom := viper.GetString(flagDepositDenom)
if len(ownerBech) != 0 {
borrowOwner, err := sdk.AccAddressFromBech32(ownerBech)
if err != nil {
return err
}
owner = borrowOwner
}
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
params := types.NewQueryBorrowParams(page, limit, owner, depositDenom)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
route := fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetBorrows)
res, height, err := cliCtx.QueryWithData(route, bz)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
var borrows []types.Borrow
if err := cdc.UnmarshalJSON(res, &borrows); err != nil {
return fmt.Errorf("failed to unmarshal borrows: %w", err)
}
return cliCtx.PrintOutput(borrows)
},
}
cmd.Flags().Int(flags.FlagPage, 1, "pagination page to query for")
cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit (max 100)")
cmd.Flags().String(flagOwner, "", "(optional) filter for borrows by owner address")
return cmd
}

View File

@ -33,6 +33,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
getCmdDeposit(cdc), getCmdDeposit(cdc),
getCmdWithdraw(cdc), getCmdWithdraw(cdc),
getCmdClaimReward(cdc), getCmdClaimReward(cdc),
getCmdBorrow(cdc),
)...) )...)
return harvestTxCmd return harvestTxCmd
@ -116,3 +117,31 @@ func getCmdClaimReward(cdc *codec.Codec) *cobra.Command {
}, },
} }
} }
func getCmdBorrow(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "borrow [1000000000ukava]",
Short: "borrow tokens from the harvest protocol",
Long: strings.TrimSpace(`borrows tokens from the harvest protocol`),
Args: cobra.ExactArgs(1),
Example: fmt.Sprintf(
`%s tx %s borrow 1000000000ukava --from <key>`, version.ClientName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContext().WithCodec(cdc)
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
coins, err := sdk.ParseCoins(args[0])
if err != nil {
return err
}
msg := types.NewMsgBorrow(cliCtx.GetFromAddress(), coins)
if err := msg.ValidateBasic(); err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -21,6 +21,8 @@ func NewHandler(k Keeper) sdk.Handler {
return handleMsgDeposit(ctx, k, msg) return handleMsgDeposit(ctx, k, msg)
case types.MsgWithdraw: case types.MsgWithdraw:
return handleMsgWithdraw(ctx, k, msg) return handleMsgWithdraw(ctx, k, msg)
case types.MsgBorrow:
return handleMsgBorrow(ctx, k, msg)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
} }
@ -80,3 +82,21 @@ func handleMsgWithdraw(ctx sdk.Context, k keeper.Keeper, msg types.MsgWithdraw)
Events: ctx.EventManager().Events(), Events: ctx.EventManager().Events(),
}, nil }, nil
} }
func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sdk.Result, error) {
err := k.Borrow(ctx, msg.Borrower, msg.Amount)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Borrower.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil
}

View File

@ -0,0 +1,34 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/harvest/types"
)
// Borrow funds
func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error {
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, borrower, coins)
if err != nil {
return err
}
borrow, found := k.GetBorrow(ctx, borrower)
if !found {
borrow = types.NewBorrow(borrower, coins)
} else {
borrow.Amount = borrow.Amount.Add(coins...)
}
k.SetBorrow(ctx, borrow)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeHarvestBorrow,
sdk.NewAttribute(types.AttributeKeyBorrower, borrow.Borrower.String()),
sdk.NewAttribute(types.AttributeKeyBorrowCoins, coins.String()),
),
)
return nil
}

View File

@ -0,0 +1,101 @@
package keeper_test
import (
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/harvest/types"
)
func (suite *KeeperTestSuite) TestBorrow() {
type args struct {
borrower sdk.AccAddress
coins sdk.Coins
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
}
type errArgs struct {
expectPass bool
contains string
}
type borrowTest struct {
name string
args args
errArgs errArgs
}
testCases := []borrowTest{
{
"valid",
args{
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
coins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50))),
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(150))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(950))),
},
errArgs{
expectPass: true,
contains: "",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.borrower},
[]sdk.Coins{sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100)))})
loanToValue := sdk.MustNewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams(
true,
types.DistributionSchedules{
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
},
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
time.Hour*24,
),
},
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper()
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000))))
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
// run the test
var err error
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.coins)
// verify results
if tc.errArgs.expectPass {
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.borrower)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
_, f := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower)
suite.Require().True(f)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}

View File

@ -253,6 +253,7 @@ func (suite *KeeperTestSuite) TestClaim() {
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
}) })
loanToValue := sdk.MustNewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams( harvestGS := types.NewGenesisState(types.NewParams(
true, true,
types.DistributionSchedules{ types.DistributionSchedules{
@ -263,6 +264,10 @@ func (suite *KeeperTestSuite) TestClaim() {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
if tc.args.validatorVesting { if tc.args.validatorVesting {

View File

@ -116,6 +116,7 @@ func (suite *KeeperTestSuite) TestDeposit() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
loanToValue, _ := sdk.NewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams( harvestGS := types.NewGenesisState(types.NewParams(
true, true,
types.DistributionSchedules{ types.DistributionSchedules{
@ -126,6 +127,10 @@ func (suite *KeeperTestSuite) TestDeposit() {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper() keeper := tApp.GetHarvestKeeper()
@ -283,6 +288,7 @@ func (suite *KeeperTestSuite) TestWithdraw() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
loanToValue := sdk.MustNewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams( harvestGS := types.NewGenesisState(types.NewParams(
true, true,
types.DistributionSchedules{ types.DistributionSchedules{
@ -293,6 +299,10 @@ func (suite *KeeperTestSuite) TestWithdraw() {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
keeper := tApp.GetHarvestKeeper() keeper := tApp.GetHarvestKeeper()

View File

@ -19,10 +19,13 @@ type Keeper struct {
accountKeeper types.AccountKeeper accountKeeper types.AccountKeeper
supplyKeeper types.SupplyKeeper supplyKeeper types.SupplyKeeper
stakingKeeper types.StakingKeeper stakingKeeper types.StakingKeeper
pricefeedKeeper types.PricefeedKeeper
} }
// NewKeeper creates a new keeper // NewKeeper creates a new keeper
func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace, ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper) Keeper { func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
ak types.AccountKeeper, sk types.SupplyKeeper, stk types.StakingKeeper,
pfk types.PricefeedKeeper) Keeper {
if !paramstore.HasKeyTable() { if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
} }
@ -34,6 +37,7 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
accountKeeper: ak, accountKeeper: ak,
supplyKeeper: sk, supplyKeeper: sk,
stakingKeeper: stk, stakingKeeper: stk,
pricefeedKeeper: pfk,
} }
} }
@ -178,7 +182,58 @@ func (k Keeper) IterateClaimsByTypeAndDenom(ctx sdk.Context, depositType types.D
} }
} }
// GetDepositsByUser gets all deposits for an individual user
func (k Keeper) GetDepositsByUser(ctx sdk.Context, user sdk.AccAddress) []types.Deposit {
var deposits []types.Deposit
k.IterateDeposits(ctx, func(deposit types.Deposit) (stop bool) {
if deposit.Depositor.Equals(user) {
deposits = append(deposits, deposit)
}
return false
})
return deposits
}
// BondDenom returns the bond denom from the staking keeper // BondDenom returns the bond denom from the staking keeper
func (k Keeper) BondDenom(ctx sdk.Context) string { func (k Keeper) BondDenom(ctx sdk.Context) string {
return k.stakingKeeper.BondDenom(ctx) return k.stakingKeeper.BondDenom(ctx)
} }
// GetBorrow returns a Borrow from the store for a particular borrower address and borrow denom
func (k Keeper) GetBorrow(ctx sdk.Context, borrower sdk.AccAddress) (types.Borrow, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix)
bz := store.Get(borrower)
if bz == nil {
return types.Borrow{}, false
}
var borrow types.Borrow
k.cdc.MustUnmarshalBinaryBare(bz, &borrow)
return borrow, true
}
// SetBorrow sets the input borrow in the store, prefixed by the borrower address and borrow denom
func (k Keeper) SetBorrow(ctx sdk.Context, borrow types.Borrow) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(borrow)
store.Set(borrow.Borrower, bz)
}
// DeleteBorrow deletes a borrow from the store
func (k Keeper) DeleteBorrow(ctx sdk.Context, borrow types.Borrow) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix)
store.Delete(borrow.Borrower)
}
// IterateBorrows iterates over all borrow objects in the store and performs a callback function
func (k Keeper) IterateBorrows(ctx sdk.Context, cb func(borrow types.Borrow) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowsKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var borrow types.Borrow
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &borrow)
if cb(borrow) {
break
}
}
}

View File

@ -18,6 +18,7 @@ func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params) k.paramSubspace.SetParamSet(ctx, &params)
} }
// GetLPSchedule gets the LP's schedule
func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.DistributionSchedule, bool) { func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.DistributionSchedule, bool) {
params := k.GetParams(ctx) params := k.GetParams(ctx)
for _, lps := range params.LiquidityProviderSchedules { for _, lps := range params.LiquidityProviderSchedules {
@ -28,6 +29,7 @@ func (k Keeper) GetLPSchedule(ctx sdk.Context, denom string) (types.Distribution
return types.DistributionSchedule{}, false return types.DistributionSchedule{}, false
} }
// GetDelegatorSchedule gets the Delgator's schedule
func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.DelegatorDistributionSchedule, bool) { func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.DelegatorDistributionSchedule, bool) {
params := k.GetParams(ctx) params := k.GetParams(ctx)
for _, dds := range params.DelegatorDistributionSchedules { for _, dds := range params.DelegatorDistributionSchedules {
@ -37,3 +39,14 @@ func (k Keeper) GetDelegatorSchedule(ctx sdk.Context, denom string) (types.Deleg
} }
return types.DelegatorDistributionSchedule{}, false return types.DelegatorDistributionSchedule{}, false
} }
// GetMoneyMarket returns the corresponding Money Market param for a specific denom
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
params := k.GetParams(ctx)
for _, mm := range params.MoneyMarkets {
if mm.Denom == denom {
return mm, true
}
}
return types.MoneyMarket{}, false
}

View File

@ -24,6 +24,8 @@ func NewQuerier(k Keeper) sdk.Querier {
return queryGetDeposits(ctx, req, k) return queryGetDeposits(ctx, req, k)
case types.QueryGetClaims: case types.QueryGetClaims:
return queryGetClaims(ctx, req, k) return queryGetClaims(ctx, req, k)
case types.QueryGetBorrows:
return queryGetBorrows(ctx, req, k)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
} }
@ -260,3 +262,36 @@ func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e
return bz, nil return bz, nil
} }
func queryGetBorrows(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var params types.QueryBorrowParams
err := types.ModuleCdc.UnmarshalJSON(req.Data, &params)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error())
}
// TODO: filter query results
// depositDenom := len(params.BorrowDenom) > 0
// owner := len(params.Owner) > 0
var borrows []types.Borrow
k.IterateBorrows(ctx, func(borrow types.Borrow) (stop bool) {
borrows = append(borrows, borrow)
return false
})
start, end := client.Paginate(len(borrows), params.Page, params.Limit, 100)
if start < 0 || end < 0 {
borrows = []types.Borrow{}
} else {
borrows = borrows[start:end]
}
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, borrows)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}

View File

@ -63,6 +63,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
// Initialize test app and set context // Initialize test app and set context
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
loanToValue, _ := sdk.NewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams( harvestGS := types.NewGenesisState(types.NewParams(
true, true,
types.DistributionSchedules{ types.DistributionSchedules{
@ -73,6 +74,10 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), tc.args.previousBlockTime, types.DefaultDistributionTimes) ), tc.args.previousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
supplyKeeper := tApp.GetSupplyKeeper() supplyKeeper := tApp.GetSupplyKeeper()
@ -400,6 +405,7 @@ func (suite *DelegatorRewardsTestSuite) kavaClaimExists(ctx sdk.Context, owner s
} }
func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState { func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState {
loanToValue := sdk.MustNewDecFromStr("0.6")
genState := types.NewGenesisState( genState := types.NewGenesisState(
types.NewParams( types.NewParams(
true, true,
@ -436,6 +442,10 @@ func harvestGenesisState(rewardRate sdk.Coin) app.GenesisState {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), ),
types.DefaultPreviousBlockTime, types.DefaultPreviousBlockTime,
types.DefaultDistributionTimes, types.DefaultDistributionTimes,

View File

@ -279,6 +279,7 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins}) authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.accArgs.addr}, []sdk.Coins{tc.args.accArgs.coins})
loanToValue := sdk.MustNewDecFromStr("0.6")
harvestGS := types.NewGenesisState(types.NewParams( harvestGS := types.NewGenesisState(types.NewParams(
true, true,
types.DistributionSchedules{ types.DistributionSchedules{
@ -289,6 +290,10 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
time.Hour*24, time.Hour*24,
), ),
}, },
types.MoneyMarkets{
types.NewMoneyMarket("usdx", sdk.NewInt(1000000000000000), loanToValue, "usdx:usd", sdk.NewInt(1000000)),
types.NewMoneyMarket("ukava", sdk.NewInt(1000000000000000), loanToValue, "kava:usd", sdk.NewInt(1000000)),
},
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes) ), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)}) tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
if tc.args.accArgs.vestingAccountBefore { if tc.args.accArgs.vestingAccountBefore {

View File

@ -79,14 +79,16 @@ type AppModule struct {
keeper Keeper keeper Keeper
supplyKeeper types.SupplyKeeper supplyKeeper types.SupplyKeeper
pricefeedKeeper types.PricefeedKeeper
} }
// NewAppModule creates a new AppModule object // NewAppModule creates a new AppModule object
func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper) AppModule { func NewAppModule(keeper Keeper, supplyKeeper types.SupplyKeeper, pricefeedKeeper types.PricefeedKeeper) AppModule {
return AppModule{ return AppModule{
AppModuleBasic: AppModuleBasic{}, AppModuleBasic: AppModuleBasic{},
keeper: keeper, keeper: keeper,
supplyKeeper: supplyKeeper, supplyKeeper: supplyKeeper,
pricefeedKeeper: pricefeedKeeper,
} }
} }

19
x/harvest/types/borrow.go Normal file
View File

@ -0,0 +1,19 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
)
// Borrow defines an amount of coins borrowed from a harvest module account
type Borrow struct {
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
}
// NewBorrow returns a new Borrow instance
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins) Borrow {
return Borrow{
Borrower: borrower,
Amount: amount,
}
}

View File

@ -17,5 +17,6 @@ func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgClaimReward{}, "harvest/MsgClaimReward", nil) cdc.RegisterConcrete(MsgClaimReward{}, "harvest/MsgClaimReward", nil)
cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil) cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil)
cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil) cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil)
cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil)
cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil) cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil)
} }

View File

@ -35,4 +35,14 @@ var (
ErrClaimExpired = sdkerrors.Register(ModuleName, 14, "claim period expired") ErrClaimExpired = sdkerrors.Register(ModuleName, 14, "claim period expired")
// ErrInvalidReceiver error for when sending and receiving accounts don't match // ErrInvalidReceiver error for when sending and receiving accounts don't match
ErrInvalidReceiver = sdkerrors.Register(ModuleName, 15, "receiver account must match sender account") ErrInvalidReceiver = sdkerrors.Register(ModuleName, 15, "receiver account must match sender account")
// ErrMoneyMarketNotFound error for money market param not found
ErrMoneyMarketNotFound = sdkerrors.Register(ModuleName, 16, "no money market found")
// ErrDepositsNotFound error for no deposits found
ErrDepositsNotFound = sdkerrors.Register(ModuleName, 17, "no deposits found")
// ErrInsufficientLoanToValue error for when an attempted borrow exceeds maximum loan-to-value
ErrInsufficientLoanToValue = sdkerrors.Register(ModuleName, 18, "total deposited value is insufficient for borrow request")
// ErrMarketNotFound error for when a market for the input denom is not found
ErrMarketNotFound = sdkerrors.Register(ModuleName, 19, "no market found for denom")
// ErrPriceNotFound error for when a price for the input market is not found
ErrPriceNotFound = sdkerrors.Register(ModuleName, 20, "no price found for market")
) )

View File

@ -8,6 +8,7 @@ const (
EventTypeDeleteHarvestDeposit = "delete_harvest_deposit" EventTypeDeleteHarvestDeposit = "delete_harvest_deposit"
EventTypeHarvestWithdrawal = "harvest_withdrawal" EventTypeHarvestWithdrawal = "harvest_withdrawal"
EventTypeClaimHarvestReward = "claim_harvest_reward" EventTypeClaimHarvestReward = "claim_harvest_reward"
EventTypeHarvestBorrow = "harvest_borrow"
AttributeValueCategory = ModuleName AttributeValueCategory = ModuleName
AttributeKeyBlockHeight = "block_height" AttributeKeyBlockHeight = "block_height"
AttributeKeyRewardsDistribution = "rewards_distributed" AttributeKeyRewardsDistribution = "rewards_distributed"
@ -18,4 +19,7 @@ const (
AttributeKeyClaimHolder = "claim_holder" AttributeKeyClaimHolder = "claim_holder"
AttributeKeyClaimAmount = "claim_amount" AttributeKeyClaimAmount = "claim_amount"
AttributeKeyClaimMultiplier = "claim_multiplier" AttributeKeyClaimMultiplier = "claim_multiplier"
AttributeKeyBorrow = "borrow"
AttributeKeyBorrower = "borrower"
AttributeKeyBorrowCoins = "borrow_coins"
) )

View File

@ -6,6 +6,8 @@ import (
stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported" stakingexported "github.com/cosmos/cosmos-sdk/x/staking/exported"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/cosmos/cosmos-sdk/x/supply/exported" "github.com/cosmos/cosmos-sdk/x/supply/exported"
pftypes "github.com/kava-labs/kava/x/pricefeed/types"
) )
// SupplyKeeper defines the expected supply keeper // SupplyKeeper defines the expected supply keeper
@ -33,3 +35,8 @@ type StakingKeeper interface {
GetBondedPool(ctx sdk.Context) (bondedPool exported.ModuleAccountI) GetBondedPool(ctx sdk.Context) (bondedPool exported.ModuleAccountI)
BondDenom(ctx sdk.Context) (res string) BondDenom(ctx sdk.Context) (res string)
} }
// PricefeedKeeper defines the expected interface for the pricefeed
type PricefeedKeeper interface {
GetCurrentPrice(sdk.Context, string) (pftypes.CurrentPrice, error)
}

View File

@ -35,6 +35,7 @@ var (
PreviousDelegationDistributionKey = []byte{0x02} PreviousDelegationDistributionKey = []byte{0x02}
DepositsKeyPrefix = []byte{0x03} DepositsKeyPrefix = []byte{0x03}
ClaimsKeyPrefix = []byte{0x04} ClaimsKeyPrefix = []byte{0x04}
BorrowsKeyPrefix = []byte{0x05}
sep = []byte(":") sep = []byte(":")
) )

View File

@ -53,6 +53,7 @@ var (
_ sdk.Msg = &MsgClaimReward{} _ sdk.Msg = &MsgClaimReward{}
_ sdk.Msg = &MsgDeposit{} _ sdk.Msg = &MsgDeposit{}
_ sdk.Msg = &MsgWithdraw{} _ sdk.Msg = &MsgWithdraw{}
_ sdk.Msg = &MsgBorrow{}
) )
// MsgDeposit deposit collateral to the harvest module. // MsgDeposit deposit collateral to the harvest module.
@ -214,3 +215,55 @@ func (msg MsgClaimReward) GetSignBytes() []byte {
func (msg MsgClaimReward) GetSigners() []sdk.AccAddress { func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender} return []sdk.AccAddress{msg.Sender}
} }
// ---------------------------------------
// MsgBorrow borrows funds from the harvest module.
type MsgBorrow struct {
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
}
// NewMsgBorrow returns a new MsgBorrow
func NewMsgBorrow(borrower sdk.AccAddress, amount sdk.Coins) MsgBorrow {
return MsgBorrow{
Borrower: borrower,
Amount: amount,
}
}
// Route return the message type used for routing the message.
func (msg MsgBorrow) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgBorrow) Type() string { return "harvest_borrow" } // TODO: or just 'borrow'
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgBorrow) ValidateBasic() error {
if msg.Borrower.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "borrow amount %s", msg.Amount)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgBorrow) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgBorrow) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Borrower}
}
// String implements the Stringer interface
func (msg MsgBorrow) String() string {
return fmt.Sprintf(`Borrow Message:
Borrower: %s
Amount: %s
`, msg.Borrower, msg.Amount)
}

View File

@ -16,10 +16,12 @@ var (
KeyActive = []byte("Active") KeyActive = []byte("Active")
KeyLPSchedules = []byte("LPSchedules") KeyLPSchedules = []byte("LPSchedules")
KeyDelegatorSchedule = []byte("DelegatorSchedule") KeyDelegatorSchedule = []byte("DelegatorSchedule")
KeyMoneyMarkets = []byte("MoneyMarkets")
DefaultActive = true DefaultActive = true
DefaultGovSchedules = DistributionSchedules{} DefaultGovSchedules = DistributionSchedules{}
DefaultLPSchedules = DistributionSchedules{} DefaultLPSchedules = DistributionSchedules{}
DefaultDelegatorSchedules = DelegatorDistributionSchedules{} DefaultDelegatorSchedules = DelegatorDistributionSchedules{}
DefaultMoneyMarkets = MoneyMarkets{}
GovDenom = cdptypes.DefaultGovDenom GovDenom = cdptypes.DefaultGovDenom
) )
@ -28,6 +30,7 @@ type Params struct {
Active bool `json:"active" yaml:"active"` Active bool `json:"active" yaml:"active"`
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"` LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"` DelegatorDistributionSchedules DelegatorDistributionSchedules `json:"delegator_distribution_schedules" yaml:"delegator_distribution_schedules"`
MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"`
} }
// DistributionSchedule distribution schedule for liquidity providers // DistributionSchedule distribution schedule for liquidity providers
@ -218,18 +221,91 @@ func (ds DistributionSchedule) GetMultiplier(name MultiplierName) (Multiplier, b
// Multipliers slice of Multiplier // Multipliers slice of Multiplier
type Multipliers []Multiplier type Multipliers []Multiplier
// BorrowLimit enforces restrictions on a money market
type BorrowLimit struct {
MaximumLimit sdk.Int `json:"maximum_limit" yaml:"maximum_limit"`
LoanToValue sdk.Dec `json:"loan_to_value" yaml:"loan_to_value"`
}
// NewBorrowLimit returns a new BorrowLimit
func NewBorrowLimit(maximumLimit sdk.Int, loanToValue sdk.Dec) BorrowLimit {
return BorrowLimit{
MaximumLimit: maximumLimit,
LoanToValue: loanToValue,
}
}
// Validate BorrowLimit
func (bl BorrowLimit) Validate() error {
if bl.MaximumLimit.IsNegative() {
return fmt.Errorf("maximum limit cannot be negative: %s", bl.MaximumLimit)
}
if !bl.LoanToValue.IsPositive() {
return fmt.Errorf("loan-to-value must be a positive integer: %s", bl.LoanToValue)
}
if bl.LoanToValue.GT(sdk.OneDec()) {
return fmt.Errorf("loan-to-value cannot be greater than 1.0: %s", bl.LoanToValue)
}
return nil
}
// MoneyMarket is a money market for an individual asset
type MoneyMarket struct {
Denom string `json:"denom" yaml:"denom"`
BorrowLimit BorrowLimit `json:"borrow_limit" yaml:"borrow_limit"`
SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
}
// NewMoneyMarket returns a new MoneyMarket
func NewMoneyMarket(denom string, maximumLimit sdk.Int, loanToValue sdk.Dec,
spotMarketID string, conversionFactor sdk.Int) MoneyMarket {
return MoneyMarket{
Denom: denom,
BorrowLimit: NewBorrowLimit(maximumLimit, loanToValue),
SpotMarketID: spotMarketID,
ConversionFactor: conversionFactor,
}
}
// Validate MoneyMarket param
func (mm MoneyMarket) Validate() error {
if err := sdk.ValidateDenom(mm.Denom); err != nil {
return err
}
if err := mm.BorrowLimit.Validate(); err != nil {
return err
}
return nil
}
// MoneyMarkets slice of MoneyMarket
type MoneyMarkets []MoneyMarket
// Validate borrow limits
func (mms MoneyMarkets) Validate() error {
for _, moneyMarket := range mms {
if err := moneyMarket.Validate(); err != nil {
return err
}
}
return nil
}
// NewParams returns a new params object // NewParams returns a new params object
func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules) Params { func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistributionSchedules, moneyMarkets MoneyMarkets) Params {
return Params{ return Params{
Active: active, Active: active,
LiquidityProviderSchedules: lps, LiquidityProviderSchedules: lps,
DelegatorDistributionSchedules: dds, DelegatorDistributionSchedules: dds,
MoneyMarkets: moneyMarkets,
} }
} }
// DefaultParams returns default params for harvest module // DefaultParams returns default params for harvest module
func DefaultParams() Params { func DefaultParams() Params {
return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules) return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules, DefaultMoneyMarkets)
} }
// String implements fmt.Stringer // String implements fmt.Stringer
@ -237,7 +313,8 @@ func (p Params) String() string {
return fmt.Sprintf(`Params: return fmt.Sprintf(`Params:
Active: %t Active: %t
Liquidity Provider Distribution Schedules %s Liquidity Provider Distribution Schedules %s
Delegator Distribution Schedule %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules) Delegator Distribution Schedule %s
Money Markets %s`, p.Active, p.LiquidityProviderSchedules, p.DelegatorDistributionSchedules, p.MoneyMarkets)
} }
// ParamKeyTable Key declaration for parameters // ParamKeyTable Key declaration for parameters
@ -251,6 +328,7 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs {
params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam), params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam),
params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams), params.NewParamSetPair(KeyLPSchedules, &p.LiquidityProviderSchedules, validateLPParams),
params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams), params.NewParamSetPair(KeyDelegatorSchedule, &p.DelegatorDistributionSchedules, validateDelegatorParams),
params.NewParamSetPair(KeyMoneyMarkets, &p.MoneyMarkets, validateMoneyMarketParams),
} }
} }
@ -300,3 +378,12 @@ func validateDelegatorParams(i interface{}) error {
return dds.Validate() return dds.Validate()
} }
func validateMoneyMarketParams(i interface{}) error {
mm, ok := i.(MoneyMarkets)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return mm.Validate()
}

View File

@ -10,6 +10,7 @@ const (
QueryGetModuleAccounts = "accounts" QueryGetModuleAccounts = "accounts"
QueryGetDeposits = "deposits" QueryGetDeposits = "deposits"
QueryGetClaims = "claims" QueryGetClaims = "claims"
QueryGetBorrows = "borrows"
) )
// QueryDepositParams is the params for a filtered deposit query // QueryDepositParams is the params for a filtered deposit query
@ -67,3 +68,21 @@ func NewQueryAccountParams(page, limit int, name string) QueryAccountParams {
Name: name, Name: name,
} }
} }
// QueryBorrowParams is the params for a filtered borrow query
type QueryBorrowParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
BorrowDenom string `json:"borrow_denom" yaml:"borrow_denom"`
}
// NewQueryBorrowParams creates a new QueryBorrowParams
func NewQueryBorrowParams(page, limit int, owner sdk.AccAddress, depositDenom string) QueryBorrowParams {
return QueryBorrowParams{
Page: page,
Limit: limit,
Owner: owner,
BorrowDenom: depositDenom,
}
}

View File

@ -58,5 +58,4 @@ func (k Keeper) GetMarket(ctx sdk.Context, marketID string) (types.Market, bool)
} }
} }
return types.Market{}, false return types.Market{}, false
} }

View File

@ -11,7 +11,6 @@ import (
// Market an asset in the pricefeed // Market an asset in the pricefeed
type Market struct { type Market struct {
// TODO: rename to ID
MarketID string `json:"market_id" yaml:"market_id"` MarketID string `json:"market_id" yaml:"market_id"`
BaseAsset string `json:"base_asset" yaml:"base_asset"` BaseAsset string `json:"base_asset" yaml:"base_asset"`
QuoteAsset string `json:"quote_asset" yaml:"quote_asset"` QuoteAsset string `json:"quote_asset" yaml:"quote_asset"`