Cdp accumulators (#751)

* Add 'InterestFactor' to CDP type (#734)

* update cdp type to include interest factor

* fix build

* Add cdp accumulator methods (#735)

* remame fees to interest

* add accumulate interest method

* add basic test

* add note

* address review comments

* update tests

* Add sync cdp interest method (#737)

* remame fees to interest

* add accumulate interest method

* add basic test

* add note

* address review comments

* update tests

* remove old fee functions

* add method to synchronize cdp interest

* add multi-cdp tests

* add test with many blocks

* add test for interest getter

* address review comments

* calculate time difference then convert to seconds

* fix: update collateral index when syncing interest

* fix: differentiate between case when apy is zero and all fees are being rounded to zero

* fix: round time difference properly

* update cdp genesis state and migrations (#738)

* remame fees to interest

* add accumulate interest method

* add basic test

* add note

* address review comments

* update tests

* remove old fee functions

* add method to synchronize cdp interest

* add multi-cdp tests

* add test with many blocks

* add test for interest getter

* update cdp genesis state and migrations

* address review comments

* calculate time difference then convert to seconds

* fix: update collateral index when syncing interest

* fix: differentiate between case when apy is zero and all fees are being rounded to zero

* fix: simplify add/remove/update collateral index

* update genesis state to include total principal amounts

* update migration

* Delete kava-4-cdp-state-block-500000.json

* Add cdp liquidations by external keeper (#750)

* feat: split liquidations between external keepers and automated begin blocker

* address review comments

* USDX incentive accumulators (#752)

* feat: split liquidations between external keepers and automated begin blocker

* wip: refactor usdx minting incentives to use accumulators/hooks

* wip: refactor usdx minting claim object

* feat: use accumulators/hooks for usdx minting rewards

* fix: get tests passing

* fix: don't create claim objects unless that cdp type is eligable for rewards

* add begin blocker

* update client

* cleanup comments/tests

* update querier

* address review comments

* fix: check for division by zero

* address review comments

* run hook before interest is synced

* Remove savings rate (#764)

* remove savings rate

* remove savings rate from debt param

* update migrations

* address review comments

* Add usdx incentives calculation test (#765)

* add usdx incentive calculation test

* update reward calculation

* add allowable error to test criteria

* Update x/incentive/keeper/rewards_test.go

Co-authored-by: Kevin Davis <karzak@users.noreply.github.com>

* fix: remove old fields from test genesis state

Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com>

Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com>
This commit is contained in:
Kevin Davis 2021-01-18 12:12:37 -07:00 committed by GitHub
parent fba6b8c4f2
commit c63ecf908a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
102 changed files with 34415 additions and 4344 deletions

View File

@ -99,7 +99,6 @@ var (
auction.ModuleName: nil,
cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
cdp.SavingsRateMacc: {supply.Minter},
bep3.ModuleName: {supply.Minter, supply.Burner},
kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner},
@ -341,7 +340,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
app.supplyKeeper,
auctionSubspace,
)
app.cdpKeeper = cdp.NewKeeper(
cdpKeeper := cdp.NewKeeper(
app.cdc,
keys[cdp.StoreKey],
cdpSubspace,
@ -370,7 +369,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
keys[incentive.StoreKey],
incentiveSubspace,
app.supplyKeeper,
app.cdpKeeper,
&cdpKeeper,
app.accountKeeper,
)
app.issuanceKeeper = issuance.NewKeeper(
@ -396,6 +395,8 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
app.stakingKeeper = *stakingKeeper.SetHooks(
staking.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks()))
app.cdpKeeper = *cdpKeeper.SetHooks(cdp.NewMultiCDPHooks(app.incentiveKeeper.Hooks()))
// create the module manager (Note: Any module instantiated in the module manager that is later modified
// must be passed by reference here.)
app.mm = module.NewManager(

View File

@ -22,12 +22,12 @@ import (
v38_5supply "github.com/kava-labs/kava/migrate/v0_11/legacy/cosmos-sdk/v0.38.5/supply"
v0_11bep3 "github.com/kava-labs/kava/x/bep3"
v0_9bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_9"
v0_11cdp "github.com/kava-labs/kava/x/cdp"
v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
v0_9cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_9"
v0_11committee "github.com/kava-labs/kava/x/committee"
v0_9committee "github.com/kava-labs/kava/x/committee/legacy/v0_9"
v0_11harvest "github.com/kava-labs/kava/x/hard"
v0_11incentive "github.com/kava-labs/kava/x/incentive"
v0_11harvest "github.com/kava-labs/kava/x/hard/legacy/v0_11"
v0_11incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_11"
v0_9incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_9"
v0_11issuance "github.com/kava-labs/kava/x/issuance"
v0_11pricefeed "github.com/kava-labs/kava/x/pricefeed"
@ -129,7 +129,7 @@ func MigrateAppState(v0_9AppState v39_genutil.AppMap) v39_genutil.AppMap {
delete(v0_9AppState, v0_9pricefeed.ModuleName)
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())
return v0_11AppState
}
@ -308,7 +308,6 @@ func MigrateCommittee(oldGenState v0_9committee.GenesisState) v0_11committee.Gen
DebtFloor: oldDebtParam.DebtFloor,
Denom: oldDebtParam.Denom,
ReferenceAsset: oldDebtParam.ReferenceAsset,
SavingsRate: oldDebtParam.SavingsRate,
}
oldAssetParam := subPermission.AllowedAssetParams[0]
newAssetParam := v0_11committee.AllowedAssetParam{
@ -633,34 +632,29 @@ func MigrateGov(oldGenState v39_1gov.GenesisState) v39_1gov.GenesisState {
return oldGenState
}
// // MigrateHarvest initializes the harvest genesis state for kava-4
// func MigrateHarvest() v0_11harvest.GenesisState {
// // total HARD per second for lps (week one): 633761
// // HARD per second for delegators (week one): 1267522
// incentiveGoLiveDate := time.Date(2020, 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)
// harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams(
// true,
// 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, "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, "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.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,
// ),
// },
// v0_11harvest.BlockLimits{
// 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
// }
// MigrateHarvest initializes the harvest genesis state for kava-4
func MigrateHarvest() v0_11harvest.GenesisState {
// total HARD per second for lps (week one): 633761
// HARD per second for delegators (week one): 1267522
incentiveGoLiveDate := time.Date(2020, 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)
harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams(
true,
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, "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, "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.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,
),
},
), 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
func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState {

View File

@ -12,11 +12,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
v39_1auth "github.com/cosmos/cosmos-sdk/x/auth"
v39_1auth_vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/cosmos/cosmos-sdk/x/genutil"
v39_1supply "github.com/cosmos/cosmos-sdk/x/supply"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/kava-labs/kava/app"
v38_5auth "github.com/kava-labs/kava/migrate/v0_11/legacy/cosmos-sdk/v0.38.5/auth"
v38_5supply "github.com/kava-labs/kava/migrate/v0_11/legacy/cosmos-sdk/v0.38.5/supply"
@ -167,25 +164,3 @@ func TestMigratePricefeed(t *testing.T) {
err = newGenState.Validate()
require.NoError(t, err)
}
func TestMigrateFull(t *testing.T) {
oldGenDoc, err := tmtypes.GenesisDocFromFile(filepath.Join("testdata", "kava-3-export.json"))
require.NoError(t, err)
// 2) migrate
newGenDoc := Migrate(*oldGenDoc)
tApp := app.NewTestApp()
cdc := app.MakeCodec()
var newAppState genutil.AppMap
require.NoError(t,
cdc.UnmarshalJSON(newGenDoc.AppState, &newAppState),
)
err = app.ModuleBasics.ValidateGenesis(newAppState)
if err != nil {
require.NoError(t, err)
}
require.NotPanics(t, func() {
// this runs both InitGenesis for all modules (which panic on errors) and runs all invariants
tApp.InitializeFromGenesisStatesWithTime(newGenDoc.GenesisTime, app.GenesisState(newAppState))
})
}

69
migrate/v0_13/migrate.go Normal file
View File

@ -0,0 +1,69 @@
package v0_13
import (
"time"
v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
v0_13cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_13"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state
func MigrateCDP(oldGenState v0_11cdp.GenesisState) v0_13cdp.GenesisState {
var newCDPs v0_13cdp.CDPs
var newDeposits v0_13cdp.Deposits
var newCollateralParams v0_13cdp.CollateralParams
var newGenesisAccumulationTimes v0_13cdp.GenesisAccumulationTimes
var previousAccumulationTime time.Time
var totalPrincipals v0_13cdp.GenesisTotalPrincipals
newStartingID := oldGenState.StartingCdpID
totalPrincipalMap := make(map[string]sdk.Int)
for _, cp := range oldGenState.Params.CollateralParams {
newCollateralParam := v0_13cdp.NewCollateralParam(cp.Denom, cp.Type, cp.LiquidationRatio, cp.DebtLimit, cp.StabilityFee, cp.AuctionSize, cp.LiquidationPenalty, cp.Prefix, cp.SpotMarketID, cp.LiquidationMarketID, sdk.MustNewDecFromStr("0.01"), sdk.NewInt(10), cp.ConversionFactor)
newCollateralParams = append(newCollateralParams, newCollateralParam)
newGenesisAccumulationTime := v0_13cdp.NewGenesisAccumulationTime(cp.Type, previousAccumulationTime, sdk.OneDec())
newGenesisAccumulationTimes = append(newGenesisAccumulationTimes, newGenesisAccumulationTime)
totalPrincipalMap[cp.Type] = sdk.ZeroInt()
}
for _, cdp := range oldGenState.CDPs {
newCDP := v0_13cdp.NewCDPWithFees(cdp.ID, cdp.Owner, cdp.Collateral, cdp.Type, cdp.Principal, cdp.AccumulatedFees, cdp.FeesUpdated, sdk.OneDec())
if previousAccumulationTime.Before(cdp.FeesUpdated) {
previousAccumulationTime = cdp.FeesUpdated
}
totalPrincipalMap[cdp.Type] = totalPrincipalMap[cdp.Type].Add(newCDP.GetTotalPrincipal().Amount)
newCDPs = append(newCDPs, newCDP)
}
for _, dep := range oldGenState.Deposits {
newDep := v0_13cdp.NewDeposit(dep.CdpID, dep.Depositor, dep.Amount)
newDeposits = append(newDeposits, newDep)
}
for ctype, tp := range totalPrincipalMap {
totalPrincipal := v0_13cdp.NewGenesisTotalPrincipal(ctype, tp)
totalPrincipals = append(totalPrincipals, totalPrincipal)
}
oldDebtParam := oldGenState.Params.DebtParam
newDebtParam := v0_13cdp.NewDebtParam(oldDebtParam.Denom, oldDebtParam.ReferenceAsset, oldDebtParam.ConversionFactor, oldDebtParam.DebtFloor)
newGlobalDebtLimit := oldGenState.Params.GlobalDebtLimit
newParams := v0_13cdp.NewParams(newGlobalDebtLimit, newCollateralParams, newDebtParam, oldGenState.Params.SurplusAuctionThreshold, oldGenState.Params.SurplusAuctionLot, oldGenState.Params.DebtAuctionThreshold, oldGenState.Params.DebtAuctionLot, false)
return v0_13cdp.NewGenesisState(
newParams,
newCDPs,
newDeposits,
newStartingID,
oldGenState.DebtDenom,
oldGenState.GovDenom,
newGenesisAccumulationTimes,
totalPrincipals,
)
}

View File

@ -0,0 +1,43 @@
package v0_13
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
v0_11cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
app.SetBip44CoinType(config)
os.Exit(m.Run())
}
func TestMigrateCdp(t *testing.T) {
bz, err := ioutil.ReadFile(filepath.Join("testdata", "kava-4-cdp-state-block-500000.json"))
require.NoError(t, err)
var oldGenState v0_11cdp.GenesisState
cdc := app.MakeCodec()
require.NotPanics(t, func() {
cdc.MustUnmarshalJSON(bz, &oldGenState)
})
newGenState := MigrateCDP(oldGenState)
err = newGenState.Validate()
require.NoError(t, err)
require.Equal(t, len(newGenState.Params.CollateralParams), len(newGenState.PreviousAccumulationTimes))
cdp1 := newGenState.CDPs[0]
require.Equal(t, sdk.OneDec(), cdp1.InterestFactor)
}

File diff suppressed because it is too large Load Diff

View File

@ -272,10 +272,11 @@ func (a CollateralAuction) String() string {
End Time: %s
Max End Time: %s
Max Bid %s
LotReturns %s`,
LotReturns %s
Corresponding Debt %s`,
a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(),
a.MaxEndTime.String(), a.MaxBid, a.LotReturns,
a.MaxEndTime.String(), a.MaxBid, a.LotReturns, a.CorrespondingDebt,
)
}

View File

@ -14,12 +14,6 @@ import (
func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
params := k.GetParams(ctx)
previousDistTime, found := k.GetPreviousSavingsDistribution(ctx)
if !found {
previousDistTime = ctx.BlockTime()
k.SetPreviousSavingsDistribution(ctx, previousDistTime)
}
for _, cp := range params.CollateralParams {
ok := k.UpdatePricefeedStatus(ctx, cp.SpotMarketID)
if !ok {
@ -31,7 +25,12 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
continue
}
err := k.UpdateFeesForAllCdps(ctx, cp.Type)
err := k.AccumulateInterest(ctx, cp.Type)
if err != nil {
panic(err)
}
err = k.SynchronizeInterestForRiskyCDPs(ctx, cp.CheckCollateralizationIndexCount, sdk.MaxSortableDec, cp.Type)
if err != nil {
panic(err)
}
@ -46,16 +45,4 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
if err != nil {
panic(err)
}
distTimeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousDistTime.Unix())
if !distTimeElapsed.GTE(sdk.NewInt(int64(params.SavingsDistributionFrequency.Seconds()))) {
return
}
err = k.DistributeSavingsRate(ctx, params.DebtParam.Denom)
if err != nil {
panic(err)
}
k.SetPreviousSavingsDistribution(ctx, ctx.BlockTime())
}

View File

@ -165,7 +165,7 @@ func (suite *ModuleTestSuite) TestSeizeSingleCdpWithFees() {
}
cdpMacc = sk.GetModuleAccount(suite.ctx, cdp.ModuleName)
suite.Equal(i(1000000900), (cdpMacc.GetCoins().AmountOf("debt")))
suite.Equal(i(1000000891), (cdpMacc.GetCoins().AmountOf("debt")))
cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp-a", 1)
err = suite.keeper.SeizeCollateral(suite.ctx, cdp)

View File

@ -8,7 +8,6 @@ import (
)
const (
BaseDigitFactor = keeper.BaseDigitFactor
AttributeKeyCdpID = types.AttributeKeyCdpID
AttributeKeyDeposit = types.AttributeKeyDeposit
AttributeKeyError = types.AttributeKeyError
@ -38,12 +37,12 @@ const (
RestOwner = types.RestOwner
RestRatio = types.RestRatio
RouterKey = types.RouterKey
SavingsRateMacc = types.SavingsRateMacc
StoreKey = types.StoreKey
)
var (
// function aliases
CalculateInterestFactor = keeper.CalculateInterestFactor
FilterCDPs = keeper.FilterCDPs
FindIntersection = keeper.FindIntersection
NewKeeper = keeper.NewKeeper
@ -65,12 +64,16 @@ var (
NewCollateralParam = types.NewCollateralParam
NewDebtParam = types.NewDebtParam
NewDeposit = types.NewDeposit
NewGenesisAccumulationTime = types.NewGenesisAccumulationTime
NewGenesisState = types.NewGenesisState
NewGenesisTotalPrincipal = types.NewGenesisTotalPrincipal
NewMsgCreateCDP = types.NewMsgCreateCDP
NewMsgDeposit = types.NewMsgDeposit
NewMsgDrawDebt = types.NewMsgDrawDebt
NewMsgLiquidate = types.NewMsgLiquidate
NewMsgRepayDebt = types.NewMsgRepayDebt
NewMsgWithdraw = types.NewMsgWithdraw
NewMultiCDPHooks = types.NewMultiCDPHooks
NewParams = types.NewParams
NewQueryCdpDeposits = types.NewQueryCdpDeposits
NewQueryCdpParams = types.NewQueryCdpParams
@ -105,13 +108,12 @@ var (
DefaultDebtThreshold = types.DefaultDebtThreshold
DefaultGlobalDebt = types.DefaultGlobalDebt
DefaultGovDenom = types.DefaultGovDenom
DefaultPreviousDistributionTime = types.DefaultPreviousDistributionTime
DefaultSavingsDistributionFrequency = types.DefaultSavingsDistributionFrequency
DefaultSavingsRateDistributed = types.DefaultSavingsRateDistributed
DefaultStableDenom = types.DefaultStableDenom
DefaultSurplusLot = types.DefaultSurplusLot
DefaultSurplusThreshold = types.DefaultSurplusThreshold
DepositKeyPrefix = types.DepositKeyPrefix
ErrAccountNotFound = types.ErrAccountNotFound
ErrBelowDebtFloor = types.ErrBelowDebtFloor
ErrCdpAlreadyExists = types.ErrCdpAlreadyExists
ErrCdpNotAvailable = types.ErrCdpNotAvailable
@ -122,6 +124,7 @@ var (
ErrDepositNotAvailable = types.ErrDepositNotAvailable
ErrDepositNotFound = types.ErrDepositNotFound
ErrExceedsDebtLimit = types.ErrExceedsDebtLimit
ErrInsufficientBalance = types.ErrInsufficientBalance
ErrInvalidCollateral = types.ErrInvalidCollateral
ErrInvalidCollateralLength = types.ErrInvalidCollateralLength
ErrInvalidCollateralRatio = types.ErrInvalidCollateralRatio
@ -130,24 +133,23 @@ var (
ErrInvalidPayment = types.ErrInvalidPayment
ErrInvalidWithdrawAmount = types.ErrInvalidWithdrawAmount
ErrLoadingAugmentedCDP = types.ErrLoadingAugmentedCDP
ErrNotLiquidatable = types.ErrNotLiquidatable
ErrPricefeedDown = types.ErrPricefeedDown
GovDenomKey = types.GovDenomKey
InterestFactorPrefix = types.InterestFactorPrefix
KeyCircuitBreaker = types.KeyCircuitBreaker
KeyCollateralParams = types.KeyCollateralParams
KeyDebtLot = types.KeyDebtLot
KeyDebtParam = types.KeyDebtParam
KeyDebtThreshold = types.KeyDebtThreshold
KeyDistributionFrequency = types.KeyDistributionFrequency
KeyGlobalDebtLimit = types.KeyGlobalDebtLimit
KeySavingsRateDistributed = types.KeySavingsRateDistributed
KeySurplusLot = types.KeySurplusLot
KeySurplusThreshold = types.KeySurplusThreshold
MaxSortableDec = types.MaxSortableDec
ModuleCdc = types.ModuleCdc
PreviousDistributionTimeKey = types.PreviousDistributionTimeKey
PreviousAccrualTimePrefix = types.PreviousAccrualTimePrefix
PricefeedStatusKeyPrefix = types.PricefeedStatusKeyPrefix
PrincipalKeyPrefix = types.PrincipalKeyPrefix
SavingsRateDistributedKey = types.SavingsRateDistributedKey
)
type (
@ -157,6 +159,7 @@ type (
AugmentedCDP = types.AugmentedCDP
AugmentedCDPs = types.AugmentedCDPs
CDP = types.CDP
CDPHooks = types.CDPHooks
CDPs = types.CDPs
CollateralParam = types.CollateralParam
CollateralParams = types.CollateralParams
@ -164,12 +167,18 @@ type (
DebtParams = types.DebtParams
Deposit = types.Deposit
Deposits = types.Deposits
GenesisAccumulationTime = types.GenesisAccumulationTime
GenesisAccumulationTimes = types.GenesisAccumulationTimes
GenesisState = types.GenesisState
GenesisTotalPrincipal = types.GenesisTotalPrincipal
GenesisTotalPrincipals = types.GenesisTotalPrincipals
MsgCreateCDP = types.MsgCreateCDP
MsgDeposit = types.MsgDeposit
MsgDrawDebt = types.MsgDrawDebt
MsgLiquidate = types.MsgLiquidate
MsgRepayDebt = types.MsgRepayDebt
MsgWithdraw = types.MsgWithdraw
MultiCDPHooks = types.MultiCDPHooks
Params = types.Params
PricefeedKeeper = types.PricefeedKeeper
QueryCdpDeposits = types.QueryCdpDeposits

View File

@ -4,7 +4,6 @@ import (
"fmt"
"strconv"
"strings"
"time"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@ -41,8 +40,6 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
QueryCdpDepositsCmd(queryRoute, cdc),
QueryParamsCmd(queryRoute, cdc),
QueryGetAccounts(queryRoute, cdc),
QueryGetSavingsRateDistributed(queryRoute, cdc),
QueryGetSavingsRateDistTime(queryRoute, cdc),
)...)
return cdpQueryCmd
@ -284,56 +281,3 @@ func QueryGetAccounts(queryRoute string, cdc *codec.Codec) *cobra.Command {
},
}
}
// QueryGetSavingsRateDistributed queries the total amount of savings rate distributed in USDX
func QueryGetSavingsRateDistributed(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "savings-rate-dist",
Short: "get total amount of savings rate distributed in USDX",
Long: "get total amount of savings rate distributed",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetSavingsRateDistributed), nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var out sdk.Int
if err := cdc.UnmarshalJSON(res, &out); err != nil {
return fmt.Errorf("failed to unmarshal sdk.Int: %w", err)
}
return cliCtx.PrintOutput(out)
},
}
}
// QueryGetSavingsRateDistributed queries the total amount of savings rate distributed in USDX
func QueryGetSavingsRateDistTime(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "savings-rate-dist-time",
Short: "get the previous savings rate distribution time",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
// Query
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/%s/%s", queryRoute, types.QueryGetPreviousSavingsDistributionTime), nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var out time.Time
if err := cdc.UnmarshalJSON(res, &out); err != nil {
return fmt.Errorf("failed to unmarshal time.Time: %w", err)
}
return cliCtx.PrintOutput(out)
},
}
}

View File

@ -31,6 +31,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
GetCmdWithdraw(cdc),
GetCmdDraw(cdc),
GetCmdRepay(cdc),
GetCmdLiquidate(cdc),
)...)
return cdpTxCmd
@ -202,3 +203,34 @@ $ %s tx %s repay atom-a 1000usdx --from myKeyName
},
}
}
// GetCmdLiquidate cli command for liquidating a cdp.
func GetCmdLiquidate(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "liquidate [cdp-owner-address] [collateral-type]",
Short: "liquidate a cdp",
Long: strings.TrimSpace(
fmt.Sprintf(`Liquidate a cdp if it is below the required liquidation ratio
Example:
$ %s tx %s liquidate kava1y70y90wzmnf00e63efk2lycgqwepthdmyzsfzm btcb-a --from myKeyName
`, 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))
addr, err := sdk.AccAddressFromBech32(args[0])
if err != nil {
return err
}
msg := types.NewMsgLiquidate(cliCtx.GetFromAddress(), addr, args[1])
err = msg.ValidateBasic()
if err != nil {
return err
}
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
},
}
}

View File

@ -18,8 +18,6 @@ import (
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdp/accounts", getAccountsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc("/cdp/parameters", getParamsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc("/cdp/savingsRateDist", getSavingsRateDistributedHandler(cliCtx)).Methods("GET")
r.HandleFunc("/cdp/savingsRateDistTime", getSavingsRateDistTimeHandler(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/cdp/{%s}/{%s}", types.RestOwner, types.RestCollateralType), queryCdpHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps"), queryCdpsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/collateralType/{%s}", types.RestCollateralType), queryCdpsByCollateralTypeHandlerFn(cliCtx)).Methods("GET") // legacy
@ -199,42 +197,6 @@ func getAccountsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
}
}
func getSavingsRateDistributedHandler(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetSavingsRateDistributed), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func getSavingsRateDistTimeHandler(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
res, height, err := cliCtx.QueryWithData(fmt.Sprintf("custom/cdp/%s", types.QueryGetPreviousSavingsDistributionTime), nil)
cliCtx = cliCtx.WithHeight(height)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)

View File

@ -65,3 +65,10 @@ type PostRepayReq struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
Payment sdk.Coin `json:"payment" yaml:"payment"`
}
// PostLiquidateReq defines the properties of cdp liquidation request's body.
type PostLiquidateReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
}

View File

@ -20,6 +20,7 @@ func registerTxRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdp/{owner}/{collateralType}/withdraw", postWithdrawHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/cdp/{owner}/{collateralType}/draw", postDrawHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/cdp/{owner}/{collateralType}/repay", postRepayHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/cdp/{owner}/collateralType}/liquidate", postLiquidateHandlerFn(cliCtx)).Methods("POST")
}
func postCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
@ -187,3 +188,35 @@ func postRepayHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}
func postLiquidateHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var requestBody PostLiquidateReq
if !rest.ReadRESTReq(w, r, cliCtx.Codec, &requestBody) {
return
}
requestBody.BaseReq = requestBody.BaseReq.Sanitize()
if !requestBody.BaseReq.ValidateBasic(w) {
return
}
fromAddr, err := sdk.AccAddressFromBech32(requestBody.BaseReq.From)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
msg := types.NewMsgLiquidate(
fromAddr,
requestBody.Owner,
requestBody.CollateralType,
)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
utils.WriteGenerateStdTxResponse(w, cliCtx, requestBody.BaseReq, []sdk.Msg{msg})
}
}

View File

@ -24,10 +24,6 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk types.PricefeedKeeper, sk types.S
if liqModuleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", LiquidatorMacc))
}
savingsRateMacc := sk.GetModuleAccount(ctx, SavingsRateMacc)
if savingsRateMacc == nil {
panic(fmt.Sprintf("%s module account has not been set", SavingsRateMacc))
}
// validate denoms - check that any collaterals in the params are in the pricefeed,
// pricefeed MUST call InitGenesis before cdp
@ -57,11 +53,16 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk types.PricefeedKeeper, sk types.S
k.SetParams(ctx, gs.Params)
// set the per second fee rate for each collateral type
for _, cp := range gs.Params.CollateralParams {
k.SetTotalPrincipal(ctx, cp.Type, gs.Params.DebtParam.Denom, sdk.ZeroInt())
for _, gat := range gs.PreviousAccumulationTimes {
k.SetInterestFactor(ctx, gat.CollateralType, gat.InterestFactor)
if !gat.PreviousAccumulationTime.IsZero() {
k.SetPreviousAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
}
}
for _, gtp := range gs.TotalPrincipals {
k.SetTotalPrincipal(ctx, gtp.CollateralType, types.DefaultStableDenom, gtp.TotalPrincipal)
}
// add cdps
for _, cdp := range gs.CDPs {
if cdp.ID == gs.StartingCdpID {
@ -74,22 +75,16 @@ func InitGenesis(ctx sdk.Context, k Keeper, pk types.PricefeedKeeper, sk types.S
k.IndexCdpByOwner(ctx, cdp)
ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.IndexCdpByCollateralRatio(ctx, cdp.Type, cdp.ID, ratio)
k.IncrementTotalPrincipal(ctx, cdp.Type, cdp.GetTotalPrincipal())
}
k.SetNextCdpID(ctx, gs.StartingCdpID)
k.SetDebtDenom(ctx, gs.DebtDenom)
k.SetGovDenom(ctx, gs.GovDenom)
// only set the previous block time if it's different than default
if !gs.PreviousDistributionTime.Equal(types.DefaultPreviousDistributionTime) {
k.SetPreviousSavingsDistribution(ctx, gs.PreviousDistributionTime)
}
for _, d := range gs.Deposits {
k.SetDeposit(ctx, d)
}
k.SetSavingsRateDistributed(ctx, gs.SavingsRateDistributed)
}
// ExportGenesis export genesis state for cdp module
@ -110,12 +105,22 @@ func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
cdpID := k.GetNextCdpID(ctx)
debtDenom := k.GetDebtDenom(ctx)
govDenom := k.GetGovDenom(ctx)
savingsRateDist := k.GetSavingsRateDistributed(ctx)
previousDistributionTime, found := k.GetPreviousSavingsDistribution(ctx)
var previousAccumTimes types.GenesisAccumulationTimes
var totalPrincipals types.GenesisTotalPrincipals
for _, cp := range params.CollateralParams {
interestFactor, found := k.GetInterestFactor(ctx, cp.Type)
if !found {
previousDistributionTime = DefaultPreviousDistributionTime
interestFactor = sdk.OneDec()
}
previousAccumTime, _ := k.GetPreviousAccrualTime(ctx, cp.Type)
previousAccumTimes = append(previousAccumTimes, types.NewGenesisAccumulationTime(cp.Type, previousAccumTime, interestFactor))
tp := k.GetTotalPrincipal(ctx, cp.Type, types.DefaultStableDenom)
genTotalPrincipal := types.NewGenesisTotalPrincipal(cp.Type, tp)
totalPrincipals = append(totalPrincipals, genTotalPrincipal)
}
return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousDistributionTime, savingsRateDist)
return NewGenesisState(params, cdps, deposits, cdpID, debtDenom, govDenom, previousAccumTimes, totalPrincipals)
}

View File

@ -28,8 +28,9 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
startingID uint64
debtDenom string
govDenom string
prevDistTime time.Time
savingsRateDist sdk.Int
genAccumTimes cdp.GenesisAccumulationTimes
genTotalPrincipals cdp.GenesisTotalPrincipals
}
type errArgs struct {
expectPass bool
@ -53,8 +54,9 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
deposits: cdp.Deposits{},
debtDenom: "",
govDenom: cdp.DefaultGovDenom,
prevDistTime: cdp.DefaultPreviousDistributionTime,
savingsRateDist: cdp.DefaultSavingsRateDistributed,
genAccumTimes: cdp.DefaultGenesisState().PreviousAccumulationTimes,
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
},
errArgs: errArgs{
expectPass: false,
@ -69,8 +71,9 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom,
govDenom: "",
prevDistTime: cdp.DefaultPreviousDistributionTime,
savingsRateDist: cdp.DefaultSavingsRateDistributed,
genAccumTimes: cdp.DefaultGenesisState().PreviousAccumulationTimes,
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
},
errArgs: errArgs{
expectPass: false,
@ -78,48 +81,49 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
},
},
{
name: "empty distribution time",
name: "interest factor below one",
args: args{
params: cdp.DefaultParams(),
cdps: cdp.CDPs{},
deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom,
govDenom: cdp.DefaultGovDenom,
prevDistTime: time.Time{},
savingsRateDist: cdp.DefaultSavingsRateDistributed,
savingsRateDist: sdk.NewInt(100),
genAccumTimes: cdp.GenesisAccumulationTimes{cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec().Sub(sdk.SmallestDec()))},
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
},
errArgs: errArgs{
expectPass: false,
contains: "previous distribution time not set",
contains: "interest factor should be ≥ 1.0",
},
},
{
name: "negative savings rate distributed",
name: "negative total principal",
args: args{
params: cdp.DefaultParams(),
cdps: cdp.CDPs{},
deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom,
govDenom: cdp.DefaultGovDenom,
prevDistTime: cdp.DefaultPreviousDistributionTime,
savingsRateDist: sdk.NewInt(-100),
savingsRateDist: sdk.NewInt(100),
genAccumTimes: cdp.DefaultGenesisState().PreviousAccumulationTimes,
genTotalPrincipals: cdp.GenesisTotalPrincipals{cdp.NewGenesisTotalPrincipal("bnb-a", sdk.NewInt(-1))},
},
errArgs: errArgs{
expectPass: false,
contains: "savings rate distributed should not be negative",
contains: "total principal should be positive",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
gs := cdp.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID,
tc.args.debtDenom, tc.args.govDenom, tc.args.prevDistTime, tc.args.savingsRateDist)
tc.args.debtDenom, tc.args.govDenom, tc.args.genAccumTimes, tc.args.genTotalPrincipals)
err := gs.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.T().Log(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})

View File

@ -20,6 +20,8 @@ func NewHandler(k Keeper) sdk.Handler {
return handleMsgDrawDebt(ctx, k, msg)
case MsgRepayDebt:
return handleMsgRepayDebt(ctx, k, msg)
case MsgLiquidate:
return handleMsgLiquidate(ctx, k, msg)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
}
@ -110,3 +112,19 @@ func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) (*sdk.Resul
)
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}
func handleMsgLiquidate(ctx sdk.Context, k Keeper, msg MsgLiquidate) (*sdk.Result, error) {
err := k.AttemptKeeperLiquidation(ctx, msg.Keeper, msg.Borrower, msg.CollateralType)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Keeper.String()),
),
)
return &sdk.Result{Events: ctx.EventManager().Events()}, nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed"
)
@ -45,7 +46,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: asset,
@ -59,6 +59,8 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ConversionFactor: i(6),
SpotMarketID: asset + ":usd",
LiquidationMarketID: asset + ":usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
},
},
DebtParam: cdp.DebtParam{
@ -66,14 +68,18 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime(asset+"-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal(asset+"-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@ -111,7 +117,6 @@ func NewCDPGenStateMulti() app.GenesisState {
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@ -124,6 +129,8 @@ func NewCDPGenStateMulti() app.GenesisState {
Prefix: 0x20,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
},
{
@ -137,6 +144,8 @@ func NewCDPGenStateMulti() app.GenesisState {
Prefix: 0x21,
SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
},
},
@ -145,24 +154,30 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("btc-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("xrp-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: types.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("btc-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("xrp-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3)
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), "btc-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(50000000)), tmtime.Canonical(time.Now()))
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), "btc-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(50000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
cdps = append(cdps, c1, c2, c3, c4)
return
}

View File

@ -11,9 +11,6 @@ import (
"github.com/kava-labs/kava/x/cdp/types"
)
// BaseDigitFactor is 10**18, used during coin calculations
const BaseDigitFactor = 1000000000000000000
// AddCdp adds a cdp for a specific owner and collateral type
func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coin, principal sdk.Coin, collateralType string) error {
// validation
@ -21,6 +18,10 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
if err != nil {
return err
}
err = k.ValidateBalance(ctx, collateral, owner)
if err != nil {
return err
}
_, found := k.GetCdpByOwnerAndCollateralType(ctx, owner, collateralType)
if found {
return sdkerrors.Wrapf(types.ErrCdpAlreadyExists, "owner %s, denom %s", owner, collateral.Denom)
@ -41,7 +42,13 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
// send coins from the owners account to the cdp module
id := k.GetNextCdpID(ctx)
cdp := types.NewCDP(id, owner, collateral, collateralType, principal, ctx.BlockHeader().Time)
interestFactor, found := k.GetInterestFactor(ctx, collateralType)
if !found {
interestFactor = sdk.OneDec()
k.SetInterestFactor(ctx, collateralType, interestFactor)
}
cdp := types.NewCDP(id, owner, collateral, collateralType, principal, ctx.BlockHeader().Time, interestFactor)
deposit := types.NewDeposit(cdp.ID, owner, collateral)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(collateral))
if err != nil {
@ -77,6 +84,8 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
k.SetDeposit(ctx, deposit)
k.SetNextCdpID(ctx, id+1)
k.hooks.AfterCDPCreated(ctx, cdp)
// emit events for cdp creation, deposit, and draw
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@ -102,6 +111,31 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
return nil
}
// UpdateCdpAndCollateralRatioIndex updates the state of an existing cdp in the store by replacing the old index values and updating the store to the latest cdp object values
func (k Keeper) UpdateCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) error {
err := k.removeOldCollateralRatioIndex(ctx, cdp.Type, cdp.ID)
if err != nil {
return err
}
err = k.SetCDP(ctx, cdp)
if err != nil {
return err
}
k.IndexCdpByCollateralRatio(ctx, cdp.Type, cdp.ID, ratio)
return nil
}
// DeleteCdpAndCollateralRatioIndex deletes an existing cdp in the store by removing the old index value and deleting the cdp object from the store
func (k Keeper) DeleteCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP) error {
err := k.removeOldCollateralRatioIndex(ctx, cdp.Type, cdp.ID)
if err != nil {
return err
}
return k.DeleteCDP(ctx, cdp)
}
// SetCdpAndCollateralRatioIndex sets the cdp and collateral ratio index in the store
func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) error {
err := k.SetCDP(ctx, cdp)
@ -112,6 +146,16 @@ func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ra
return nil
}
func (k Keeper) removeOldCollateralRatioIndex(ctx sdk.Context, ctype string, id uint64) error {
storedCDP, found := k.GetCDP(ctx, ctype, id)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "%d", storedCDP.ID)
}
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, storedCDP.Collateral, storedCDP.Type, storedCDP.GetTotalPrincipal())
k.RemoveCdpCollateralRatioIndex(ctx, storedCDP.Type, storedCDP.ID, oldCollateralToDebtRatio)
return nil
}
// MintDebtCoins mints debt coins in the cdp module account
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coin) error {
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, principalCoins.Amount))
@ -120,7 +164,10 @@ func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom strin
// BurnDebtCoins burns debt coins from the cdp module account
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coin) error {
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, paymentCoins.Amount))
macc := k.supplyKeeper.GetModuleAccount(ctx, moduleAccount)
maxBurnableAmount := macc.GetCoins().AmountOf(denom)
// check that the requested burn is not greater than the mod account balance
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, sdk.MinInt(paymentCoins.Amount, maxBurnableAmount)))
return k.supplyKeeper.BurnCoins(ctx, moduleAccount, debtCoins)
}
@ -419,6 +466,20 @@ func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.C
return nil
}
// ValidateBalance validates that the input account has sufficient spendable funds
func (k Keeper) ValidateBalance(ctx sdk.Context, amount sdk.Coin, sender sdk.AccAddress) error {
acc := k.accountKeeper.GetAccount(ctx, sender)
if acc == nil {
return sdkerrors.Wrapf(types.ErrAccountNotFound, "address: %s", sender)
}
spendableBalance := acc.SpendableCoins(ctx.BlockTime()).AmountOf(amount.Denom)
if spendableBalance.LT(amount.Amount) {
return sdkerrors.Wrapf(types.ErrInsufficientBalance, "%s < %s", sdk.NewCoin(amount.Denom, spendableBalance), amount)
}
return nil
}
// CalculateCollateralToDebtRatio returns the collateral to debt ratio of the input collateral and debt amounts
func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coin, collateralType string, debt sdk.Coin) sdk.Dec {
debtTotal := k.convertDebtToBaseUnits(ctx, debt)
@ -433,6 +494,9 @@ func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.C
// LoadAugmentedCDP creates a new augmented CDP from an existing CDP
func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) types.AugmentedCDP {
// sync the latest interest of the cdp
interestAccumulated := k.CalculateNewInterest(ctx, cdp)
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(interestAccumulated)
// calculate collateralization ratio
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Type, cdp.Principal, cdp.AccumulatedFees, liquidation)
if err != nil {

View File

@ -131,7 +131,7 @@ func (suite *CdpTestSuite) TestGetNextCdpID() {
func (suite *CdpTestSuite) TestGetSetCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()), sdk.OneDec())
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
@ -147,7 +147,7 @@ func (suite *CdpTestSuite) TestGetSetCdp() {
func (suite *CdpTestSuite) TestGetSetCdpId() {
_, addrs := app.GeneratePrivKeyAddressPairs(2)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()), sdk.OneDec())
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
@ -162,7 +162,7 @@ func (suite *CdpTestSuite) TestGetSetCdpId() {
func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndCollateralType() {
_, addrs := app.GeneratePrivKeyAddressPairs(2)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()), sdk.OneDec())
err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
@ -178,17 +178,17 @@ func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndCollateralType() {
func (suite *CdpTestSuite) TestCalculateCollateralToDebtRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()), sdk.OneDec())
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("3.0"), cr)
cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 2), tmtime.Canonical(time.Now()))
cdp = types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 1), "xrp-a", c("usdx", 2), tmtime.Canonical(time.Now()), sdk.OneDec())
cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("0.5"), cr)
}
func (suite *CdpTestSuite) TestSetCdpByCollateralRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()))
cdp := types.NewCDP(types.DefaultCdpStartingID, addrs[0], c("xrp", 3), "xrp-a", c("usdx", 1), tmtime.Canonical(time.Now()), sdk.OneDec())
cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal)
suite.NotPanics(func() { suite.keeper.IndexCdpByCollateralRatio(suite.ctx, cdp.Type, cdp.ID, cr) })
}

View File

@ -21,6 +21,12 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner, depositor sdk.AccAddre
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateralType)
}
err = k.ValidateBalance(ctx, collateral, depositor)
if err != nil {
return err
}
k.hooks.BeforeCDPModified(ctx, cdp)
cdp = k.SynchronizeInterest(ctx, cdp)
deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if found {
@ -32,6 +38,12 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner, depositor sdk.AccAddre
if err != nil {
return err
}
k.SetDeposit(ctx, deposit)
cdp.Collateral = cdp.Collateral.Add(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpDeposit,
@ -40,14 +52,7 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner, depositor sdk.AccAddre
),
)
k.SetDeposit(ctx, deposit)
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio)
cdp.Collateral = cdp.Collateral.Add(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
return k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
}
// WithdrawCollateral removes collateral from a cdp if it does not put the cdp below the liquidation ratio
@ -67,6 +72,8 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner, depositor sdk.AccAddr
if collateral.Amount.GT(deposit.Amount.Amount) {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "collateral %s, deposit %s", collateral, deposit.Amount)
}
k.hooks.BeforeCDPModified(ctx, cdp)
cdp = k.SynchronizeInterest(ctx, cdp)
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Type, cdp.Principal, cdp.AccumulatedFees, spot)
if err != nil {
@ -76,24 +83,15 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner, depositor sdk.AccAddr
if collateralizationRatio.LT(liquidationRatio) {
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ration %s", collateral.Denom, collateralizationRatio, liquidationRatio)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, sdk.NewCoins(collateral))
if err != nil {
panic(err)
}
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio)
cdp.Collateral = cdp.Collateral.Sub(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
err = k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
return err
}
@ -105,6 +103,15 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner, depositor sdk.AccAddr
} else {
k.SetDeposit(ctx, deposit)
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeCdpWithdrawal,
sdk.NewAttribute(sdk.AttributeKeyAmount, collateral.String()),
sdk.NewAttribute(types.AttributeKeyCdpID, fmt.Sprintf("%d", cdp.ID)),
),
)
return nil
}

View File

@ -25,6 +25,8 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateralTy
if err != nil {
return err
}
k.hooks.BeforeCDPModified(ctx, cdp)
cdp = k.SynchronizeInterest(ctx, cdp)
err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Type, cdp.Principal.Add(principal), cdp.AccumulatedFees)
if err != nil {
@ -56,10 +58,6 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateralTy
),
)
// remove old collateral:debt index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
cdp.Principal = cdp.Principal.Add(principal)
@ -68,7 +66,7 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateralTy
// set cdp state and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
return k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
}
// RepayPrincipal removes debt from the cdp
@ -85,6 +83,13 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
return err
}
err = k.ValidateBalance(ctx, payment, owner)
if err != nil {
return err
}
k.hooks.BeforeCDPModified(ctx, cdp)
cdp = k.SynchronizeInterest(ctx, cdp)
// Note: assumes cdp.Principal and cdp.AccumulatedFees don't change during calculations
totalPrincipal := cdp.GetTotalPrincipal()
@ -134,8 +139,6 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
)
// remove the old collateral:debt ratio index
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, totalPrincipal)
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio)
// update cdp state
if !principalPayment.IsZero() {
@ -150,12 +153,12 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
// and remove the cdp and indexes from the store
if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() {
k.ReturnCollateral(ctx, cdp)
if err := k.DeleteCDP(ctx, cdp); err != nil {
k.RemoveCdpOwnerIndex(ctx, cdp)
err := k.DeleteCdpAndCollateralRatioIndex(ctx, cdp)
if err != nil {
return err
}
k.RemoveCdpOwnerIndex(ctx, cdp)
// emit cdp close event
ctx.EventManager().EmitEvent(
sdk.NewEvent(
@ -168,7 +171,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
// set cdp state and update indexes
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
return k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
}
// ValidatePaymentCoins validates that the input coins are valid for repaying debt

View File

@ -129,37 +129,6 @@ func (suite *DrawTestSuite) TestRepayPrincipalOverpay() {
suite.False(found)
}
func (suite *DrawTestSuite) TestAddRepayPrincipalFees() {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[2], c("xrp", 1000000000000), c("usdx", 100000000000), "xrp-a")
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Minute * 10))
err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp-a")
suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp-a", c("usdx", 10000000))
suite.NoError(err)
t, _ := suite.keeper.GetCDP(suite.ctx, "xrp-a", uint64(2))
suite.Equal(c("usdx", 92827), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp-a", c("usdx", 100))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp-a", uint64(2))
suite.Equal(c("usdx", 92727), t.AccumulatedFees)
err = suite.keeper.RepayPrincipal(suite.ctx, suite.addrs[2], "xrp-a", c("usdx", 100010092727))
suite.NoError(err)
_, f := suite.keeper.GetCDP(suite.ctx, "xrp-a", uint64(2))
suite.False(f)
err = suite.keeper.AddCdp(suite.ctx, suite.addrs[2], c("xrp", 1000000000000), c("usdx", 100000000), "xrp-a")
suite.NoError(err)
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 31536000)) // move forward one year in time
err = suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp-a")
suite.NoError(err)
err = suite.keeper.AddPrincipal(suite.ctx, suite.addrs[2], "xrp-a", c("usdx", 100000000))
suite.NoError(err)
t, _ = suite.keeper.GetCDP(suite.ctx, "xrp-a", uint64(3))
suite.Equal(c("usdx", 5000000), t.AccumulatedFees)
}
func (suite *DrawTestSuite) TestPricefeedFailure() {
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pfk := suite.app.GetPriceFeedKeeper()

View File

@ -1,137 +0,0 @@
package keeper
import (
"fmt"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// CalculateFees returns the fees accumulated since fees were last calculated based on
// the input amount of outstanding debt (principal) and the number of periods (seconds) that have passed
func (k Keeper) CalculateFees(ctx sdk.Context, principal sdk.Coin, periods sdk.Int, collateralType string) sdk.Coin {
// how fees are calculated:
// feesAccumulated = (outstandingDebt * (feeRate^periods)) - outstandingDebt
// Note that since we can't do x^y using sdk.Decimal, we are converting to int and using RelativePow
feePerSecond := k.getFeeRate(ctx, collateralType)
scalar := sdk.NewInt(1000000000000000000)
feeRateInt := feePerSecond.Mul(sdk.NewDecFromInt(scalar)).TruncateInt()
accumulator := sdk.NewDecFromInt(types.RelativePow(feeRateInt, periods, scalar)).Mul(sdk.SmallestDec())
feesAccumulated := (sdk.NewDecFromInt(principal.Amount).Mul(accumulator)).Sub(sdk.NewDecFromInt(principal.Amount))
newFees := sdk.NewCoin(principal.Denom, feesAccumulated.TruncateInt())
return newFees
}
// UpdateFeesForAllCdps updates the fees for each of the CDPs
func (k Keeper) UpdateFeesForAllCdps(ctx sdk.Context, collateralType string) error {
var iterationErr error
k.IterateCdpsByCollateralType(ctx, collateralType, func(cdp types.CDP) bool {
oldCollateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
// periods = bblock timestamp - fees updated
periods := sdk.NewInt(ctx.BlockTime().Unix()).Sub(sdk.NewInt(cdp.FeesUpdated.Unix()))
newFees := k.CalculateFees(ctx, cdp.Principal, periods, collateralType)
// exit without updating fees if amount has rounded down to zero
// cdp will get updated next block when newFees, newFeesSavings, newFeesSurplus >0
if newFees.IsZero() {
return false
}
dp, found := k.GetDebtParam(ctx, cdp.Principal.Denom)
if !found {
return false
}
savingsRate := dp.SavingsRate
newFeesSavings := sdk.NewDecFromInt(newFees.Amount).Mul(savingsRate).RoundInt()
newFeesSurplus := newFees.Amount.Sub(newFeesSavings)
// similar to checking for rounding to zero of all fees, but in this case we
// need to handle cases where we expect surplus or savings fees to be zero, namely
// if newFeesSavings = 0, check if savings rate is not zero
// if newFeesSurplus = 0, check if savings rate is not one
if (newFeesSavings.IsZero() && !savingsRate.IsZero()) || (newFeesSurplus.IsZero() && !savingsRate.Equal(sdk.OneDec())) {
return false
}
// mint debt coins to the cdp account
err := k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), newFees)
if err != nil {
iterationErr = err
return true
}
previousDebt := k.GetTotalPrincipal(ctx, cdp.Type, dp.Denom)
newDebt := previousDebt.Add(newFees.Amount)
k.SetTotalPrincipal(ctx, cdp.Type, dp.Denom, newDebt)
// mint surplus coins divided between the liquidator and savings module accounts.
err = k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus)))
if err != nil {
iterationErr = err
return true
}
err = k.supplyKeeper.MintCoins(ctx, types.SavingsRateMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSavings)))
if err != nil {
iterationErr = err
return true
}
// now add the new fees to the accumulated fees for the cdp
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(newFees)
// and set the fees updated time to the current block time since we just updated it
cdp.FeesUpdated = ctx.BlockTime()
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.RemoveCdpCollateralRatioIndex(ctx, cdp.Type, cdp.ID, oldCollateralToDebtRatio)
err = k.SetCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
if err != nil {
iterationErr = err
return true
}
return false // this returns true when you want to stop iterating. Since we want to iterate through all we return false
})
return iterationErr
}
// IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type
func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom)
total = total.Add(principal.Amount)
k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total)
}
// DecrementTotalPrincipal decrements the total amount of debt that has been drawn for a particular collateral type
func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom)
// NOTE: negative total principal can happen in tests due to rounding errors
// in fee calculation
total = sdk.MaxInt(total.Sub(principal.Amount), sdk.ZeroInt())
k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total)
}
// GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral
func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string) (total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
bz := store.Get([]byte(collateralType + principalDenom))
if bz == nil {
k.SetTotalPrincipal(ctx, collateralType, principalDenom, sdk.ZeroInt())
return sdk.ZeroInt()
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total)
return total
}
// SetTotalPrincipal sets the total amount of principal that has been drawn for the input collateral
func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string, total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
_, found := k.GetCollateralTypePrefix(ctx, collateralType)
if !found {
panic(fmt.Sprintf("collateral not found: %s", collateralType))
}
store.Set([]byte(collateralType+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total))
}

View File

@ -1,101 +0,0 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp/keeper"
)
type FeeTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *FeeTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
keeper := tApp.GetCDPKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
}
// createCdps is a helper function to create two CDPs each with zero fees
func (suite *FeeTestSuite) createCdps() {
// create 2 accounts in the state and give them some coins
// create two private key pair addresses
_, addrs := app.GeneratePrivKeyAddressPairs(2)
ak := suite.app.GetAccountKeeper()
// setup the first account
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc)
// now setup the second account
acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1])
acc2.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc2)
// now create two cdps with the addresses we just created
// use the created account to create a cdp that SHOULD have fees updated
// to get a ratio between 100 - 110% of liquidation ratio we can use 200xrp ($50) and 24 usdx (208% collateralization with liquidation ratio of 200%)
// create CDP for the first address
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 200000000), c("usdx", 24000000), "xrp-a")
suite.NoError(err) // check that no error was thrown
// use the other account to create a cdp that SHOULD NOT have fees updated - 500% collateralization
// create CDP for the second address
err = suite.keeper.AddCdp(suite.ctx, addrs[1], c("xrp", 200000000), c("usdx", 10000000), "xrp-a")
suite.NoError(err) // check that no error was thrown
}
// TestUpdateFees tests the functionality for updating the fees for CDPs
func (suite *FeeTestSuite) TestUpdateFees() {
// this helper function creates two CDPs with id 1 and 2 respectively, each with zero fees
suite.createCdps()
// move the context forward in time so that cdps will have fees accumulate if CalculateFees is called
// note - time must be moved forward by a sufficient amount in order for additional
// fees to accumulate, in this example 600 seconds
oldtime := suite.ctx.BlockTime()
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 600))
err := suite.keeper.UpdateFeesForAllCdps(suite.ctx, "xrp-a")
suite.NoError(err) // check that we don't have any error
// cdp we expect fees to accumulate for
cdp1, found := suite.keeper.GetCDP(suite.ctx, "xrp-a", 1)
suite.True(found)
// check fees are not zero
// check that the fees have been updated
suite.False(cdp1.AccumulatedFees.IsZero())
// now check that we have the correct amount of fees overall (22 USDX for this scenario)
suite.Equal(sdk.NewInt(22), cdp1.AccumulatedFees.Amount)
suite.Equal(suite.ctx.BlockTime(), cdp1.FeesUpdated)
// cdp we expect fees to not accumulate for because of rounding to zero
cdp2, found := suite.keeper.GetCDP(suite.ctx, "xrp-a", 2)
suite.True(found)
// check fees are zero
suite.True(cdp2.AccumulatedFees.IsZero())
suite.Equal(oldtime, cdp2.FeesUpdated)
}
func TestFeeTestSuite(t *testing.T) {
suite.Run(t, new(FeeTestSuite))
}

23
x/cdp/keeper/hooks.go Normal file
View File

@ -0,0 +1,23 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
// Implements StakingHooks interface
var _ types.CDPHooks = Keeper{}
// AfterCDPCreated - call hook if registered
func (k Keeper) AfterCDPCreated(ctx sdk.Context, cdp types.CDP) {
if k.hooks != nil {
k.hooks.AfterCDPCreated(ctx, cdp)
}
}
// BeforeCDPModified - call hook if registered
func (k Keeper) BeforeCDPModified(ctx sdk.Context, cdp types.CDP) {
if k.hooks != nil {
k.hooks.BeforeCDPModified(ctx, cdp)
}
}

View File

@ -45,7 +45,6 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: asset,
@ -56,9 +55,11 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
LiquidationPenalty: d("0.05"),
AuctionSize: i(100),
Prefix: 0x20,
ConversionFactor: i(6),
SpotMarketID: asset + ":usd",
LiquidationMarketID: asset + ":usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
},
},
DebtParam: cdp.DebtParam{
@ -66,14 +67,18 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.9"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime(asset+"-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal(asset+"-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@ -85,6 +90,7 @@ func NewPricefeedGenStateMulti() app.GenesisState {
{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "busd:usd", BaseAsset: "busd", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
@ -106,6 +112,12 @@ func NewPricefeedGenStateMulti() app.GenesisState {
Price: sdk.MustNewDecFromStr("17.25"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "busd:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.OneDec(),
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
@ -113,12 +125,11 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1500000000000),
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@ -131,6 +142,8 @@ func NewCDPGenStateMulti() app.GenesisState {
Prefix: 0x20,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
},
{
@ -144,6 +157,8 @@ func NewCDPGenStateMulti() app.GenesisState {
Prefix: 0x21,
SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
},
{
@ -157,6 +172,23 @@ func NewCDPGenStateMulti() app.GenesisState {
Prefix: 0x22,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
},
{
Denom: "busd",
Type: "busd-a",
LiquidationRatio: d("1.01"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.OneDec(), // %0 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(10000000000),
Prefix: 0x23,
SpotMarketID: "busd:usd",
LiquidationMarketID: "busd:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
},
},
@ -165,14 +197,24 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("btc-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("xrp-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("busd-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("btc-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("xrp-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("busd-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("bnb-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
@ -185,7 +227,6 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
@ -198,6 +239,8 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
Prefix: 0x20,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
},
{
@ -211,6 +254,8 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
Prefix: 0x21,
SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
},
},
@ -219,24 +264,30 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("btc-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("xrp-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("btc-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("xrp-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3)
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(10000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()))
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), "btc-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()))
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(500000000)), tmtime.Canonical(time.Now()))
c1 := cdp.NewCDP(uint64(1), addrs[0], sdk.NewCoin("xrp", sdk.NewInt(10000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(8000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c2 := cdp.NewCDP(uint64(2), addrs[1], sdk.NewCoin("xrp", sdk.NewInt(100000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c3 := cdp.NewCDP(uint64(3), addrs[1], sdk.NewCoin("btc", sdk.NewInt(1000000000)), "btc-a", sdk.NewCoin("usdx", sdk.NewInt(10000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
c4 := cdp.NewCDP(uint64(4), addrs[2], sdk.NewCoin("xrp", sdk.NewInt(1000000000)), "xrp-a", sdk.NewCoin("usdx", sdk.NewInt(500000000)), tmtime.Canonical(time.Now()), sdk.OneDec())
cdps = append(cdps, c1, c2, c3, c4)
return
}

164
x/cdp/keeper/interest.go Normal file
View File

@ -0,0 +1,164 @@
package keeper
import (
"fmt"
"math"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/cdp/types"
)
var (
scalingFactor = 1e18
secondsPerYear = 31536000
)
// AccumulateInterest calculates the new interest that has accrued for the input collateral type based on the total amount of principal
// that has been created with that collateral type and the amount of time that has passed since interest was last accumulated
func (k Keeper) AccumulateInterest(ctx sdk.Context, ctype string) error {
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, ctype)
if !found {
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
return nil
}
timeElapsed := int64(math.RoundToEven(
ctx.BlockTime().Sub(previousAccrualTime).Seconds(),
))
if timeElapsed == 0 {
return nil
}
totalPrincipalPrior := k.GetTotalPrincipal(ctx, ctype, types.DefaultStableDenom)
if totalPrincipalPrior.IsZero() || totalPrincipalPrior.IsNegative() {
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
return nil
}
interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, ctype)
if !foundInterestFactorPrior {
k.SetInterestFactor(ctx, ctype, sdk.OneDec())
// set previous accrual time exit early because interest accumulated will be zero
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
return nil
}
borrowRateSpy := k.getFeeRate(ctx, ctype)
if borrowRateSpy.Equal(sdk.OneDec()) {
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
return nil
}
interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
interestAccumulated := (interestFactor.Mul(totalPrincipalPrior.ToDec())).RoundInt().Sub(totalPrincipalPrior)
if interestAccumulated.IsZero() {
// in the case accumulated interest rounds to zero, exit early without updating accrual time
return nil
}
err := k.MintDebtCoins(ctx, types.ModuleName, k.GetDebtDenom(ctx), sdk.NewCoin(types.DefaultStableDenom, interestAccumulated))
if err != nil {
return err
}
dp, found := k.GetDebtParam(ctx, types.DefaultStableDenom)
if !found {
panic(fmt.Sprintf("Debt parameters for %s not found", types.DefaultStableDenom))
}
newFeesSurplus := interestAccumulated
// mint surplus coins to the liquidator module account.
if newFeesSurplus.IsPositive() {
err := k.supplyKeeper.MintCoins(ctx, types.LiquidatorMacc, sdk.NewCoins(sdk.NewCoin(dp.Denom, newFeesSurplus)))
if err != nil {
return err
}
}
interestFactorNew := interestFactorPrior.Mul(interestFactor)
totalPrincipalNew := totalPrincipalPrior.Add(interestAccumulated)
k.SetTotalPrincipal(ctx, ctype, types.DefaultStableDenom, totalPrincipalNew)
k.SetInterestFactor(ctx, ctype, interestFactorNew)
k.SetPreviousAccrualTime(ctx, ctype, ctx.BlockTime())
return nil
}
// CalculateInterestFactor calculates the simple interest scaling factor,
// which is equal to: (per-second interest rate ** number of seconds elapsed)
// Will return 1.000x, multiply by principal to get new principal with added interest
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
scalingFactorInt := sdk.NewInt(int64(scalingFactor))
// Convert per-second interest rate to a uint scaled by 1e18
interestMantissa := sdk.NewUint(perSecondInterestRate.MulInt(scalingFactorInt).RoundInt().Uint64())
// Convert seconds elapsed to uint (*not scaled*)
secondsElapsedUint := sdk.NewUint(secondsElapsed.Uint64())
// Calculate the interest factor as a uint scaled by 1e18
interestFactorMantissa := sdk.RelativePow(interestMantissa, secondsElapsedUint, scalingFactorUint)
// Convert interest factor to an unscaled sdk.Dec
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
}
// SynchronizeInterest updates the input cdp object to reflect the current accumulated interest, updates the cdp state in the store,
// and returns the updated cdp object
func (k Keeper) SynchronizeInterest(ctx sdk.Context, cdp types.CDP) types.CDP {
globalInterestFactor, found := k.GetInterestFactor(ctx, cdp.Type)
if !found {
k.SetInterestFactor(ctx, cdp.Type, sdk.OneDec())
cdp.InterestFactor = sdk.OneDec()
cdp.FeesUpdated = ctx.BlockTime()
k.SetCDP(ctx, cdp)
return cdp
}
accumulatedInterest := k.CalculateNewInterest(ctx, cdp)
if accumulatedInterest.IsZero() {
// accumulated interest is zero if apy is zero or are if the total fees for all cdps round to zero
prevAccrualTime, found := k.GetPreviousAccrualTime(ctx, cdp.Type)
if !found {
return cdp
}
if cdp.FeesUpdated.Equal(prevAccrualTime) {
// if all fees are rounding to zero, don't update FeesUpdated
return cdp
}
// if apy is zero, we need to update FeesUpdated
cdp.FeesUpdated = ctx.BlockTime()
k.SetCDP(ctx, cdp)
}
cdp.AccumulatedFees = cdp.AccumulatedFees.Add(accumulatedInterest)
cdp.FeesUpdated = ctx.BlockTime()
cdp.InterestFactor = globalInterestFactor
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
return cdp
}
// CalculateNewInterest returns the amount of interest that has accrued to the cdp since its interest was last synchronized
func (k Keeper) CalculateNewInterest(ctx sdk.Context, cdp types.CDP) sdk.Coin {
globalInterestFactor, found := k.GetInterestFactor(ctx, cdp.Type)
if !found {
return sdk.NewCoin(cdp.AccumulatedFees.Denom, sdk.ZeroInt())
}
cdpInterestFactor := globalInterestFactor.Quo(cdp.InterestFactor)
if cdpInterestFactor.Equal(sdk.OneDec()) {
return sdk.NewCoin(cdp.AccumulatedFees.Denom, sdk.ZeroInt())
}
accumulatedInterest := cdp.GetTotalPrincipal().Amount.ToDec().Mul(cdpInterestFactor).RoundInt().Sub(cdp.GetTotalPrincipal().Amount)
return sdk.NewCoin(cdp.AccumulatedFees.Denom, accumulatedInterest)
}
// SynchronizeInterestForRiskyCDPs synchronizes the interest for the slice of cdps with the lowest collateral:debt ratio
func (k Keeper) SynchronizeInterestForRiskyCDPs(ctx sdk.Context, slice sdk.Int, targetRatio sdk.Dec, collateralType string) error {
cdps := k.GetSliceOfCDPsByRatioAndType(ctx, slice, targetRatio, collateralType)
for _, cdp := range cdps {
k.SynchronizeInterest(ctx, cdp)
}
return nil
}

View File

@ -0,0 +1,763 @@
package keeper_test
import (
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp/keeper"
"github.com/kava-labs/kava/x/cdp/types"
)
type InterestTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
}
func (suite *InterestTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates(
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
keeper := tApp.GetCDPKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
}
// createCdps is a helper function to create two CDPs each with zero fees
func (suite *InterestTestSuite) createCdps() {
// create 2 accounts in the state and give them some coins
// create two private key pair addresses
_, addrs := app.GeneratePrivKeyAddressPairs(2)
ak := suite.app.GetAccountKeeper()
// setup the first account
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
acc.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc)
// now setup the second account
acc2 := ak.NewAccountWithAddress(suite.ctx, addrs[1])
acc2.SetCoins(cs(c("xrp", 200000000), c("btc", 500000000)))
ak.SetAccount(suite.ctx, acc2)
// now create two cdps with the addresses we just created
// use the created account to create a cdp that SHOULD have fees updated
// to get a ratio between 100 - 110% of liquidation ratio we can use 200xrp ($50) and 24 usdx (208% collateralization with liquidation ratio of 200%)
// create CDP for the first address
err := suite.keeper.AddCdp(suite.ctx, addrs[0], c("xrp", 200000000), c("usdx", 24000000), "xrp-a")
suite.NoError(err) // check that no error was thrown
// use the other account to create a cdp that SHOULD NOT have fees updated - 500% collateralization
// create CDP for the second address
err = suite.keeper.AddCdp(suite.ctx, addrs[1], c("xrp", 200000000), c("usdx", 10000000), "xrp-a")
suite.NoError(err) // check that no error was thrown
}
func (suite *InterestTestSuite) TestCalculateInterestFactor() {
type args struct {
perSecondInterestRate sdk.Dec
timeElapsed sdk.Int
expectedValue sdk.Dec
}
type test struct {
name string
args args
}
oneYearInSeconds := int64(31536000)
testCases := []test{
{
"1 year",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.191463614477847370"),
},
},
{
"10 year",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds * 10),
expectedValue: sdk.MustNewDecFromStr("5.765113233897391189"),
},
},
{
"1 month",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds / 12),
expectedValue: sdk.MustNewDecFromStr("1.014705619075717373"),
},
},
{
"1 day",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000005555"),
timeElapsed: sdk.NewInt(oneYearInSeconds / 365),
expectedValue: sdk.MustNewDecFromStr("1.000480067194057924"),
},
},
{
"1 year: low interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.017656545925063632"),
},
},
{
"1 year, lower interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000055"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.001735985079841390"),
},
},
{
"1 year, lowest interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000000005"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("1.000157692432076670"),
},
},
{
"1 year: high interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000055555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("5.766022095987868825"),
},
},
{
"1 year: higher interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000000555555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("40628388.864535408465693310"),
},
},
// If we raise the per second interest rate too much we'll cause an integer overflow.
// For example, perSecondInterestRate: '1.000005555555' will cause a panic.
{
"1 year: highest interest rate",
args{
perSecondInterestRate: sdk.MustNewDecFromStr("1.000001555555"),
timeElapsed: sdk.NewInt(oneYearInSeconds),
expectedValue: sdk.MustNewDecFromStr("2017093013158200407564.613502861572552603"),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
interestFactor := keeper.CalculateInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
suite.Require().Equal(tc.args.expectedValue, interestFactor)
})
}
}
func (suite *InterestTestSuite) TestAccumulateInterest() {
type args struct {
ctype string
initialTime time.Time
totalPrincipal sdk.Int
timeElapsed int
expectedTotalPrincipal sdk.Int
expectedLastAccrualTime time.Time
}
type test struct {
name string
args args
}
oneYearInSeconds := 31536000
testCases := []test{
{
"1 year",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(100000000000000),
timeElapsed: oneYearInSeconds,
expectedTotalPrincipal: sdk.NewInt(105000000000012),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * oneYearInSeconds)),
},
},
{
"1 year - zero principal",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.ZeroInt(),
timeElapsed: oneYearInSeconds,
expectedTotalPrincipal: sdk.ZeroInt(),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * oneYearInSeconds)),
},
},
{
"1 month",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(100000000000000),
timeElapsed: 86400 * 30,
expectedTotalPrincipal: sdk.NewInt(100401820189198),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 86400 * 30)),
},
},
{
"1 month - interest rounds to zero",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(10),
timeElapsed: 86400 * 30,
expectedTotalPrincipal: sdk.NewInt(10),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
},
},
{
"7 seconds",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(100000000000000),
timeElapsed: 7,
expectedTotalPrincipal: sdk.NewInt(100000001082988),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7)),
},
},
{
"7 seconds - interest rounds to zero",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(30000000),
timeElapsed: 7,
expectedTotalPrincipal: sdk.NewInt(30000000),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
},
},
{
"7 seconds - zero interest",
args{
ctype: "busd-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
totalPrincipal: sdk.NewInt(100000000000000),
timeElapsed: 7,
expectedTotalPrincipal: sdk.NewInt(100000000000000),
expectedLastAccrualTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7)),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
suite.keeper.SetTotalPrincipal(suite.ctx, tc.args.ctype, types.DefaultStableDenom, tc.args.totalPrincipal)
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err := suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype)
suite.Require().NoError(err)
actualTotalPrincipal := suite.keeper.GetTotalPrincipal(suite.ctx, tc.args.ctype, types.DefaultStableDenom)
suite.Require().Equal(tc.args.expectedTotalPrincipal, actualTotalPrincipal)
actualAccrualTime, _ := suite.keeper.GetPreviousAccrualTime(suite.ctx, tc.args.ctype)
suite.Require().Equal(tc.args.expectedLastAccrualTime, actualAccrualTime)
})
}
}
// TestSynchronizeInterest tests the functionality of synchronizing the accumulated interest for CDPs
func (suite *InterestTestSuite) TestSynchronizeInterest() {
type args struct {
ctype string
initialTime time.Time
initialCollateral sdk.Coin
initialPrincipal sdk.Coin
timeElapsed int
expectedFees sdk.Coin
expectedFeesUpdatedTime time.Time
}
type test struct {
name string
args args
}
oneYearInSeconds := 31536000
testCases := []test{
{
"1 year",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: oneYearInSeconds,
expectedFees: c("usdx", 5000000000),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * oneYearInSeconds)),
},
},
{
"1 month",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: 86400 * 30,
expectedFees: c("usdx", 401820189),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 86400 * 30)),
},
},
{
"7 seconds",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: 7,
expectedFees: c("usdx", 1083),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7)),
},
},
{
"7 seconds - zero apy",
args{
ctype: "busd-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("busd", 10000000000000),
initialPrincipal: c("usdx", 10000000000),
timeElapsed: 7,
expectedFees: c("usdx", 0),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7)),
},
},
{
"7 seconds - fees round to zero",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000),
initialPrincipal: c("usdx", 10000000),
timeElapsed: 7,
expectedFees: c("usdx", 0),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup account state
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ak := suite.app.GetAccountKeeper()
// setup the first account
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
ak.SetAccount(suite.ctx, acc)
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, types.ModuleName, cs(tc.args.initialCollateral))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, addrs[0], cs(tc.args.initialCollateral))
suite.Require().NoError(err)
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
pk.SetPrice(suite.ctx, sdk.AccAddress{}, "bnb:usd", d("17.25"), tc.args.expectedFeesUpdatedTime.Add(time.Second))
pk.SetPrice(suite.ctx, sdk.AccAddress{}, "busd:usd", d("1"), tc.args.expectedFeesUpdatedTime.Add(time.Second))
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype)
suite.Require().NoError(err)
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err = suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype)
suite.Require().NoError(err)
cdp, found := suite.keeper.GetCDP(suite.ctx, tc.args.ctype, 1)
suite.Require().True(found)
cdp = suite.keeper.SynchronizeInterest(suite.ctx, cdp)
suite.Require().Equal(tc.args.expectedFees, cdp.AccumulatedFees)
suite.Require().Equal(tc.args.expectedFeesUpdatedTime, cdp.FeesUpdated)
})
}
}
func (suite *InterestTestSuite) TestMultipleCDPInterest() {
type args struct {
ctype string
initialTime time.Time
blockInterval int
numberOfBlocks int
initialCDPCollateral sdk.Coin
initialCDPPrincipal sdk.Coin
numberOfCdps int
expectedFeesPerCDP sdk.Coin
expectedTotalPrincipalPerCDP sdk.Coin
expectedFeesUpdatedTime time.Time
expectedTotalPrincipal sdk.Int
expectedDebtBalance sdk.Int
expectedStableBalance sdk.Int
expectedSumOfCDPPrincipal sdk.Int
}
type test struct {
name string
args args
}
testCases := []test{
{
"1 block",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
blockInterval: 7,
numberOfBlocks: 1,
initialCDPCollateral: c("bnb", 10000000000),
initialCDPPrincipal: c("usdx", 500000000),
numberOfCdps: 100,
expectedFeesPerCDP: c("usdx", 5),
expectedTotalPrincipalPerCDP: c("usdx", 500000005),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7)),
expectedTotalPrincipal: i(50000000541),
expectedDebtBalance: i(50000000541),
expectedStableBalance: i(50000000541),
expectedSumOfCDPPrincipal: i(50000000500),
},
},
{
"100 blocks",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
blockInterval: 7,
numberOfBlocks: 100,
initialCDPCollateral: c("bnb", 10000000000),
initialCDPPrincipal: c("usdx", 500000000),
numberOfCdps: 100,
expectedFeesPerCDP: c("usdx", 541),
expectedTotalPrincipalPerCDP: c("usdx", 500000541),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7 * 100)),
expectedTotalPrincipal: i(50000054100),
expectedDebtBalance: i(50000054100),
expectedStableBalance: i(50000054100),
expectedSumOfCDPPrincipal: i(50000054100),
},
},
{
"10000 blocks",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
blockInterval: 7,
numberOfBlocks: 10000,
initialCDPCollateral: c("bnb", 10000000000),
initialCDPPrincipal: c("usdx", 500000000),
numberOfCdps: 100,
expectedFeesPerCDP: c("usdx", 54152),
expectedTotalPrincipalPerCDP: c("usdx", 500054152),
expectedFeesUpdatedTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC).Add(time.Duration(int(time.Second) * 7 * 10000)),
expectedTotalPrincipal: i(50005418990),
expectedDebtBalance: i(50005418990),
expectedStableBalance: i(50005418990),
expectedSumOfCDPPrincipal: i(50005415200),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
pk.SetPrice(suite.ctx, sdk.AccAddress{}, "bnb:usd", d("17.25"), tc.args.expectedFeesUpdatedTime.Add(time.Second))
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
// setup account state
_, addrs := app.GeneratePrivKeyAddressPairs(tc.args.numberOfCdps)
for j := 0; j < tc.args.numberOfCdps; j++ {
ak := suite.app.GetAccountKeeper()
// setup the first account
acc := ak.NewAccountWithAddress(suite.ctx, addrs[j])
ak.SetAccount(suite.ctx, acc)
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, types.ModuleName, cs(tc.args.initialCDPCollateral))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, addrs[j], cs(tc.args.initialCDPCollateral))
suite.Require().NoError(err)
err = suite.keeper.AddCdp(suite.ctx, addrs[j], tc.args.initialCDPCollateral, tc.args.initialCDPPrincipal, tc.args.ctype)
suite.Require().NoError(err)
}
// run a number of blocks where CDPs are not synchronized
for j := 0; j < tc.args.numberOfBlocks; j++ {
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.blockInterval))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err := suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype)
suite.Require().NoError(err)
}
sk := suite.app.GetSupplyKeeper()
supplyTotal := sk.GetSupply(suite.ctx).GetTotal()
debtSupply := supplyTotal.AmountOf(types.DefaultDebtDenom)
usdxSupply := supplyTotal.AmountOf(types.DefaultStableDenom)
totalPrincipal := suite.keeper.GetTotalPrincipal(suite.ctx, tc.args.ctype, types.DefaultStableDenom)
suite.Require().Equal(tc.args.expectedDebtBalance, debtSupply)
suite.Require().Equal(tc.args.expectedStableBalance, usdxSupply)
suite.Require().Equal(tc.args.expectedTotalPrincipal, totalPrincipal)
sumOfCDPPrincipal := sdk.ZeroInt()
for j := 0; j < tc.args.numberOfCdps; j++ {
cdp, found := suite.keeper.GetCDP(suite.ctx, tc.args.ctype, uint64(j+1))
suite.Require().True(found)
cdp = suite.keeper.SynchronizeInterest(suite.ctx, cdp)
suite.Require().Equal(tc.args.expectedFeesPerCDP, cdp.AccumulatedFees)
suite.Require().Equal(tc.args.expectedTotalPrincipalPerCDP, cdp.GetTotalPrincipal())
suite.Require().Equal(tc.args.expectedFeesUpdatedTime, cdp.FeesUpdated)
sumOfCDPPrincipal = sumOfCDPPrincipal.Add(cdp.GetTotalPrincipal().Amount)
}
suite.Require().Equal(tc.args.expectedSumOfCDPPrincipal, sumOfCDPPrincipal)
})
}
}
// TestSynchronizeInterest tests the functionality of synchronizing the accumulated interest for CDPs
func (suite *InterestTestSuite) TestCalculateCDPInterest() {
type args struct {
ctype string
initialTime time.Time
initialCollateral sdk.Coin
initialPrincipal sdk.Coin
timeElapsed int
expectedFees sdk.Coin
}
type test struct {
name string
args args
}
oneYearInSeconds := 31536000
testCases := []test{
{
"1 year",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: oneYearInSeconds,
expectedFees: c("usdx", 5000000000),
},
},
{
"1 month",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: 86400 * 30,
expectedFees: c("usdx", 401820189),
},
},
{
"7 seconds",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 100000000000),
timeElapsed: 7,
expectedFees: c("usdx", 1083),
},
},
{
"7 seconds - fees round to zero",
args{
ctype: "bnb-a",
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000),
initialPrincipal: c("usdx", 10000000),
timeElapsed: 7,
expectedFees: c("usdx", 0),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup account state
_, addrs := app.GeneratePrivKeyAddressPairs(1)
ak := suite.app.GetAccountKeeper()
// setup the first account
acc := ak.NewAccountWithAddress(suite.ctx, addrs[0])
ak.SetAccount(suite.ctx, acc)
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, types.ModuleName, cs(tc.args.initialCollateral))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, addrs[0], cs(tc.args.initialCollateral))
suite.Require().NoError(err)
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
pk.SetPrice(suite.ctx, sdk.AccAddress{}, "bnb:usd", d("17.25"), tc.args.initialTime.Add(time.Duration(int(time.Second)*tc.args.timeElapsed)))
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
err = suite.keeper.AddCdp(suite.ctx, addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype)
suite.Require().NoError(err)
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err = suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype)
suite.Require().NoError(err)
cdp, found := suite.keeper.GetCDP(suite.ctx, tc.args.ctype, 1)
suite.Require().True(found)
newInterest := suite.keeper.CalculateNewInterest(suite.ctx, cdp)
suite.Require().Equal(tc.args.expectedFees, newInterest)
})
}
}
func (suite *InterestTestSuite) TestSyncInterestForRiskyCDPs() {
type args struct {
ctype string
numberCdps int
slice int
initialCollateral sdk.Coin
minPrincipal sdk.Coin
principalIncrement sdk.Coin
initialTime time.Time
timeElapsed int
expectedCDPs int
}
type test struct {
name string
args args
}
oneYearInSeconds := 31536000
testCases := []test{
{
"1 year",
args{
ctype: "bnb-a",
numberCdps: 20,
slice: 10,
initialCollateral: c("bnb", 100000000000),
minPrincipal: c("usdx", 100000000),
principalIncrement: c("usdx", 10000000),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
timeElapsed: oneYearInSeconds,
expectedCDPs: 10,
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup account state
_, addrs := app.GeneratePrivKeyAddressPairs(tc.args.numberCdps)
ak := suite.app.GetAccountKeeper()
sk := suite.app.GetSupplyKeeper()
for _, addr := range addrs {
acc := ak.NewAccountWithAddress(suite.ctx, addr)
ak.SetAccount(suite.ctx, acc)
err := sk.MintCoins(suite.ctx, types.ModuleName, cs(tc.args.initialCollateral))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, types.ModuleName, addr, cs(tc.args.initialCollateral))
suite.Require().NoError(err)
}
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
pk.SetPrice(suite.ctx, sdk.AccAddress{}, "bnb:usd", d("20.0"), tc.args.initialTime.Add(time.Duration(int(time.Second)*tc.args.timeElapsed)))
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
for j, addr := range addrs {
initialPrincipal := tc.args.minPrincipal.Add(c("usdx", int64(j)*tc.args.principalIncrement.Amount.Int64()))
err := suite.keeper.AddCdp(suite.ctx, addr, tc.args.initialCollateral, initialPrincipal, tc.args.ctype)
suite.Require().NoError(err)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
err := suite.keeper.AccumulateInterest(suite.ctx, tc.args.ctype)
suite.Require().NoError(err)
err = suite.keeper.SynchronizeInterestForRiskyCDPs(suite.ctx, i(int64(tc.args.slice)), sdk.MaxSortableDec, tc.args.ctype)
suite.Require().NoError(err)
cdpsUpdatedCount := 0
for _, addr := range addrs {
cdp, found := suite.keeper.GetCdpByOwnerAndCollateralType(suite.ctx, addr, tc.args.ctype)
suite.Require().True(found)
if cdp.FeesUpdated.Equal(suite.ctx.BlockTime()) {
cdpsUpdatedCount += 1
}
}
suite.Require().Equal(tc.args.expectedCDPs, cdpsUpdatedCount)
})
}
}
func TestInterestTestSuite(t *testing.T) {
suite.Run(t, new(InterestTestSuite))
}

View File

@ -2,6 +2,7 @@ package keeper
import (
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix"
@ -20,6 +21,7 @@ type Keeper struct {
supplyKeeper types.SupplyKeeper
auctionKeeper types.AuctionKeeper
accountKeeper types.AccountKeeper
hooks types.CDPHooks
maccPerms map[string][]string
}
@ -38,10 +40,20 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
auctionKeeper: ak,
supplyKeeper: sk,
accountKeeper: ack,
hooks: nil,
maccPerms: maccs,
}
}
// SetHooks sets the cdp keeper hooks
func (k *Keeper) SetHooks(hooks types.CDPHooks) *Keeper {
if k.hooks != nil {
panic("cannot set validator hooks twice")
}
k.hooks = hooks
return k
}
// CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, collateralType string) sdk.Iterator {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix)
@ -111,22 +123,105 @@ func (k Keeper) IterateCdpsByCollateralRatio(ctx sdk.Context, collateralType str
}
}
// SetSavingsRateDistributed sets the SavingsRateDistributed in the store
func (k Keeper) SetSavingsRateDistributed(ctx sdk.Context, totalDistributed sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey)
bz := k.cdc.MustMarshalBinaryLengthPrefixed(totalDistributed)
store.Set([]byte{}, bz)
// GetSliceOfCDPsByRatioAndType returns a slice of cdps of size equal to the input cutoffCount
// sorted by target ratio in ascending order (ie, the lowest collateral:debt ratio cdps are returned first)
func (k Keeper) GetSliceOfCDPsByRatioAndType(ctx sdk.Context, cutoffCount sdk.Int, targetRatio sdk.Dec, collateralType string) (cdps types.CDPs) {
count := sdk.ZeroInt()
k.IterateCdpsByCollateralRatio(ctx, collateralType, targetRatio, func(cdp types.CDP) bool {
cdps = append(cdps, cdp)
count = count.Add(sdk.OneInt())
if count.GTE(cutoffCount) {
return true
}
return false
})
return cdps
}
// GetSavingsRateDistributed gets the SavingsRateDistributed from the store
func (k Keeper) GetSavingsRateDistributed(ctx sdk.Context) sdk.Int {
savingsRateDistributed := sdk.ZeroInt()
store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey)
bz := store.Get([]byte{})
// GetPreviousAccrualTime returns the last time an individual market accrued interest
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, ctype string) (time.Time, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
bz := store.Get([]byte(ctype))
if bz == nil {
return savingsRateDistributed
return time.Time{}, false
}
var previousAccrualTime time.Time
k.cdc.MustUnmarshalBinaryBare(bz, &previousAccrualTime)
return previousAccrualTime, true
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &savingsRateDistributed)
return savingsRateDistributed
// SetPreviousAccrualTime sets the most recent accrual time for a particular market
func (k Keeper) SetPreviousAccrualTime(ctx sdk.Context, ctype string, previousAccrualTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
bz := k.cdc.MustMarshalBinaryBare(previousAccrualTime)
store.Set([]byte(ctype), bz)
}
// GetInterestFactor returns the current interest factor for an individual collateral type
func (k Keeper) GetInterestFactor(ctx sdk.Context, ctype string) (sdk.Dec, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
bz := store.Get([]byte(ctype))
if bz == nil {
return sdk.ZeroDec(), false
}
var interestFactor sdk.Dec
k.cdc.MustUnmarshalBinaryBare(bz, &interestFactor)
return interestFactor, true
}
// SetInterestFactor sets the current interest factor for an individual collateral type
func (k Keeper) SetInterestFactor(ctx sdk.Context, ctype string, interestFactor sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
bz := k.cdc.MustMarshalBinaryBare(interestFactor)
store.Set([]byte(ctype), bz)
}
// IncrementTotalPrincipal increments the total amount of debt that has been drawn with that collateral type
func (k Keeper) IncrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom)
total = total.Add(principal.Amount)
k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total)
}
// DecrementTotalPrincipal decrements the total amount of debt that has been drawn for a particular collateral type
func (k Keeper) DecrementTotalPrincipal(ctx sdk.Context, collateralType string, principal sdk.Coin) {
total := k.GetTotalPrincipal(ctx, collateralType, principal.Denom)
// NOTE: negative total principal can happen in tests due to rounding errors
// in fee calculation
total = sdk.MaxInt(total.Sub(principal.Amount), sdk.ZeroInt())
k.SetTotalPrincipal(ctx, collateralType, principal.Denom, total)
}
// GetTotalPrincipal returns the total amount of principal that has been drawn for a particular collateral
func (k Keeper) GetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string) (total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
bz := store.Get([]byte(collateralType + principalDenom))
if bz == nil {
k.SetTotalPrincipal(ctx, collateralType, principalDenom, sdk.ZeroInt())
return sdk.ZeroInt()
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &total)
return total
}
// SetTotalPrincipal sets the total amount of principal that has been drawn for the input collateral
func (k Keeper) SetTotalPrincipal(ctx sdk.Context, collateralType, principalDenom string, total sdk.Int) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PrincipalKeyPrefix)
_, found := k.GetCollateralTypePrefix(ctx, collateralType)
if !found {
panic(fmt.Sprintf("collateral not found: %s", collateralType))
}
store.Set([]byte(collateralType+principalDenom), k.cdc.MustMarshalBinaryLengthPrefixed(total))
}
// getModuleAccountCoins gets the total coin balance of this coin currently held by module accounts
func (k Keeper) getModuleAccountCoins(ctx sdk.Context, denom string) sdk.Coins {
totalModCoinBalance := sdk.NewCoins(sdk.NewCoin(denom, sdk.ZeroInt()))
for macc := range k.maccPerms {
modCoinBalance := k.supplyKeeper.GetModuleAccount(ctx, macc).GetCoins().AmountOf(denom)
if modCoinBalance.IsPositive() {
totalModCoinBalance = totalModCoinBalance.Add(sdk.NewCoin(denom, modCoinBalance))
}
}
return totalModCoinBalance
}

View File

@ -34,15 +34,3 @@ func (suite *KeeperTestSuite) ResetChain() {
suite.ctx = ctx
suite.keeper = keeper
}
func (suite *KeeperTestSuite) TestGetSetSavingsRateDistributed() {
suite.ResetChain()
// Set savings rate distributed value
savingsRateDist := sdk.NewInt(555000555000)
suite.keeper.SetSavingsRateDistributed(suite.ctx, savingsRateDist)
// Check store's savings rate distributed value
s := suite.keeper.GetSavingsRateDistributed(suite.ctx)
suite.Equal(savingsRateDist, s)
}

View File

@ -1,8 +1,6 @@
package keeper
import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/client"
@ -32,10 +30,6 @@ func NewQuerier(keeper Keeper) sdk.Querier {
return queryGetParams(ctx, req, keeper)
case types.QueryGetAccounts:
return queryGetAccounts(ctx, req, keeper)
case types.QueryGetSavingsRateDistributed:
return queryGetSavingsRateDistributed(ctx, req, keeper)
case types.QueryGetPreviousSavingsDistributionTime:
return queryGetPreviousSavingsDistributionTime(ctx, req, keeper)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint %s", types.ModuleName, path[0])
}
@ -172,12 +166,10 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]by
func queryGetAccounts(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
cdpAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.ModuleName)
liquidatorAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc)
savingsRateAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc)
accounts := []supply.ModuleAccount{
*cdpAccAccount.(*supply.ModuleAccount),
*liquidatorAccAccount.(*supply.ModuleAccount),
*savingsRateAccAccount.(*supply.ModuleAccount),
}
// Encode results
@ -188,36 +180,6 @@ func queryGetAccounts(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]
return bz, nil
}
// query get savings rate distributed in the cdp store
func queryGetSavingsRateDistributed(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get savings rate distributed
savingsRateDist := keeper.GetSavingsRateDistributed(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, savingsRateDist)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query get savings rate distributed in the cdp store
func queryGetPreviousSavingsDistributionTime(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
// Get savings rate distributed
savingsRateDistTime, found := keeper.GetPreviousSavingsDistribution(ctx)
if !found {
return nil, fmt.Errorf("previous distribution time not found")
}
// Encode results
bz, err := codec.MarshalJSONIndent(types.ModuleCdc, savingsRateDistTime)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query cdps in store and filter by request params
func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryCdpsParams

View File

@ -287,7 +287,7 @@ func (suite *QuerierTestSuite) TestQueryAccounts() {
var accounts []supply.ModuleAccount
suite.Require().Nil(supply.ModuleCdc.UnmarshalJSON(bz, &accounts))
suite.Require().Equal(3, len(accounts))
suite.Require().Equal(2, len(accounts))
findByName := func(name string) bool {
for _, account := range accounts {
@ -300,18 +300,6 @@ func (suite *QuerierTestSuite) TestQueryAccounts() {
suite.Require().True(findByName("cdp"))
suite.Require().True(findByName("liquidator"))
suite.Require().True(findByName("savings"))
}
func (suite *QuerierTestSuite) TestQuerySavingsRateDistributed() {
ctx := suite.ctx.WithIsCheckTx(false)
bz, err := suite.querier(ctx, []string{types.QueryGetSavingsRateDistributed}, abci.RequestQuery{})
suite.Nil(err)
suite.NotNil(bz)
var distAmount sdk.Int
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &distAmount))
suite.True(sdk.ZeroInt().Equal(distAmount))
}
func (suite *QuerierTestSuite) TestFindIntersection() {

View File

@ -1,106 +0,0 @@
package keeper
import (
"time"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
"github.com/kava-labs/kava/x/cdp/types"
)
// DistributeSavingsRate distributes surplus that has accumulated in the liquidator account to address holding stable coins according the the savings rate
func (k Keeper) DistributeSavingsRate(ctx sdk.Context, debtDenom string) error {
dp, found := k.GetDebtParam(ctx, debtDenom)
if !found {
return sdkerrors.Wrap(types.ErrDebtNotSupported, debtDenom)
}
savingsRateMacc := k.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc)
surplusToDistribute := savingsRateMacc.GetCoins().AmountOf(dp.Denom)
if surplusToDistribute.IsZero() {
return nil
}
modAccountCoins := k.getModuleAccountCoins(ctx, dp.Denom)
totalSupplyLessModAccounts := k.supplyKeeper.GetSupply(ctx).GetTotal().Sub(modAccountCoins)
// values to use in interest calculation
totalSurplus := sdk.NewDecFromInt(surplusToDistribute)
totalSupply := sdk.NewDecFromInt(totalSupplyLessModAccounts.AmountOf(debtDenom))
var iterationErr error
// TODO: avoid iterating over all the accounts by keeping the stored stable coin
// holders' addresses separately.
k.accountKeeper.IterateAccounts(ctx, func(acc authexported.Account) (stop bool) {
_, ok := acc.(supplyexported.ModuleAccountI)
if ok {
// don't distribute savings rate to module accounts
return false
}
debtAmount := acc.GetCoins().AmountOf(debtDenom)
if !debtAmount.IsPositive() {
return false
}
// (balance * rewardToDisribute) / totalSupply
// interest is the ratable fraction of savings rate owed to that account, rounded using bankers rounding
interest := (sdk.NewDecFromInt(debtAmount).Mul(totalSurplus)).Quo(totalSupply).RoundInt()
// sanity check, if we are going to over-distribute due to rounding, distribute only the remaining savings rate that hasn't been distributed.
interest = sdk.MinInt(interest, surplusToDistribute)
// sanity check - don't send saving rate if the rounded amount is zero
if !interest.IsPositive() {
return false
}
// update total savings rate distributed by surplus to distribute
previousSavingsDistributed := k.GetSavingsRateDistributed(ctx)
newTotalDistributed := previousSavingsDistributed.Add(interest)
k.SetSavingsRateDistributed(ctx, newTotalDistributed)
interestCoins := sdk.NewCoins(sdk.NewCoin(debtDenom, interest))
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.SavingsRateMacc, acc.GetAddress(), interestCoins)
if err != nil {
iterationErr = err
return true
}
surplusToDistribute = surplusToDistribute.Sub(interest)
return false
})
return iterationErr
}
// GetPreviousSavingsDistribution get the time of the previous savings rate distribution
func (k Keeper) GetPreviousSavingsDistribution(ctx sdk.Context) (distTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey)
b := store.Get([]byte{})
if b == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &distTime)
return distTime, true
}
// SetPreviousSavingsDistribution set the time of the previous block
func (k Keeper) SetPreviousSavingsDistribution(ctx sdk.Context, distTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousDistributionTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryLengthPrefixed(distTime))
}
// getModuleAccountCoins gets the total coin balance of this coin currently held by module accounts
func (k Keeper) getModuleAccountCoins(ctx sdk.Context, denom string) sdk.Coins {
totalModCoinBalance := sdk.NewCoins(sdk.NewCoin(denom, sdk.ZeroInt()))
for macc := range k.maccPerms {
modCoinBalance := k.supplyKeeper.GetModuleAccount(ctx, macc).GetCoins().AmountOf(denom)
if modCoinBalance.IsPositive() {
totalModCoinBalance = totalModCoinBalance.Add(sdk.NewCoin(denom, modCoinBalance))
}
}
return totalModCoinBalance
}

View File

@ -1,113 +0,0 @@
package keeper_test
import (
"testing"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp/keeper"
"github.com/kava-labs/kava/x/cdp/types"
)
type SavingsTestSuite struct {
suite.Suite
keeper keeper.Keeper
app app.TestApp
ctx sdk.Context
addrs []sdk.AccAddress
amountToDistribute int64
}
func (suite *SavingsTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
_, addrs := app.GeneratePrivKeyAddressPairs(3)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("usdx", 100000)), cs(c("usdx", 50000)), cs(c("usdx", 50000)),
},
)
tApp.InitializeFromGenesisStates(
authGS,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
sk := tApp.GetSupplyKeeper()
macc := sk.GetModuleAccount(ctx, types.SavingsRateMacc)
distAmount := int64(10000)
err := sk.MintCoins(ctx, macc.GetName(), cs(c("usdx", distAmount)))
suite.NoError(err)
keeper := tApp.GetCDPKeeper()
suite.app = tApp
suite.keeper = keeper
suite.ctx = ctx
suite.addrs = addrs
suite.amountToDistribute = distAmount
}
func (suite *SavingsTestSuite) TestApplySavingsRate() {
preSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx)
err := suite.keeper.DistributeSavingsRate(suite.ctx, "usdx")
suite.NoError(err)
ak := suite.app.GetAccountKeeper()
acc0 := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Equal(cs(c("usdx", 105000)), acc0.GetCoins())
acc1 := ak.GetAccount(suite.ctx, suite.addrs[1])
suite.Equal(cs(c("usdx", 52500)), acc1.GetCoins())
acc2 := ak.GetAccount(suite.ctx, suite.addrs[2])
suite.Equal(cs(c("usdx", 52500)), acc2.GetCoins())
sk := suite.app.GetSupplyKeeper()
macc := sk.GetModuleAccount(suite.ctx, types.SavingsRateMacc)
suite.True(macc.GetCoins().AmountOf("usdx").IsZero())
expectedPostSavingsRateDistAmount := preSavingsRateDistAmount.Add(sdk.NewInt(suite.amountToDistribute))
postSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx)
suite.True(expectedPostSavingsRateDistAmount.Equal(postSavingsRateDistAmount))
}
func (suite *SavingsTestSuite) TestGetSetPreviousDistributionTime() {
now := tmtime.Now()
_, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx)
suite.Require().False(f) // distr time not set at genesis when the default genesis is used
suite.NotPanics(func() { suite.keeper.SetPreviousSavingsDistribution(suite.ctx, now) })
pdt, f := suite.keeper.GetPreviousSavingsDistribution(suite.ctx)
suite.True(f)
suite.Equal(now, pdt)
}
func (suite *SavingsTestSuite) TestGetSetSavingsRateDistributed() {
// Savings rate dist set to 0 when the default genesis is used
preSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx)
suite.True(preSavingsRateDistAmount.Equal(types.DefaultSavingsRateDistributed))
// Adding new dist amount to existing dist so default genesis value can be updated in the future
amountToDistribute := sdk.NewInt(9876543210)
newTotalDistributed := preSavingsRateDistAmount.Add(amountToDistribute)
suite.NotPanics(func() { suite.keeper.SetSavingsRateDistributed(suite.ctx, newTotalDistributed) })
postSavingsRateDistAmount := suite.keeper.GetSavingsRateDistributed(suite.ctx)
suite.Equal(newTotalDistributed, postSavingsRateDistAmount)
}
func TestSavingsTestSuite(t *testing.T) {
suite.Run(t, new(SavingsTestSuite))
}

View File

@ -4,10 +4,33 @@ import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types"
)
// AttemptKeeperLiquidation liquidates the cdp with the input collateral type and owner if it is below the required collateralization ratio
// if the cdp is liquidated, the keeper that sent the transaction is rewarded a percentage of the collateral according to that collateral types'
// keeper reward percentage.
func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper, owner sdk.AccAddress, collateralType string) error {
cdp, found := k.GetCdpByOwnerAndCollateralType(ctx, owner, collateralType)
if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, denom %s", owner, collateralType)
}
k.hooks.BeforeCDPModified(ctx, cdp)
cdp = k.SynchronizeInterest(ctx, cdp)
err := k.ValidateLiquidation(ctx, cdp.Collateral, cdp.Type, cdp.Principal, cdp.AccumulatedFees)
if err != nil {
return err
}
cdp, err = k.payoutKeeperLiquidationReward(ctx, keeper, cdp)
if err != nil {
return err
}
return k.SeizeCollateral(ctx, cdp)
}
// SeizeCollateral liquidates the collateral in the input cdp.
// the following operations are performed:
// 1. Collateral for all deposits is sent from the cdp module to the liquidator module account
@ -79,6 +102,7 @@ func (k Keeper) LiquidateCdps(ctx sdk.Context, marketID string, collateralType s
normalizedRatio := sdk.OneDec().Quo(priceDivLiqRatio)
cdpsToLiquidate := k.GetAllCdpsByCollateralTypeAndRatio(ctx, collateralType, normalizedRatio)
for _, c := range cdpsToLiquidate {
k.hooks.BeforeCDPModified(ctx, c)
err := k.SeizeCollateral(ctx, c)
if err != nil {
return err
@ -93,7 +117,53 @@ func (k Keeper) ApplyLiquidationPenalty(ctx sdk.Context, collateralType string,
return sdk.NewDecFromInt(debt).Mul(penalty).RoundInt()
}
// ValidateLiquidation validate that adding the input principal puts the cdp below the liquidation ratio
func (k Keeper) ValidateLiquidation(ctx sdk.Context, collateral sdk.Coin, collateralType string, principal sdk.Coin, fees sdk.Coin) error {
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, collateral, collateralType, principal, fees, spot)
if err != nil {
return err
}
liquidationRatio := k.getLiquidationRatio(ctx, collateralType)
if collateralizationRatio.GT(liquidationRatio) {
return sdkerrors.Wrapf(types.ErrNotLiquidatable, "collateral %s, collateral ratio %s, liquidation ratio %s", collateral.Denom, collateralizationRatio, liquidationRatio)
}
return nil
}
func (k Keeper) getModAccountDebt(ctx sdk.Context, accountName string) sdk.Int {
macc := k.supplyKeeper.GetModuleAccount(ctx, accountName)
return macc.GetCoins().AmountOf(k.GetDebtDenom(ctx))
}
func (k Keeper) payoutKeeperLiquidationReward(ctx sdk.Context, keeper sdk.AccAddress, cdp types.CDP) (types.CDP, error) {
collateralParam, found := k.GetCollateral(ctx, cdp.Type)
if !found {
return types.CDP{}, sdkerrors.Wrapf(types.ErrInvalidCollateral, "%s", cdp.Type)
}
reward := cdp.Collateral.Amount.ToDec().Mul(collateralParam.KeeperRewardPercentage).RoundInt()
rewardCoin := sdk.NewCoin(cdp.Collateral.Denom, reward)
paidReward := false
deposits := k.GetDeposits(ctx, cdp.ID)
for _, dep := range deposits {
if dep.Amount.IsGTE(rewardCoin) {
dep.Amount = dep.Amount.Sub(rewardCoin)
k.SetDeposit(ctx, dep)
paidReward = true
break
}
}
if !paidReward {
return cdp, nil
}
err := k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, keeper, sdk.NewCoins(rewardCoin))
if err != nil {
return types.CDP{}, err
}
cdp.Collateral = cdp.Collateral.Sub(rewardCoin)
ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
err = k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, ratio)
if err != nil {
return types.CDP{}, err
}
return cdp, nil
}

View File

@ -3,6 +3,7 @@ package keeper_test
import (
"errors"
"math/rand"
"strings"
"testing"
"time"
@ -205,6 +206,258 @@ func (suite *SeizeTestSuite) TestApplyLiquidationPenalty() {
suite.Panics(func() { suite.keeper.ApplyLiquidationPenalty(suite.ctx, "lol-a", i(1000)) })
}
func (suite *SeizeTestSuite) TestKeeperLiquidation() {
type args struct {
ctype string
blockTime time.Time
initialPrice sdk.Dec
finalPrice sdk.Dec
collateral sdk.Coin
principal sdk.Coin
expectedKeeperCoins sdk.Coins // additional coins (if any) the borrower address should have after successfully liquidating position
expectedAuctions auction.Auctions // the auctions we should expect to find have been started
}
type errArgs struct {
expectLiquidate bool
contains string
}
type test struct {
name string
args args
errArgs errArgs
}
// Set up auction constants
layout := "2006-01-02T15:04:05.000Z"
endTimeStr := "9000-01-01T00:00:00.000Z"
endTime, _ := time.Parse(layout, endTimeStr)
addr, _ := sdk.AccAddressFromBech32("kava1ze7y9qwdddejmy7jlw4cymqqlt2wh05yhwmrv2")
testCases := []test{
{
"valid liquidation",
args{
"btc-a",
time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
d("20000.00"),
d("19000.0"),
c("btc", 10000000),
c("usdx", 1333330000),
cs(c("btc", 100100000), c("xrp", 10000000000)),
auction.Auctions{
auction.CollateralAuction{
BaseAuction: auction.BaseAuction{
ID: 1,
Initiator: "liquidator",
Lot: c("btc", 9900000),
Bidder: nil,
Bid: c("usdx", 0),
HasReceivedBids: false,
EndTime: endTime,
MaxEndTime: endTime,
},
CorrespondingDebt: c("debt", 1333330000),
MaxBid: c("usdx", 1366663250),
LotReturns: auction.WeightedAddresses{[]sdk.AccAddress{addr}, []sdk.Int{sdk.NewInt(9900000)}},
},
},
},
errArgs{
true,
"",
},
},
{
"invalid - not below collateralization ratio",
args{
"btc-a",
time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
d("20000.00"),
d("21000.0"),
c("btc", 10000000),
c("usdx", 1333330000),
cs(),
auction.Auctions{},
},
errArgs{
false,
"collateral ratio not below liquidation ratio",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
_, err := pk.SetPrice(suite.ctx, sdk.AccAddress{}, "btc:usd", tc.args.initialPrice, suite.ctx.BlockTime().Add(time.Hour*24))
suite.Require().NoError(err)
err = pk.SetCurrentPrices(suite.ctx, "btc:usd")
suite.Require().NoError(err)
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
err = suite.keeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.collateral, tc.args.principal, tc.args.ctype)
suite.Require().NoError(err)
// update pricefeed
_, err = pk.SetPrice(suite.ctx, sdk.AccAddress{}, "btc:usd", tc.args.finalPrice, suite.ctx.BlockTime().Add(time.Hour*24))
suite.Require().NoError(err)
err = pk.SetCurrentPrices(suite.ctx, "btc:usd")
suite.Require().NoError(err)
_, found := suite.keeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype)
suite.Require().True(found)
err = suite.keeper.AttemptKeeperLiquidation(suite.ctx, suite.addrs[1], suite.addrs[0], tc.args.ctype)
if tc.errArgs.expectLiquidate {
suite.Require().NoError(err)
_, found = suite.keeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype)
suite.Require().False(found)
ak := suite.app.GetAuctionKeeper()
auctions := ak.GetAllAuctions(suite.ctx)
suite.Require().Equal(tc.args.expectedAuctions, auctions)
ack := suite.app.GetAccountKeeper()
keeper := ack.GetAccount(suite.ctx, suite.addrs[1])
suite.Require().Equal(tc.args.expectedKeeperCoins, keeper.GetCoins())
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}
func (suite *SeizeTestSuite) TestBeginBlockerLiquidation() {
type args struct {
ctype string
blockTime time.Time
initialPrice sdk.Dec
finalPrice sdk.Dec
collaterals sdk.Coins
principals sdk.Coins
expectedAuctions auction.Auctions // the auctions we should expect to find have been started
}
type errArgs struct {
expectLiquidate bool
contains string
}
type test struct {
name string
args args
errArgs errArgs
}
// Set up auction constants
layout := "2006-01-02T15:04:05.000Z"
endTimeStr := "9000-01-01T00:00:00.000Z"
endTime, _ := time.Parse(layout, endTimeStr)
addr, _ := sdk.AccAddressFromBech32("kava1ze7y9qwdddejmy7jlw4cymqqlt2wh05yhwmrv2")
testCases := []test{
{
"1 liquidation",
args{
"btc-a",
time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
d("20000.00"),
d("10000.00"),
sdk.Coins{c("btc", 10000000), c("btc", 10000000)},
sdk.Coins{c("usdx", 1000000000), c("usdx", 500000000)},
auction.Auctions{
auction.CollateralAuction{
BaseAuction: auction.BaseAuction{
ID: 1,
Initiator: "liquidator",
Lot: c("btc", 10000000),
Bidder: nil,
Bid: c("usdx", 0),
HasReceivedBids: false,
EndTime: endTime,
MaxEndTime: endTime,
},
CorrespondingDebt: c("debt", 1000000000),
MaxBid: c("usdx", 1025000000),
LotReturns: auction.WeightedAddresses{[]sdk.AccAddress{addr}, []sdk.Int{sdk.NewInt(10000000)}},
},
},
},
errArgs{
true,
"",
},
},
{
"no liquidation",
args{
"btc-a",
time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
d("20000.00"),
d("10000.00"),
sdk.Coins{c("btc", 10000000), c("btc", 10000000)},
sdk.Coins{c("usdx", 500000000), c("usdx", 500000000)},
auction.Auctions{},
},
errArgs{
false,
"collateral ratio not below liquidation ratio",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()
// setup pricefeed
pk := suite.app.GetPriceFeedKeeper()
_, err := pk.SetPrice(suite.ctx, sdk.AccAddress{}, "btc:usd", tc.args.initialPrice, suite.ctx.BlockTime().Add(time.Hour*24))
suite.Require().NoError(err)
err = pk.SetCurrentPrices(suite.ctx, "btc:usd")
suite.Require().NoError(err)
// setup cdp state
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, suite.ctx.BlockTime())
suite.keeper.SetInterestFactor(suite.ctx, tc.args.ctype, sdk.OneDec())
for idx, col := range tc.args.collaterals {
err := suite.keeper.AddCdp(suite.ctx, suite.addrs[idx], col, tc.args.principals[idx], tc.args.ctype)
suite.Require().NoError(err)
}
// update pricefeed
_, err = pk.SetPrice(suite.ctx, sdk.AccAddress{}, "btc:usd", tc.args.finalPrice, suite.ctx.BlockTime().Add(time.Hour*24))
suite.Require().NoError(err)
err = pk.SetCurrentPrices(suite.ctx, "btc:usd")
suite.Require().NoError(err)
_ = suite.app.BeginBlocker(suite.ctx, abci.RequestBeginBlock{Header: suite.ctx.BlockHeader()})
ak := suite.app.GetAuctionKeeper()
auctions := ak.GetAllAuctions(suite.ctx)
if tc.errArgs.expectLiquidate {
suite.Require().Equal(tc.args.expectedAuctions, auctions)
for _, a := range auctions {
ca := a.(auction.CollateralAuction)
_, found := suite.keeper.GetCdpByOwnerAndCollateralType(suite.ctx, ca.LotReturns.Addresses[0], tc.args.ctype)
suite.Require().False(found)
}
} else {
suite.Require().Equal(0, len(auctions))
for idx, _ := range tc.args.collaterals {
_, found := suite.keeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[idx], tc.args.ctype)
suite.Require().True(found)
}
}
})
}
}
func TestSeizeTestSuite(t *testing.T) {
suite.Run(t, new(SeizeTestSuite))
}

View File

@ -8,12 +8,68 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
// ModuleName The name that will be used throughout the module
ModuleName = "cdp"
// StoreKey Top level store key where all module items will be stored
StoreKey = ModuleName
// RouterKey Top level router key
RouterKey = ModuleName
// QuerierRoute Top level query string
QuerierRoute = ModuleName
// DefaultParamspace default name for parameter store
DefaultParamspace = ModuleName
// LiquidatorMacc module account for liquidator
LiquidatorMacc = "liquidator"
// SavingsRateMacc module account for savings rate
SavingsRateMacc = "savings"
)
// Parameter keys
var (
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyDebtParam = []byte("DebtParam")
KeyDistributionFrequency = []byte("DistributionFrequency")
KeyCircuitBreaker = []byte("CircuitBreaker")
KeyDebtThreshold = []byte("DebtThreshold")
KeyDebtLot = []byte("DebtLot")
KeySurplusThreshold = []byte("SurplusThreshold")
KeySurplusLot = []byte("SurplusLot")
KeySavingsRateDistributed = []byte("SavingsRateDistributed")
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
DefaultDebtParam = DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
DefaultGovDenom = "ukava"
DefaultStableDenom = "usdx"
DefaultSurplusThreshold = sdk.NewInt(500000000000)
DefaultDebtThreshold = sdk.NewInt(100000000000)
DefaultSurplusLot = sdk.NewInt(10000000000)
DefaultDebtLot = sdk.NewInt(10000000000)
DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0))
DefaultSavingsDistributionFrequency = time.Hour * 12
DefaultSavingsRateDistributed = sdk.NewInt(0)
minCollateralPrefix = 0
maxCollateralPrefix = 255
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
)
// CDP is the state of a single collateralized debt position.
@ -40,6 +96,19 @@ func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType
}
}
// NewCDPWithFees creates a new CDP object, for use during migration
func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal, fees sdk.Coin, time time.Time) CDP {
return CDP{
ID: id,
Owner: owner,
Type: collateralType,
Collateral: collateral,
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
}
}
func (cdp CDP) Validate() error {
if cdp.ID == 0 {
return errors.New("cdp id cannot be 0")

653
x/cdp/legacy/v0_13/types.go Normal file
View File

@ -0,0 +1,653 @@
package v0_13
import (
"errors"
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
// Parameter keys
var (
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyDebtParam = []byte("DebtParam")
KeyCircuitBreaker = []byte("CircuitBreaker")
KeyDebtThreshold = []byte("DebtThreshold")
KeyDebtLot = []byte("DebtLot")
KeySurplusThreshold = []byte("SurplusThreshold")
KeySurplusLot = []byte("SurplusLot")
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
DefaultDebtParam = DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
DefaultGovDenom = "ukava"
DefaultStableDenom = "usdx"
DefaultSurplusThreshold = sdk.NewInt(500000000000)
DefaultDebtThreshold = sdk.NewInt(100000000000)
DefaultSurplusLot = sdk.NewInt(10000000000)
DefaultDebtLot = sdk.NewInt(10000000000)
DefaultSavingsRateDistributed = sdk.NewInt(0)
minCollateralPrefix = 0
maxCollateralPrefix = 255
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
)
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
CDPs CDPs `json:"cdps" yaml:"cdps"`
Deposits Deposits `json:"deposits" yaml:"deposits"`
StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
GovDenom string `json:"gov_denom" yaml:"gov_denom"`
PreviousAccumulationTimes GenesisAccumulationTimes `json:"previous_accumulation_times" yaml:"previous_accumulation_times"`
TotalPrincipals GenesisTotalPrincipals `json:"total_principals" yaml:"total_principals"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64,
debtDenom, govDenom string, prevAccumTimes GenesisAccumulationTimes,
totalPrincipals GenesisTotalPrincipals) GenesisState {
return GenesisState{
Params: params,
CDPs: cdps,
Deposits: deposits,
StartingCdpID: startingCdpID,
DebtDenom: debtDenom,
GovDenom: govDenom,
PreviousAccumulationTimes: prevAccumTimes,
TotalPrincipals: totalPrincipals,
}
}
// 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 err := gs.CDPs.Validate(); err != nil {
return err
}
if err := gs.Deposits.Validate(); err != nil {
return err
}
if err := gs.PreviousAccumulationTimes.Validate(); err != nil {
return err
}
if err := gs.TotalPrincipals.Validate(); err != nil {
return err
}
if err := sdk.ValidateDenom(gs.DebtDenom); err != nil {
return fmt.Errorf(fmt.Sprintf("debt denom invalid: %v", err))
}
if err := sdk.ValidateDenom(gs.GovDenom); err != nil {
return fmt.Errorf(fmt.Sprintf("gov denom invalid: %v", err))
}
return nil
}
// GenesisAccumulationTime stores the previous distribution time and its corresponding denom
type GenesisAccumulationTime struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
PreviousAccumulationTime time.Time `json:"previous_accumulation_time" yaml:"previous_accumulation_time"`
InterestFactor sdk.Dec `json:"interest_factor" yaml:"interest_factor"`
}
// NewGenesisAccumulationTime returns a new GenesisAccumulationTime
func NewGenesisAccumulationTime(ctype string, prevTime time.Time, factor sdk.Dec) GenesisAccumulationTime {
return GenesisAccumulationTime{
CollateralType: ctype,
PreviousAccumulationTime: prevTime,
InterestFactor: factor,
}
}
// GenesisAccumulationTimes slice of GenesisAccumulationTime
type GenesisAccumulationTimes []GenesisAccumulationTime
// Validate performs validation of GenesisAccumulationTimes
func (gats GenesisAccumulationTimes) Validate() error {
for _, gat := range gats {
if err := gat.Validate(); err != nil {
return err
}
}
return nil
}
// Validate performs validation of GenesisAccumulationTime
func (gat GenesisAccumulationTime) Validate() error {
if gat.InterestFactor.LT(sdk.OneDec()) {
return fmt.Errorf("interest factor should be ≥ 1.0, is %s for %s", gat.InterestFactor, gat.CollateralType)
}
return nil
}
// GenesisTotalPrincipal stores the total principal and its corresponding collateral type
type GenesisTotalPrincipal struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
TotalPrincipal sdk.Int `json:"total_principal" yaml:"total_principal"`
}
// NewGenesisTotalPrincipal returns a new GenesisTotalPrincipal
func NewGenesisTotalPrincipal(ctype string, principal sdk.Int) GenesisTotalPrincipal {
return GenesisTotalPrincipal{
CollateralType: ctype,
TotalPrincipal: principal,
}
}
// GenesisTotalPrincipals slice of GenesisTotalPrincipal
type GenesisTotalPrincipals []GenesisTotalPrincipal
// Validate performs validation of GenesisTotalPrincipal
func (gtp GenesisTotalPrincipal) Validate() error {
if gtp.TotalPrincipal.IsNegative() {
return fmt.Errorf("total principal should be positive, is %s for %s", gtp.TotalPrincipal, gtp.CollateralType)
}
return nil
}
// Validate performs validation of GenesisTotalPrincipals
func (gtps GenesisTotalPrincipals) Validate() error {
for _, gtp := range gtps {
if err := gtp.Validate(); err != nil {
return err
}
}
return nil
}
// Params governance parameters for cdp module
type Params struct {
CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"`
DebtParam DebtParam `json:"debt_param" yaml:"debt_param"`
GlobalDebtLimit sdk.Coin `json:"global_debt_limit" yaml:"global_debt_limit"`
SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"`
SurplusAuctionLot sdk.Int `json:"surplus_auction_lot" yaml:"surplus_auction_lot"`
DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
DebtAuctionLot sdk.Int `json:"debt_auction_lot" yaml:"debt_auction_lot"`
CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"`
}
// NewParams returns a new params object
func NewParams(
debtLimit sdk.Coin, collateralParams CollateralParams, debtParam DebtParam, surplusThreshold,
surplusLot, debtThreshold, debtLot sdk.Int, breaker bool,
) Params {
return Params{
GlobalDebtLimit: debtLimit,
CollateralParams: collateralParams,
DebtParam: debtParam,
SurplusAuctionThreshold: surplusThreshold,
SurplusAuctionLot: surplusLot,
DebtAuctionThreshold: debtThreshold,
DebtAuctionLot: debtLot,
CircuitBreaker: breaker,
}
}
// CollateralParam governance parameters for each collateral type within the cdp module
type CollateralParam struct {
Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type
Type string `json:"type" yaml:"type"`
LiquidationRatio sdk.Dec `json:"liquidation_ratio" yaml:"liquidation_ratio"` // The ratio (Collateral (priced in stable coin) / Debt) under which a CDP will be liquidated
DebtLimit sdk.Coin `json:"debt_limit" yaml:"debt_limit"` // Maximum amount of debt allowed to be drawn from this collateral type
StabilityFee sdk.Dec `json:"stability_fee" yaml:"stability_fee"` // per second stability fee for loans opened using this collateral
AuctionSize sdk.Int `json:"auction_size" yaml:"auction_size"` // Max amount of collateral to sell off in any one auction.
LiquidationPenalty sdk.Dec `json:"liquidation_penalty" yaml:"liquidation_penalty"` // percentage penalty (between [0, 1]) applied to a cdp if it is liquidated
Prefix byte `json:"prefix" yaml:"prefix"`
SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` // marketID of the spot price of the asset from the pricefeed - used for opening CDPs, depositing, withdrawing
LiquidationMarketID string `json:"liquidation_market_id" yaml:"liquidation_market_id"` // marketID of the pricefeed used for liquidation
KeeperRewardPercentage sdk.Dec `json:"keeper_reward_percentage" yaml:"keeper_reward_percentage"` // the percentage of a CDPs collateral that gets rewarded to a keeper that liquidates the position
CheckCollateralizationIndexCount sdk.Int `json:"check_collateralization_index_count" yaml:"check_collateralization_index_count"` // the number of cdps that will be checked for liquidation in the begin blocker
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral
}
// NewCollateralParam returns a new CollateralParam
func NewCollateralParam(
denom, ctype string, liqRatio sdk.Dec, debtLimit sdk.Coin, stabilityFee sdk.Dec, auctionSize sdk.Int,
liqPenalty sdk.Dec, prefix byte, spotMarketID, liquidationMarketID string, keeperReward sdk.Dec, checkIndexCount sdk.Int, conversionFactor sdk.Int) CollateralParam {
return CollateralParam{
Denom: denom,
Type: ctype,
LiquidationRatio: liqRatio,
DebtLimit: debtLimit,
StabilityFee: stabilityFee,
AuctionSize: auctionSize,
LiquidationPenalty: liqPenalty,
Prefix: prefix,
SpotMarketID: spotMarketID,
LiquidationMarketID: liquidationMarketID,
KeeperRewardPercentage: keeperReward,
CheckCollateralizationIndexCount: checkIndexCount,
ConversionFactor: conversionFactor,
}
}
// String implements fmt.Stringer
func (cp CollateralParam) String() string {
return fmt.Sprintf(`Collateral:
Denom: %s
Type: %s
Liquidation Ratio: %s
Stability Fee: %s
Liquidation Penalty: %s
Debt Limit: %s
Auction Size: %s
Prefix: %b
Spot Market ID: %s
Liquidation Market ID: %s
Keeper Reward Percentage: %s
Check Collateralization Count: %s
Conversion Factor: %s`,
cp.Denom, cp.Type, cp.LiquidationRatio, cp.StabilityFee, cp.LiquidationPenalty,
cp.DebtLimit, cp.AuctionSize, cp.Prefix, cp.SpotMarketID, cp.LiquidationMarketID,
cp.KeeperRewardPercentage, cp.CheckCollateralizationIndexCount, cp.ConversionFactor)
}
// CollateralParams array of CollateralParam
type CollateralParams []CollateralParam
// String implements fmt.Stringer
func (cps CollateralParams) String() string {
out := "Collateral Params\n"
for _, cp := range cps {
out += fmt.Sprintf("%s\n", cp)
}
return out
}
// DebtParam governance params for debt assets
type DebtParam struct {
Denom string `json:"denom" yaml:"denom"`
ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust
}
// NewDebtParam returns a new DebtParam
func NewDebtParam(denom, refAsset string, conversionFactor, debtFloor sdk.Int) DebtParam {
return DebtParam{
Denom: denom,
ReferenceAsset: refAsset,
ConversionFactor: conversionFactor,
DebtFloor: debtFloor,
}
}
// DebtParams array of DebtParam
type DebtParams []DebtParam
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateGlobalDebtLimitParam(p.GlobalDebtLimit); err != nil {
return err
}
if err := validateCollateralParams(p.CollateralParams); err != nil {
return err
}
if err := validateDebtParam(p.DebtParam); err != nil {
return err
}
if err := validateCircuitBreakerParam(p.CircuitBreaker); err != nil {
return err
}
if err := validateSurplusAuctionThresholdParam(p.SurplusAuctionThreshold); err != nil {
return err
}
if err := validateSurplusAuctionLotParam(p.SurplusAuctionLot); err != nil {
return err
}
if err := validateDebtAuctionThresholdParam(p.DebtAuctionThreshold); err != nil {
return err
}
if err := validateDebtAuctionLotParam(p.DebtAuctionLot); err != nil {
return err
}
if len(p.CollateralParams) == 0 { // default value OK
return nil
}
if (DebtParam{}) != p.DebtParam {
if p.DebtParam.Denom != p.GlobalDebtLimit.Denom {
return fmt.Errorf("debt denom %s does not match global debt denom %s",
p.DebtParam.Denom, p.GlobalDebtLimit.Denom)
}
}
// validate collateral params
collateralDupMap := make(map[string]int)
prefixDupMap := make(map[int]int)
collateralParamsDebtLimit := sdk.ZeroInt()
for _, cp := range p.CollateralParams {
prefix := int(cp.Prefix)
prefixDupMap[prefix] = 1
collateralDupMap[cp.Denom] = 1
if cp.DebtLimit.Denom != p.GlobalDebtLimit.Denom {
return fmt.Errorf("collateral debt limit denom %s does not match global debt limit denom %s",
cp.DebtLimit.Denom, p.GlobalDebtLimit.Denom)
}
collateralParamsDebtLimit = collateralParamsDebtLimit.Add(cp.DebtLimit.Amount)
if cp.DebtLimit.Amount.GT(p.GlobalDebtLimit.Amount) {
return fmt.Errorf("collateral debt limit %s exceeds global debt limit: %s", cp.DebtLimit, p.GlobalDebtLimit)
}
}
if collateralParamsDebtLimit.GT(p.GlobalDebtLimit.Amount) {
return fmt.Errorf("sum of collateral debt limits %s exceeds global debt limit %s",
collateralParamsDebtLimit, p.GlobalDebtLimit)
}
return nil
}
func validateGlobalDebtLimitParam(i interface{}) error {
globalDebtLimit, ok := i.(sdk.Coin)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if !globalDebtLimit.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "global debt limit %s", globalDebtLimit.String())
}
return nil
}
func validateCollateralParams(i interface{}) error {
collateralParams, ok := i.(CollateralParams)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
prefixDupMap := make(map[int]bool)
typeDupMap := make(map[string]bool)
for _, cp := range collateralParams {
if err := sdk.ValidateDenom(cp.Denom); err != nil {
return fmt.Errorf("collateral denom invalid %s", cp.Denom)
}
if strings.TrimSpace(cp.SpotMarketID) == "" {
return fmt.Errorf("spot market id cannot be blank %s", cp)
}
if strings.TrimSpace(cp.Type) == "" {
return fmt.Errorf("collateral type cannot be blank %s", cp)
}
if strings.TrimSpace(cp.LiquidationMarketID) == "" {
return fmt.Errorf("liquidation market id cannot be blank %s", cp)
}
prefix := int(cp.Prefix)
if prefix < minCollateralPrefix || prefix > maxCollateralPrefix {
return fmt.Errorf("invalid prefix for collateral denom %s: %b", cp.Denom, cp.Prefix)
}
_, found := prefixDupMap[prefix]
if found {
return fmt.Errorf("duplicate prefix for collateral denom %s: %v", cp.Denom, []byte{cp.Prefix})
}
prefixDupMap[prefix] = true
_, found = typeDupMap[cp.Type]
if found {
return fmt.Errorf("duplicate cdp collateral type: %s", cp.Type)
}
typeDupMap[cp.Type] = true
if !cp.DebtLimit.IsValid() {
return fmt.Errorf("debt limit for all collaterals should be positive, is %s for %s", cp.DebtLimit, cp.Denom)
}
if cp.LiquidationPenalty.LT(sdk.ZeroDec()) || cp.LiquidationPenalty.GT(sdk.OneDec()) {
return fmt.Errorf("liquidation penalty should be between 0 and 1, is %s for %s", cp.LiquidationPenalty, cp.Denom)
}
if !cp.AuctionSize.IsPositive() {
return fmt.Errorf("auction size should be positive, is %s for %s", cp.AuctionSize, cp.Denom)
}
if cp.StabilityFee.LT(sdk.OneDec()) || cp.StabilityFee.GT(stabilityFeeMax) {
return fmt.Errorf("stability fee must be ≥ 1.0, ≤ %s, is %s for %s", stabilityFeeMax, cp.StabilityFee, cp.Denom)
}
if cp.KeeperRewardPercentage.IsNegative() || cp.KeeperRewardPercentage.GT(sdk.OneDec()) {
return fmt.Errorf("keeper reward percentage should be between 0 and 1, is %s for %s", cp.KeeperRewardPercentage, cp.Denom)
}
if cp.CheckCollateralizationIndexCount.IsNegative() {
return fmt.Errorf("keeper reward percentage should be positive, is %s for %s", cp.CheckCollateralizationIndexCount, cp.Denom)
}
}
return nil
}
func validateDebtParam(i interface{}) error {
debtParam, ok := i.(DebtParam)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if err := sdk.ValidateDenom(debtParam.Denom); err != nil {
return fmt.Errorf("debt denom invalid %s", debtParam.Denom)
}
return nil
}
func validateCircuitBreakerParam(i interface{}) error {
_, ok := i.(bool)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return nil
}
func validateSurplusAuctionThresholdParam(i interface{}) error {
sat, ok := i.(sdk.Int)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if !sat.IsPositive() {
return fmt.Errorf("surplus auction threshold should be positive: %s", sat)
}
return nil
}
func validateSurplusAuctionLotParam(i interface{}) error {
sal, ok := i.(sdk.Int)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if !sal.IsPositive() {
return fmt.Errorf("surplus auction lot should be positive: %s", sal)
}
return nil
}
func validateDebtAuctionThresholdParam(i interface{}) error {
dat, ok := i.(sdk.Int)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if !dat.IsPositive() {
return fmt.Errorf("debt auction threshold should be positive: %s", dat)
}
return nil
}
func validateDebtAuctionLotParam(i interface{}) error {
dal, ok := i.(sdk.Int)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if !dal.IsPositive() {
return fmt.Errorf("debt auction lot should be positive: %s", dal)
}
return nil
}
// CDP is the state of a single collateralized debt position.
type CDP struct {
ID uint64 `json:"id" yaml:"id"` // unique id for cdp
Owner sdk.AccAddress `json:"owner" yaml:"owner"` // Account that authorizes changes to the CDP
Type string `json:"type" yaml:"type"` // string representing the unique collateral type of the CDP
Collateral sdk.Coin `json:"collateral" yaml:"collateral"` // Amount of collateral stored in this CDP
Principal sdk.Coin `json:"principal" yaml:"principal"` // Amount of debt drawn using the CDP
AccumulatedFees sdk.Coin `json:"accumulated_fees" yaml:"accumulated_fees"` // Fees accumulated since the CDP was opened or debt was last repaid
FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // The time when fees were last updated
InterestFactor sdk.Dec `json:"interest_factor" yaml:"interest_factor"` // the interest factor when fees were last calculated for this CDP
}
// NewCDP creates a new CDP object
func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal sdk.Coin, time time.Time, interestFactor sdk.Dec) CDP {
fees := sdk.NewCoin(principal.Denom, sdk.ZeroInt())
return CDP{
ID: id,
Owner: owner,
Type: collateralType,
Collateral: collateral,
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
InterestFactor: interestFactor,
}
}
// NewCDPWithFees creates a new CDP object, for use during migration
func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal, fees sdk.Coin, time time.Time, interestFactor sdk.Dec) CDP {
return CDP{
ID: id,
Owner: owner,
Type: collateralType,
Collateral: collateral,
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
InterestFactor: interestFactor,
}
}
// Validate performs a basic validation of the CDP fields.
func (cdp CDP) Validate() error {
if cdp.ID == 0 {
return errors.New("cdp id cannot be 0")
}
if cdp.Owner.Empty() {
return errors.New("cdp owner cannot be empty")
}
if !cdp.Collateral.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "collateral %s", cdp.Collateral)
}
if !cdp.Principal.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "principal %s", cdp.Principal)
}
if !cdp.AccumulatedFees.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "accumulated fees %s", cdp.AccumulatedFees)
}
if cdp.FeesUpdated.IsZero() {
return errors.New("cdp updated fee time cannot be zero")
}
if strings.TrimSpace(cdp.Type) == "" {
return fmt.Errorf("cdp type cannot be empty")
}
return nil
}
// GetTotalPrincipal returns the total principle for the cdp
func (cdp CDP) GetTotalPrincipal() sdk.Coin {
return cdp.Principal.Add(cdp.AccumulatedFees)
}
// CDPs a collection of CDP objects
type CDPs []CDP
// Validate validates each CDP
func (cdps CDPs) Validate() error {
for _, cdp := range cdps {
if err := cdp.Validate(); err != nil {
return err
}
}
return nil
}
// Deposit defines an amount of coins deposited by an account to a cdp
type Deposit struct {
CdpID uint64 `json:"cdp_id" yaml:"cdp_id"` // cdpID of the cdp
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` // Address of the depositor
Amount sdk.Coin `json:"amount" yaml:"amount"` // Deposit amount
}
// NewDeposit creates a new Deposit object
func NewDeposit(cdpID uint64, depositor sdk.AccAddress, amount sdk.Coin) Deposit {
return Deposit{cdpID, depositor, amount}
}
// Validate performs a basic validation of the deposit fields.
func (d Deposit) Validate() error {
if d.CdpID == 0 {
return errors.New("deposit's cdp id cannot be 0")
}
if d.Depositor.Empty() {
return errors.New("depositor cannot be empty")
}
if !d.Amount.IsValid() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "deposit %s", d.Amount)
}
return nil
}
// Deposits a collection of Deposit objects
type Deposits []Deposit
// Validate validates each deposit
func (ds Deposits) Validate() error {
for _, d := range ds {
if err := d.Validate(); err != nil {
return err
}
}
return nil
}

View File

@ -4,12 +4,50 @@ import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
ModuleName = "cdp"
)
// Parameter keys
var (
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyDebtParam = []byte("DebtParam")
KeyDistributionFrequency = []byte("DistributionFrequency")
KeyCircuitBreaker = []byte("CircuitBreaker")
KeyDebtThreshold = []byte("DebtThreshold")
KeyDebtLot = []byte("DebtLot")
KeySurplusThreshold = []byte("SurplusThreshold")
KeySurplusLot = []byte("SurplusLot")
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
DefaultDebtParam = DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
DefaultGovDenom = "ukava"
DefaultStableDenom = "usdx"
DefaultSurplusThreshold = sdk.NewInt(500000000000)
DefaultDebtThreshold = sdk.NewInt(100000000000)
DefaultSurplusLot = sdk.NewInt(10000000000)
DefaultDebtLot = sdk.NewInt(10000000000)
DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0))
DefaultSavingsDistributionFrequency = time.Hour * 12
minCollateralPrefix = 0
maxCollateralPrefix = 255
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
)
// CDP is the state of a single collateralized debt position.
type CDP struct {
ID uint64 `json:"id" yaml:"id"` // unique id for cdp

View File

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/binary"
"fmt"
"time"
"github.com/tendermint/tendermint/libs/kv"
@ -54,12 +53,6 @@ func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &totalB)
return fmt.Sprintf("%s\n%s", totalA, totalB)
case bytes.Equal(kvA.Key[:1], types.PreviousDistributionTimeKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}

View File

@ -32,7 +32,7 @@ func TestDecodeDistributionStore(t *testing.T) {
deposit := types.Deposit{CdpID: 1, Amount: oneCoins}
principal := sdk.OneInt()
prevDistTime := time.Now().UTC()
cdp := types.CDP{ID: 1, FeesUpdated: prevDistTime, Collateral: oneCoins, Principal: oneCoins, AccumulatedFees: oneCoins}
cdp := types.CDP{ID: 1, FeesUpdated: prevDistTime, Collateral: oneCoins, Principal: oneCoins, AccumulatedFees: oneCoins, InterestFactor: sdk.OneDec()}
kvPairs := kv.Pairs{
kv.Pair{Key: types.CdpIDKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(cdpIds)},
@ -43,7 +43,6 @@ func TestDecodeDistributionStore(t *testing.T) {
kv.Pair{Key: []byte(types.GovDenomKey), Value: cdc.MustMarshalBinaryLengthPrefixed(denom)},
kv.Pair{Key: []byte(types.DepositKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(deposit)},
kv.Pair{Key: []byte(types.PrincipalKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(principal)},
kv.Pair{Key: []byte(types.PreviousDistributionTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevDistTime)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
@ -59,7 +58,6 @@ func TestDecodeDistributionStore(t *testing.T) {
{"GovDenom", fmt.Sprintf("%s\n%s", denom, denom)},
{"DepositKeyPrefix", fmt.Sprintf("%v\n%v", deposit, deposit)},
{"Principal", fmt.Sprintf("%v\n%v", principal, principal)},
{"PreviousDistributionTime", fmt.Sprintf("%s\n%s", prevDistTime, prevDistTime)},
{"other", ""},
}
for i, tt := range tests {

View File

@ -75,7 +75,6 @@ func randomCdpGenState(selection int) types.GenesisState {
SurplusAuctionLot: types.DefaultSurplusLot,
DebtAuctionLot: types.DefaultDebtLot,
DebtAuctionThreshold: types.DefaultDebtThreshold,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
CollateralParams: types.CollateralParams{
{
Denom: "xrp",
@ -122,14 +121,12 @@ func randomCdpGenState(selection int) types.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
},
StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom,
CDPs: types.CDPs{},
PreviousDistributionTime: types.DefaultPreviousDistributionTime,
}
case 1:
return types.GenesisState{
@ -139,7 +136,6 @@ func randomCdpGenState(selection int) types.GenesisState {
DebtAuctionThreshold: types.DefaultDebtThreshold,
SurplusAuctionLot: types.DefaultSurplusLot,
DebtAuctionLot: types.DefaultDebtLot,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
CollateralParams: types.CollateralParams{
{
Denom: "bnb",
@ -160,14 +156,12 @@ func randomCdpGenState(selection int) types.GenesisState {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
},
StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom,
CDPs: types.CDPs{},
PreviousDistributionTime: types.DefaultPreviousDistributionTime,
}
default:
panic("invalid genesis state selector")

View File

@ -184,12 +184,12 @@ func SimulateMsgCdp(ak types.AccountKeeper, k keeper.Keeper, pfk types.Pricefeed
if shouldDraw(r) {
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.Amount), randCollateralParam.ConversionFactor.Neg())
collateralValue := collateralShifted.Mul(priceShifted)
newFeesAccumulated := k.CalculateFees(ctx, existingCDP.Principal, sdk.NewInt(ctx.BlockTime().Unix()-existingCDP.FeesUpdated.Unix()), randCollateralParam.Type).Amount
totalFees := existingCDP.AccumulatedFees.Amount.Add(newFeesAccumulated)
newFeesAccumulated := k.CalculateNewInterest(ctx, existingCDP)
totalFees := existingCDP.AccumulatedFees.Add(newFeesAccumulated)
// given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio
debt := existingCDP.Principal.Amount.Add(totalFees)
debt := existingCDP.Principal.Add(totalFees)
maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio)
maxDebt := (maxTotalDebt.Sub(sdk.NewDecFromInt(debt))).Mul(sdk.MustNewDecFromStr("0.95")).TruncateInt()
maxDebt := (maxTotalDebt.Sub(sdk.NewDecFromInt(debt.Amount))).Mul(sdk.MustNewDecFromStr("0.95")).TruncateInt()
if maxDebt.LTE(sdk.OneInt()) {
// debt in cdp is maxed out
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "cdp debt maxed out, cannot draw more debt", false, nil), nil, nil

View File

@ -26,10 +26,12 @@ The CDP's collateral always equal to the total of the deposits.
type CDP struct {
ID uint64
Owner sdk.AccAddress
Type string
Collateral sdk.Coin
Principal sdk.Coin
AccumulatedFees sdk.Coin
FeesUpdated time.Time
InterestFactor sdk.Dec
}
```

View File

@ -17,12 +17,13 @@ type CDP struct {
Type string `json:"type" yaml:"type"` // string representing the unique collateral type of the CDP
Collateral sdk.Coin `json:"collateral" yaml:"collateral"` // Amount of collateral stored in this CDP
Principal sdk.Coin `json:"principal" yaml:"principal"` // Amount of debt drawn using the CDP
AccumulatedFees sdk.Coin `json:"accumulated_fees" yaml:"accumulated_fees"` // Fees accumulated since the CDP was opened or debt was last repayed
FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // Amount of stable coin drawn from this CDP
AccumulatedFees sdk.Coin `json:"accumulated_fees" yaml:"accumulated_fees"` // Fees accumulated since the CDP was opened or debt was last repaid
FeesUpdated time.Time `json:"fees_updated" yaml:"fees_updated"` // The time when fees were last updated
InterestFactor sdk.Dec `json:"interest_factor" yaml:"interest_factor"` // the interest factor when fees were last calculated for this CDP
}
// NewCDP creates a new CDP object
func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal sdk.Coin, time time.Time) CDP {
func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal sdk.Coin, time time.Time, interestFactor sdk.Dec) CDP {
fees := sdk.NewCoin(principal.Denom, sdk.ZeroInt())
return CDP{
ID: id,
@ -32,11 +33,12 @@ func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
InterestFactor: interestFactor,
}
}
// NewCDPWithFees creates a new CDP object, for use during migration
func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal, fees sdk.Coin, time time.Time) CDP {
func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType string, principal, fees sdk.Coin, time time.Time, interestFactor sdk.Dec) CDP {
return CDP{
ID: id,
Owner: owner,
@ -45,6 +47,7 @@ func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collat
Principal: principal,
AccumulatedFees: fees,
FeesUpdated: time,
InterestFactor: interestFactor,
}
}
@ -57,7 +60,8 @@ func (cdp CDP) String() string {
Collateral: %s
Principal: %s
AccumulatedFees: %s
Fees Last Updated: %s`,
Fees Last Updated: %s
Interest Factor: %s`,
cdp.Owner,
cdp.ID,
cdp.Type,
@ -65,6 +69,7 @@ func (cdp CDP) String() string {
cdp.Principal,
cdp.AccumulatedFees,
cdp.FeesUpdated,
cdp.InterestFactor,
))
}
@ -139,6 +144,7 @@ func NewAugmentedCDP(cdp CDP, collateralValue sdk.Coin, collateralizationRatio s
Principal: cdp.Principal,
AccumulatedFees: cdp.AccumulatedFees,
FeesUpdated: cdp.FeesUpdated,
InterestFactor: cdp.InterestFactor,
},
CollateralValue: collateralValue,
CollateralizationRatio: collateralizationRatio,
@ -157,6 +163,7 @@ func (augCDP AugmentedCDP) String() string {
Principal: %s
Fees: %s
Fees Last Updated: %s
Interest Factor: %s
Collateralization ratio: %s`,
augCDP.Owner,
augCDP.ID,
@ -166,6 +173,7 @@ func (augCDP AugmentedCDP) String() string {
augCDP.Principal,
augCDP.AccumulatedFees,
augCDP.FeesUpdated,
augCDP.InterestFactor,
augCDP.CollateralizationRatio,
))
}

View File

@ -42,7 +42,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}{
{
name: "valid cdp",
cdp: types.NewCDP(1, suite.addrs[0], sdk.NewInt64Coin("bnb", 100000), "bnb-a", sdk.NewInt64Coin("usdx", 100000), tmtime.Now()),
cdp: types.NewCDP(1, suite.addrs[0], sdk.NewInt64Coin("bnb", 100000), "bnb-a", sdk.NewInt64Coin("usdx", 100000), tmtime.Now(), sdk.OneDec()),
errArgs: errArgs{
expectPass: true,
contains: "",
@ -50,7 +50,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid cdp id",
cdp: types.NewCDP(0, suite.addrs[0], sdk.NewInt64Coin("bnb", 100000), "bnb-a", sdk.NewInt64Coin("usdx", 100000), tmtime.Now()),
cdp: types.NewCDP(0, suite.addrs[0], sdk.NewInt64Coin("bnb", 100000), "bnb-a", sdk.NewInt64Coin("usdx", 100000), tmtime.Now(), sdk.OneDec()),
errArgs: errArgs{
expectPass: false,
contains: "cdp id cannot be 0",
@ -58,7 +58,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid collateral",
cdp: types.CDP{1, suite.addrs[0], "bnb-a", sdk.Coin{"", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now()},
cdp: types.CDP{1, suite.addrs[0], "bnb-a", sdk.Coin{"", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now(), sdk.OneDec()},
errArgs: errArgs{
expectPass: false,
contains: "invalid coins: collateral",
@ -66,7 +66,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid prinicpal",
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now()},
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now(), sdk.OneDec()},
errArgs: errArgs{
expectPass: false,
contains: "invalid coins: principal",
@ -74,7 +74,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid fees",
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"", sdk.NewInt(0)}, tmtime.Now()},
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"", sdk.NewInt(0)}, tmtime.Now(), sdk.OneDec()},
errArgs: errArgs{
expectPass: false,
contains: "invalid coins: accumulated fees",
@ -82,7 +82,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid fees updated",
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, time.Time{}},
cdp: types.CDP{1, suite.addrs[0], "xrp-a", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, time.Time{}, sdk.OneDec()},
errArgs: errArgs{
expectPass: false,
contains: "cdp updated fee time cannot be zero",
@ -90,7 +90,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
},
{
name: "invalid type",
cdp: types.CDP{1, suite.addrs[0], "", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now()},
cdp: types.CDP{1, suite.addrs[0], "", sdk.Coin{"xrp", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(100)}, sdk.Coin{"usdx", sdk.NewInt(0)}, tmtime.Now(), sdk.OneDec()},
errArgs: errArgs{
expectPass: false,
contains: "cdp type cannot be empty",

View File

@ -45,4 +45,10 @@ var (
ErrPricefeedDown = sdkerrors.Register(ModuleName, 19, "no price found for collateral")
// ErrInvalidCollateral error for when the input collateral denom does not match the expected collateral denom
ErrInvalidCollateral = sdkerrors.Register(ModuleName, 20, "invalid collateral for input collateral type")
// ErrAccountNotFound error for when no account is found for an input address
ErrAccountNotFound = sdkerrors.Register(ModuleName, 21, "account not found")
// ErrInsufficientBalance error for when an account does not have enough funds
ErrInsufficientBalance = sdkerrors.Register(ModuleName, 22, "insufficient balance")
// ErrNotLiquidatable error for when an cdp is not liquidatable
ErrNotLiquidatable = sdkerrors.Register(ModuleName, 23, "cdp collateral ratio not below liquidation ratio")
)

View File

@ -48,3 +48,9 @@ type AccountKeeper interface {
IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) (stop bool))
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
}
// CDPHooks event hooks for other keepers to run code in response to CDP modifications
type CDPHooks interface {
AfterCDPCreated(ctx sdk.Context, cdp CDP)
BeforeCDPModified(ctx sdk.Context, cdp CDP)
}

View File

@ -16,13 +16,14 @@ type GenesisState struct {
StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
GovDenom string `json:"gov_denom" yaml:"gov_denom"`
PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"`
SavingsRateDistributed sdk.Int `json:"savings_rate_distributed" yaml:"savings_rate_distributed"`
PreviousAccumulationTimes GenesisAccumulationTimes `json:"previous_accumulation_times" yaml:"previous_accumulation_times"`
TotalPrincipals GenesisTotalPrincipals `json:"total_principals" yaml:"total_principals"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64,
debtDenom, govDenom string, previousDistTime time.Time, savingsRateDist sdk.Int) GenesisState {
debtDenom, govDenom string, prevAccumTimes GenesisAccumulationTimes,
totalPrincipals GenesisTotalPrincipals) GenesisState {
return GenesisState{
Params: params,
CDPs: cdps,
@ -30,8 +31,8 @@ func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID
StartingCdpID: startingCdpID,
DebtDenom: debtDenom,
GovDenom: govDenom,
PreviousDistributionTime: previousDistTime,
SavingsRateDistributed: savingsRateDist,
PreviousAccumulationTimes: prevAccumTimes,
TotalPrincipals: totalPrincipals,
}
}
@ -44,8 +45,8 @@ func DefaultGenesisState() GenesisState {
DefaultCdpStartingID,
DefaultDebtDenom,
DefaultGovDenom,
DefaultPreviousDistributionTime,
DefaultSavingsRateDistributed,
GenesisAccumulationTimes{},
GenesisTotalPrincipals{},
)
}
@ -65,11 +66,11 @@ func (gs GenesisState) Validate() error {
return err
}
if gs.PreviousDistributionTime.IsZero() {
return fmt.Errorf("previous distribution time not set")
if err := gs.PreviousAccumulationTimes.Validate(); err != nil {
return err
}
if err := validateSavingsRateDistributed(gs.SavingsRateDistributed); err != nil {
if err := gs.TotalPrincipals.Validate(); err != nil {
return err
}
@ -108,3 +109,75 @@ func (gs GenesisState) Equal(gs2 GenesisState) bool {
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}
// GenesisTotalPrincipal stores the total principal and its corresponding collateral type
type GenesisTotalPrincipal struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
TotalPrincipal sdk.Int `json:"total_principal" yaml:"total_principal"`
}
// NewGenesisTotalPrincipal returns a new GenesisTotalPrincipal
func NewGenesisTotalPrincipal(ctype string, principal sdk.Int) GenesisTotalPrincipal {
return GenesisTotalPrincipal{
CollateralType: ctype,
TotalPrincipal: principal,
}
}
// GenesisTotalPrincipals slice of GenesisTotalPrincipal
type GenesisTotalPrincipals []GenesisTotalPrincipal
// Validate performs validation of GenesisTotalPrincipal
func (gtp GenesisTotalPrincipal) Validate() error {
if gtp.TotalPrincipal.IsNegative() {
return fmt.Errorf("total principal should be positive, is %s for %s", gtp.TotalPrincipal, gtp.CollateralType)
}
return nil
}
// Validate performs validation of GenesisTotalPrincipals
func (gtps GenesisTotalPrincipals) Validate() error {
for _, gtp := range gtps {
if err := gtp.Validate(); err != nil {
return err
}
}
return nil
}
// GenesisAccumulationTime stores the previous distribution time and its corresponding denom
type GenesisAccumulationTime struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
PreviousAccumulationTime time.Time `json:"previous_accumulation_time" yaml:"previous_accumulation_time"`
InterestFactor sdk.Dec `json:"interest_factor" yaml:"interest_factor"`
}
// NewGenesisAccumulationTime returns a new GenesisAccumulationTime
func NewGenesisAccumulationTime(ctype string, prevTime time.Time, factor sdk.Dec) GenesisAccumulationTime {
return GenesisAccumulationTime{
CollateralType: ctype,
PreviousAccumulationTime: prevTime,
InterestFactor: factor,
}
}
// GenesisAccumulationTimes slice of GenesisAccumulationTime
type GenesisAccumulationTimes []GenesisAccumulationTime
// Validate performs validation of GenesisAccumulationTimes
func (gats GenesisAccumulationTimes) Validate() error {
for _, gat := range gats {
if err := gat.Validate(); err != nil {
return err
}
}
return nil
}
// Validate performs validation of GenesisAccumulationTime
func (gat GenesisAccumulationTime) Validate() error {
if gat.InterestFactor.LT(sdk.OneDec()) {
return fmt.Errorf("interest factor should be ≥ 1.0, is %s for %s", gat.InterestFactor, gat.CollateralType)
}
return nil
}

25
x/cdp/types/hooks.go Normal file
View File

@ -0,0 +1,25 @@
package types
import sdk "github.com/cosmos/cosmos-sdk/types"
// MultiCDPHooks combine multiple cdp hooks, all hook functions are run in array sequence
type MultiCDPHooks []CDPHooks
// NewMultiCDPHooks returns a new MultiCDPHooks
func NewMultiCDPHooks(hooks ...CDPHooks) MultiCDPHooks {
return hooks
}
// BeforeCDPModified runs before a cdp is modified
func (h MultiCDPHooks) BeforeCDPModified(ctx sdk.Context, cdp CDP) {
for i := range h {
h[i].BeforeCDPModified(ctx, cdp)
}
}
// AfterCDPCreated runs before a cdp is created
func (h MultiCDPHooks) AfterCDPCreated(ctx sdk.Context, cdp CDP) {
for i := range h {
h[i].AfterCDPCreated(ctx, cdp)
}
}

View File

@ -25,9 +25,6 @@ const (
// LiquidatorMacc module account for liquidator
LiquidatorMacc = "liquidator"
// SavingsRateMacc module account for savings rate
SavingsRateMacc = "savings"
)
var sep = []byte(":")
@ -51,17 +48,17 @@ var sep = []byte(":")
// KVStore key prefixes
var (
CdpIDKeyPrefix = []byte{0x00}
CdpKeyPrefix = []byte{0x01}
CollateralRatioIndexPrefix = []byte{0x02}
CdpIDKey = []byte{0x03}
DebtDenomKey = []byte{0x04}
GovDenomKey = []byte{0x05}
DepositKeyPrefix = []byte{0x06}
PrincipalKeyPrefix = []byte{0x07}
PreviousDistributionTimeKey = []byte{0x08}
PricefeedStatusKeyPrefix = []byte{0x09}
SavingsRateDistributedKey = []byte{0x10}
CdpIDKeyPrefix = []byte{0x01}
CdpKeyPrefix = []byte{0x02}
CollateralRatioIndexPrefix = []byte{0x03}
CdpIDKey = []byte{0x04}
DebtDenomKey = []byte{0x05}
GovDenomKey = []byte{0x06}
DepositKeyPrefix = []byte{0x07}
PrincipalKeyPrefix = []byte{0x08}
PricefeedStatusKeyPrefix = []byte{0x10}
PreviousAccrualTimePrefix = []byte{0x12}
InterestFactorPrefix = []byte{0x13}
)
// GetCdpIDBytes returns the byte representation of the cdpID

View File

@ -314,3 +314,59 @@ func (msg MsgRepayDebt) String() string {
Payment: %s
`, msg.Sender, msg.CollateralType, msg.Payment)
}
// MsgLiquidate attempts to liquidate a borrower's cdp
type MsgLiquidate struct {
Keeper sdk.AccAddress `json:"keeper" yaml:"keeper"`
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
}
// NewMsgLiquidate returns a new MsgLiquidate
func NewMsgLiquidate(keeper, borrower sdk.AccAddress, ctype string) MsgLiquidate {
return MsgLiquidate{
Keeper: keeper,
Borrower: borrower,
CollateralType: ctype,
}
}
// Route return the message type used for routing the message.
func (msg MsgLiquidate) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgLiquidate) Type() string { return "liquidate" }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgLiquidate) ValidateBasic() error {
if msg.Keeper.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "keeper address cannot be empty")
}
if msg.Borrower.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "borrower address cannot be empty")
}
if strings.TrimSpace(msg.CollateralType) == "" {
return sdkerrors.Wrap(ErrInvalidCollateral, "collateral type cannot be empty")
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgLiquidate) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgLiquidate) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Keeper}
}
// String implements the Stringer interface
func (msg MsgLiquidate) String() string {
return fmt.Sprintf(`Liquidate Message:
Keeper: %s
Borrower: %s
Collateral Type %s
`, msg.Keeper, msg.Borrower, msg.CollateralType)
}

View File

@ -3,13 +3,10 @@ package types
import (
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
)
// Parameter keys
@ -17,13 +14,11 @@ var (
KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams")
KeyDebtParam = []byte("DebtParam")
KeyDistributionFrequency = []byte("DistributionFrequency")
KeyCircuitBreaker = []byte("CircuitBreaker")
KeyDebtThreshold = []byte("DebtThreshold")
KeyDebtLot = []byte("DebtLot")
KeySurplusThreshold = []byte("SurplusThreshold")
KeySurplusLot = []byte("SurplusLot")
KeySavingsRateDistributed = []byte("SavingsRateDistributed")
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
DefaultCircuitBreaker = false
DefaultCollateralParams = CollateralParams{}
@ -32,7 +27,6 @@ var (
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}
DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt"
@ -42,8 +36,6 @@ var (
DefaultDebtThreshold = sdk.NewInt(100000000000)
DefaultSurplusLot = sdk.NewInt(10000000000)
DefaultDebtLot = sdk.NewInt(10000000000)
DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0))
DefaultSavingsDistributionFrequency = time.Hour * 12
DefaultSavingsRateDistributed = sdk.NewInt(0)
minCollateralPrefix = 0
maxCollateralPrefix = 255
@ -59,7 +51,6 @@ type Params struct {
SurplusAuctionLot sdk.Int `json:"surplus_auction_lot" yaml:"surplus_auction_lot"`
DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
DebtAuctionLot sdk.Int `json:"debt_auction_lot" yaml:"debt_auction_lot"`
SavingsDistributionFrequency time.Duration `json:"savings_distribution_frequency" yaml:"savings_distribution_frequency"`
CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"`
}
@ -73,17 +64,16 @@ func (p Params) String() string {
Surplus Auction Lot: %s
Debt Auction Threshold: %s
Debt Auction Lot: %s
Savings Distribution Frequency: %s
Circuit Breaker: %t`,
p.GlobalDebtLimit, p.CollateralParams, p.DebtParam, p.SurplusAuctionThreshold, p.SurplusAuctionLot,
p.DebtAuctionThreshold, p.DebtAuctionLot, p.SavingsDistributionFrequency, p.CircuitBreaker,
p.DebtAuctionThreshold, p.DebtAuctionLot, p.CircuitBreaker,
)
}
// NewParams returns a new params object
func NewParams(
debtLimit sdk.Coin, collateralParams CollateralParams, debtParam DebtParam, surplusThreshold,
surplusLot, debtThreshold, debtLot sdk.Int, distributionFreq time.Duration, breaker bool,
surplusLot, debtThreshold, debtLot sdk.Int, breaker bool,
) Params {
return Params{
GlobalDebtLimit: debtLimit,
@ -93,7 +83,6 @@ func NewParams(
SurplusAuctionLot: surplusLot,
DebtAuctionThreshold: debtThreshold,
DebtAuctionLot: debtLot,
SavingsDistributionFrequency: distributionFreq,
CircuitBreaker: breaker,
}
}
@ -102,7 +91,7 @@ func NewParams(
func DefaultParams() Params {
return NewParams(
DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParam, DefaultSurplusThreshold,
DefaultSurplusLot, DefaultDebtThreshold, DefaultDebtLot, DefaultSavingsDistributionFrequency,
DefaultSurplusLot, DefaultDebtThreshold, DefaultDebtLot,
DefaultCircuitBreaker,
)
}
@ -119,11 +108,15 @@ type CollateralParam struct {
Prefix byte `json:"prefix" yaml:"prefix"`
SpotMarketID string `json:"spot_market_id" yaml:"spot_market_id"` // marketID of the spot price of the asset from the pricefeed - used for opening CDPs, depositing, withdrawing
LiquidationMarketID string `json:"liquidation_market_id" yaml:"liquidation_market_id"` // marketID of the pricefeed used for liquidation
KeeperRewardPercentage sdk.Dec `json:"keeper_reward_percentage" yaml:"keeper_reward_percentage"` // the percentage of a CDPs collateral that gets rewarded to a keeper that liquidates the position
CheckCollateralizationIndexCount sdk.Int `json:"check_collateralization_index_count" yaml:"check_collateralization_index_count"` // the number of cdps that will be checked for liquidation in the begin blocker
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral
}
// NewCollateralParam returns a new CollateralParam
func NewCollateralParam(denom, ctype string, liqRatio sdk.Dec, debtLimit sdk.Coin, stabilityFee sdk.Dec, auctionSize sdk.Int, liqPenalty sdk.Dec, prefix byte, spotMarketID, liquidationMarketID string, conversionFactor sdk.Int) CollateralParam {
func NewCollateralParam(
denom, ctype string, liqRatio sdk.Dec, debtLimit sdk.Coin, stabilityFee sdk.Dec, auctionSize sdk.Int,
liqPenalty sdk.Dec, prefix byte, spotMarketID, liquidationMarketID string, keeperReward sdk.Dec, checkIndexCount sdk.Int, conversionFactor sdk.Int) CollateralParam {
return CollateralParam{
Denom: denom,
Type: ctype,
@ -135,6 +128,8 @@ func NewCollateralParam(denom, ctype string, liqRatio sdk.Dec, debtLimit sdk.Coi
Prefix: prefix,
SpotMarketID: spotMarketID,
LiquidationMarketID: liquidationMarketID,
KeeperRewardPercentage: keeperReward,
CheckCollateralizationIndexCount: checkIndexCount,
ConversionFactor: conversionFactor,
}
}
@ -152,8 +147,12 @@ func (cp CollateralParam) String() string {
Prefix: %b
Spot Market ID: %s
Liquidation Market ID: %s
Keeper Reward Percentage: %s
Check Collateralization Count: %s
Conversion Factor: %s`,
cp.Denom, cp.Type, cp.LiquidationRatio, cp.StabilityFee, cp.LiquidationPenalty, cp.DebtLimit, cp.AuctionSize, cp.Prefix, cp.SpotMarketID, cp.LiquidationMarketID, cp.ConversionFactor)
cp.Denom, cp.Type, cp.LiquidationRatio, cp.StabilityFee, cp.LiquidationPenalty,
cp.DebtLimit, cp.AuctionSize, cp.Prefix, cp.SpotMarketID, cp.LiquidationMarketID,
cp.KeeperRewardPercentage, cp.CheckCollateralizationIndexCount, cp.ConversionFactor)
}
// CollateralParams array of CollateralParam
@ -174,17 +173,15 @@ type DebtParam struct {
ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor sdk.Int `json:"debt_floor" yaml:"debt_floor"` // minimum active loan size, used to prevent dust
SavingsRate sdk.Dec `json:"savings_rate" yaml:"savings_rate"` // the percentage of stability fees that are redirected to savings rate
}
// NewDebtParam returns a new DebtParam
func NewDebtParam(denom, refAsset string, conversionFactor, debtFloor sdk.Int, savingsRate sdk.Dec) DebtParam {
func NewDebtParam(denom, refAsset string, conversionFactor, debtFloor sdk.Int) DebtParam {
return DebtParam{
Denom: denom,
ReferenceAsset: refAsset,
ConversionFactor: conversionFactor,
DebtFloor: debtFloor,
SavingsRate: savingsRate,
}
}
@ -194,8 +191,7 @@ func (dp DebtParam) String() string {
Reference Asset: %s
Conversion Factor: %s
Debt Floor %s
Savings Rate %s
`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor, dp.SavingsRate)
`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor)
}
// DebtParams array of DebtParam
@ -228,7 +224,6 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs {
params.NewParamSetPair(KeySurplusLot, &p.SurplusAuctionLot, validateSurplusAuctionLotParam),
params.NewParamSetPair(KeyDebtThreshold, &p.DebtAuctionThreshold, validateDebtAuctionThresholdParam),
params.NewParamSetPair(KeyDebtLot, &p.DebtAuctionLot, validateDebtAuctionLotParam),
params.NewParamSetPair(KeyDistributionFrequency, &p.SavingsDistributionFrequency, validateSavingsDistributionFrequencyParam),
}
}
@ -266,10 +261,6 @@ func (p Params) Validate() error {
return err
}
if err := validateSavingsDistributionFrequencyParam(p.SavingsDistributionFrequency); err != nil {
return err
}
if len(p.CollateralParams) == 0 { // default value OK
return nil
}
@ -381,6 +372,12 @@ func validateCollateralParams(i interface{}) error {
if cp.StabilityFee.LT(sdk.OneDec()) || cp.StabilityFee.GT(stabilityFeeMax) {
return fmt.Errorf("stability fee must be ≥ 1.0, ≤ %s, is %s for %s", stabilityFeeMax, cp.StabilityFee, cp.Denom)
}
if cp.KeeperRewardPercentage.IsNegative() || cp.KeeperRewardPercentage.GT(sdk.OneDec()) {
return fmt.Errorf("keeper reward percentage should be between 0 and 1, is %s for %s", cp.KeeperRewardPercentage, cp.Denom)
}
if cp.CheckCollateralizationIndexCount.IsNegative() {
return fmt.Errorf("keeper reward percentage should be positive, is %s for %s", cp.CheckCollateralizationIndexCount, cp.Denom)
}
}
return nil
@ -395,9 +392,6 @@ func validateDebtParam(i interface{}) error {
return fmt.Errorf("debt denom invalid %s", debtParam.Denom)
}
if debtParam.SavingsRate.LT(sdk.ZeroDec()) || debtParam.SavingsRate.GT(sdk.OneDec()) {
return fmt.Errorf("savings rate should be between 0 and 1, is %s for %s", debtParam.SavingsRate, debtParam.Denom)
}
return nil
}
@ -461,16 +455,3 @@ func validateDebtAuctionLotParam(i interface{}) error {
return nil
}
func validateSavingsDistributionFrequencyParam(i interface{}) error {
sdf, ok := i.(time.Duration)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if sdf.Seconds() <= float64(0) {
return fmt.Errorf("savings distribution frequency should be positive: %s", sdf)
}
return nil
}

View File

@ -3,7 +3,6 @@ package types_test
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/suite"
@ -28,7 +27,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot sdk.Int
debtThreshold sdk.Int
debtLot sdk.Int
distributionFreq time.Duration
breaker bool
}
type errArgs struct {
@ -51,7 +49,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -75,7 +72,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -83,13 +82,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -113,7 +110,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -121,13 +120,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -151,7 +148,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -159,13 +158,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -189,7 +186,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "xrp",
@ -202,7 +201,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x21,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -210,13 +211,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -240,7 +239,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "xrp",
@ -253,7 +254,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x21,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -261,13 +264,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -291,7 +292,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "xrp",
@ -304,7 +307,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x21,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -312,13 +317,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -341,7 +344,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -349,13 +354,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -379,7 +382,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "",
LiquidationMarketID: "",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -387,13 +392,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -417,7 +420,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "bnb",
@ -430,7 +435,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x21,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -438,13 +445,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -468,7 +473,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "bnb",
@ -481,7 +488,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x21,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -489,13 +498,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -519,7 +526,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
{
Denom: "xrp",
@ -532,7 +541,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -540,13 +551,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -570,7 +579,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -578,13 +589,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -608,7 +617,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -616,13 +627,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -646,7 +655,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -654,13 +665,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -684,7 +693,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -692,13 +703,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -722,7 +731,9 @@ func (suite *ParamsTestSuite) TestParamValidation() {
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
},
},
debtParam: types.DebtParam{
@ -730,13 +741,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -744,44 +753,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
contains: "debt denom invalid",
},
},
{
name: "invalid debt param savings rate out of range",
args: args{
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{
{
Denom: "bnb",
Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8),
},
},
debtParam: types.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("1.05"),
},
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "savings rate should be between 0 and 1",
},
},
{
name: "nil debt limit",
args: args{
@ -792,7 +763,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -800,24 +770,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
contains: "invalid coins: global debt limit",
},
},
{
name: "zero savings distribution frequency",
args: args{
globalDebtLimit: types.DefaultGlobalDebt,
collateralParams: types.DefaultCollateralParams,
debtParam: types.DefaultDebtParam,
surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: time.Second * 0,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
expectPass: false,
contains: "savings distribution frequency should be positive",
},
},
{
name: "zero surplus auction threshold",
args: args{
@ -828,7 +780,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -846,7 +797,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot,
debtThreshold: sdk.ZeroInt(),
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -864,7 +814,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: sdk.ZeroInt(),
debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -882,7 +831,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold,
debtLot: sdk.ZeroInt(),
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker,
},
errArgs: errArgs{
@ -893,7 +841,7 @@ func (suite *ParamsTestSuite) TestParamValidation() {
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(tc.args.globalDebtLimit, tc.args.collateralParams, tc.args.debtParam, tc.args.surplusThreshold, tc.args.surplusLot, tc.args.debtThreshold, tc.args.debtLot, tc.args.distributionFreq, tc.args.breaker)
params := types.NewParams(tc.args.globalDebtLimit, tc.args.collateralParams, tc.args.debtParam, tc.args.surplusThreshold, tc.args.surplusLot, tc.args.debtThreshold, tc.args.debtLot, tc.args.breaker)
err := params.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err)

View File

@ -68,7 +68,6 @@ func (suite *PermissionTestSuite) TestSubParamChangePermission_Allows() {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}
testDPUpdatedDebtFloor := testDP
testDPUpdatedDebtFloor.DebtFloor = i(1000)

View File

@ -14,7 +14,7 @@ import (
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
cdptypes "github.com/kava-labs/kava/x/cdp/legacy/v0_11"
"github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)

View File

@ -14,7 +14,7 @@ import (
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
cdptypes "github.com/kava-labs/kava/x/cdp/legacy/v0_9"
"github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)

View File

@ -542,7 +542,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}
newDenomDP := testDP
newDenomDP.Denom = "usdz"
@ -565,7 +564,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
name: "allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDebtFloorDP,
@ -575,7 +573,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
name: "un-allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDenomDP,
@ -585,7 +582,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
name: "allowed no change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: testDP, // no change
@ -595,7 +591,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
name: "un-allowed change with allowed change",
allowed: AllowedDebtParam{
DebtFloor: true,
SavingsRate: true,
},
current: testDP,
incoming: newDenomAndDebtFloorDP,

View File

@ -425,15 +425,13 @@ type AllowedDebtParam struct {
ReferenceAsset bool `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor bool `json:"conversion_factor" yaml:"conversion_factor"`
DebtFloor bool `json:"debt_floor" yaml:"debt_floor"`
SavingsRate bool `json:"savings_rate" yaml:"savings_rate"`
}
func (adp AllowedDebtParam) Allows(current, incoming cdptypes.DebtParam) bool {
allowed := ((current.Denom == incoming.Denom) || adp.Denom) &&
((current.ReferenceAsset == incoming.ReferenceAsset) || adp.ReferenceAsset) &&
(current.ConversionFactor.Equal(incoming.ConversionFactor) || adp.ConversionFactor) &&
(current.DebtFloor.Equal(incoming.DebtFloor) || adp.DebtFloor) &&
(current.SavingsRate.Equal(incoming.SavingsRate) || adp.SavingsRate)
(current.DebtFloor.Equal(incoming.DebtFloor) || adp.DebtFloor)
return allowed
}

View File

@ -1,4 +1,4 @@
package v0_12
package v0_11
import (
"errors"
@ -10,6 +10,22 @@ import (
"github.com/cosmos/cosmos-sdk/x/params"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
// ModuleName name that will be used throughout the module
ModuleName = "harvest"
// LPAccount LP distribution module account
LPAccount = "harvest_lp_distribution"
// DelegatorAccount delegator distribution module account
DelegatorAccount = "harvest_delegator_distribution"
// ModuleAccountName name of module account used to hold deposits
ModuleAccountName = "harvest"
)
// Parameter keys and default values
@ -21,10 +37,58 @@ var (
DefaultGovSchedules = DistributionSchedules{}
DefaultLPSchedules = DistributionSchedules{}
DefaultDelegatorSchedules = DelegatorDistributionSchedules{}
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
DefaultDistributionTimes = GenesisDistributionTimes{}
GovDenom = cdptypes.DefaultGovDenom
)
// Params governance parameters for hard module
// GenesisState is the state that must be provided at genesis.
type GenesisState struct {
Params Params `json:"params" yaml:"params"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"`
PreviousDistributionTimes GenesisDistributionTimes `json:"previous_distribution_times" yaml:"previous_distribution_times"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, previousBlockTime time.Time, previousDistTimes GenesisDistributionTimes) GenesisState {
return GenesisState{
Params: params,
PreviousBlockTime: previousBlockTime,
PreviousDistributionTimes: previousDistTimes,
}
}
// Validate performs basic validation of genesis data returning an
// error for any failed validation criteria.
func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
if gs.PreviousBlockTime.Equal(time.Time{}) {
return fmt.Errorf("previous block time not set")
}
for _, gdt := range gs.PreviousDistributionTimes {
if gdt.PreviousDistributionTime.Equal(time.Time{}) {
return fmt.Errorf("previous distribution time not set for %s", gdt.Denom)
}
if err := sdk.ValidateDenom(gdt.Denom); err != nil {
return err
}
}
return nil
}
// GenesisDistributionTime stores the previous distribution time and its corresponding denom
type GenesisDistributionTime struct {
Denom string `json:"denom" yaml:"denom"`
PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"`
}
// GenesisDistributionTimes slice of GenesisDistributionTime
type GenesisDistributionTimes []GenesisDistributionTime
// Params governance parameters for harvest module
type Params struct {
Active bool `json:"active" yaml:"active"`
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
@ -228,7 +292,7 @@ func NewParams(active bool, lps DistributionSchedules, dds DelegatorDistribution
}
}
// DefaultParams returns default params for hard module
// DefaultParams returns default params for harvest module
func DefaultParams() Params {
return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules)
}
@ -342,7 +406,7 @@ func (dt DepositType) IsValid() error {
return fmt.Errorf("invalid deposit type: %s", dt)
}
// Deposit defines an amount of coins deposited into a hard module account
// Deposit defines an amount of coins deposited into a harvest module account
type Deposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"`

View File

@ -8,7 +8,10 @@ import (
// 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)
for _, rp := range k.GetParams(ctx).RewardPeriods {
err := k.AccumulateRewards(ctx, rp)
if err != nil {
panic(err)
}
}
}

View File

@ -35,77 +35,76 @@ const (
var (
// function aliases
CalculateTimeElapsed = keeper.CalculateTimeElapsed
NewKeeper = keeper.NewKeeper
NewQuerier = keeper.NewQuerier
BytesToUint64 = types.BytesToUint64
DefaultGenesisState = types.DefaultGenesisState
DefaultParams = types.DefaultParams
GetClaimPeriodPrefix = types.GetClaimPeriodPrefix
GetClaimPrefix = types.GetClaimPrefix
GetTotalVestingPeriodLength = types.GetTotalVestingPeriodLength
NewAugmentedClaim = types.NewAugmentedClaim
NewClaim = types.NewClaim
NewClaimPeriod = types.NewClaimPeriod
NewGenesisAccumulationTime = types.NewGenesisAccumulationTime
NewGenesisState = types.NewGenesisState
NewMsgClaimReward = types.NewMsgClaimReward
NewMsgClaimUSDXMintingReward = types.NewMsgClaimUSDXMintingReward
NewMultiplier = types.NewMultiplier
NewParams = types.NewParams
NewPeriod = types.NewPeriod
NewQueryClaimsParams = types.NewQueryClaimsParams
NewReward = types.NewReward
NewRewardIndex = types.NewRewardIndex
NewRewardPeriod = types.NewRewardPeriod
NewRewardPeriodFromReward = types.NewRewardPeriodFromReward
NewUSDXMintingClaim = types.NewUSDXMintingClaim
ParamKeyTable = types.ParamKeyTable
RegisterCodec = types.RegisterCodec
// variable aliases
BlockTimeKey = types.BlockTimeKey
ClaimKeyPrefix = types.ClaimKeyPrefix
ClaimPeriodKeyPrefix = types.ClaimPeriodKeyPrefix
DefaultActive = types.DefaultActive
DefaultPreviousBlockTime = types.DefaultPreviousBlockTime
DefaultRewards = types.DefaultRewards
DefaultClaimEnd = types.DefaultClaimEnd
DefaultClaims = types.DefaultClaims
DefaultGenesisAccumulationTimes = types.DefaultGenesisAccumulationTimes
DefaultMultipliers = types.DefaultMultipliers
DefaultRewardPeriods = types.DefaultRewardPeriods
ErrAccountNotFound = types.ErrAccountNotFound
ErrClaimExpired = types.ErrClaimExpired
ErrClaimNotFound = types.ErrClaimNotFound
ErrClaimPeriodNotFound = types.ErrClaimPeriodNotFound
ErrInsufficientModAccountBalance = types.ErrInsufficientModAccountBalance
ErrInvalidAccountType = types.ErrInvalidAccountType
ErrInvalidMultiplier = types.ErrInvalidMultiplier
ErrNoClaimsFound = types.ErrNoClaimsFound
ErrRewardPeriodNotFound = types.ErrRewardPeriodNotFound
ErrZeroClaim = types.ErrZeroClaim
GovDenom = types.GovDenom
IncentiveMacc = types.IncentiveMacc
KeyActive = types.KeyActive
KeyClaimEnd = types.KeyClaimEnd
KeyMultipliers = types.KeyMultipliers
KeyRewards = types.KeyRewards
ModuleCdc = types.ModuleCdc
NextClaimPeriodIDPrefix = types.NextClaimPeriodIDPrefix
PreviousBlockTimeKey = types.PreviousBlockTimeKey
PrincipalDenom = types.PrincipalDenom
RewardPeriodKeyPrefix = types.RewardPeriodKeyPrefix
RewardFactorKey = types.RewardFactorKey
USDXMintingRewardDenom = types.USDXMintingRewardDenom
)
type (
Hooks = keeper.Hooks
Keeper = keeper.Keeper
AccountKeeper = types.AccountKeeper
AugmentedClaim = types.AugmentedClaim
AugmentedClaims = types.AugmentedClaims
CDPHooks = types.CDPHooks
CdpKeeper = types.CdpKeeper
Claim = types.Claim
ClaimPeriod = types.ClaimPeriod
ClaimPeriods = types.ClaimPeriods
Claims = types.Claims
GenesisClaimPeriodID = types.GenesisClaimPeriodID
GenesisClaimPeriodIDs = types.GenesisClaimPeriodIDs
GenesisAccumulationTime = types.GenesisAccumulationTime
GenesisAccumulationTimes = types.GenesisAccumulationTimes
GenesisState = types.GenesisState
MsgClaimReward = types.MsgClaimReward
MsgClaimUSDXMintingReward = types.MsgClaimUSDXMintingReward
Multiplier = types.Multiplier
MultiplierName = types.MultiplierName
Multipliers = types.Multipliers
Params = types.Params
PostClaimReq = types.PostClaimReq
QueryClaimsParams = types.QueryClaimsParams
Reward = types.Reward
RewardIndex = types.RewardIndex
RewardIndexes = types.RewardIndexes
RewardPeriod = types.RewardPeriod
RewardPeriods = types.RewardPeriods
Rewards = types.Rewards
SupplyKeeper = types.SupplyKeeper
USDXMintingClaim = types.USDXMintingClaim
USDXMintingClaims = types.USDXMintingClaims
)

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags"
@ -25,35 +26,42 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
incentiveQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc),
queryRewardPeriodsCmd(queryRoute, cdc),
queryClaimPeriodsCmd(queryRoute, cdc),
)...)
return incentiveQueryCmd
}
const (
flagOwner = "owner"
)
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claims [owner-addr] [collateral-type]",
Short: "get claims by owner and collateral-type",
cmd := &cobra.Command{
Use: "claims ",
Short: "query USDX minting claims",
Long: strings.TrimSpace(
fmt.Sprintf(`Get all claims owned by the owner address for the particular collateral type.
fmt.Sprintf(`Query USDX minting claims with optional flag for finding claims for a specifc owner
Example:
$ %s query %s claims kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb-a`, version.ClientName, types.ModuleName)),
Args: cobra.ExactArgs(2),
$ %s query %s claims
$ %s query %s claims --owner kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw
`,
version.ClientName, types.ModuleName, version.ClientName, types.ModuleName)),
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc)
strOwner := viper.GetString(flagOwner)
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
// Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0])
owner, err := sdk.AccAddressFromBech32(strOwner)
if err != nil {
return err
}
bz, err := cdc.MarshalJSON(types.QueryClaimsParams{
Owner: ownerAddress,
CollateralType: args[1],
})
params := types.NewQueryClaimsParams(page, limit, owner)
bz, err := cdc.MarshalJSON(params)
if err != nil {
return err
}
@ -66,7 +74,7 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
}
cliCtx = cliCtx.WithHeight(height)
var claims types.AugmentedClaims
var claims types.USDXMintingClaims
if err := cdc.UnmarshalJSON(res, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims: %w", err)
}
@ -74,6 +82,10 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
},
}
cmd.Flags().String(flagOwner, "", "(optional) filter by claim owner address")
cmd.Flags().Int(flags.FlagPage, 1, "pagination page of CDPs to to query for")
cmd.Flags().Int(flags.FlagLimit, 100, "pagination limit of CDPs to query for")
return cmd
}
func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
@ -102,57 +114,3 @@ func queryParamsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
},
}
}
func queryRewardPeriodsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "reward-periods",
Short: "get active reward periods",
Long: "Get the current set of active incentive reward periods.",
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.QueryGetRewardPeriods)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var rewardPeriods types.RewardPeriods
if err := cdc.UnmarshalJSON(res, &rewardPeriods); err != nil {
return fmt.Errorf("failed to unmarshal reward periods: %w", err)
}
return cliCtx.PrintOutput(rewardPeriods)
},
}
}
func queryClaimPeriodsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claim-periods",
Short: "get active claim periods",
Long: "Get the current set of active incentive claim periods.",
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.QueryGetClaimPeriods)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
return err
}
cliCtx = cliCtx.WithHeight(height)
// Decode and print results
var claimPeriods types.ClaimPeriods
if err := cdc.UnmarshalJSON(res, &claimPeriods); err != nil {
return fmt.Errorf("failed to unmarshal claim periods: %w", err)
}
return cliCtx.PrintOutput(claimPeriods)
},
}
}

View File

@ -35,16 +35,16 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "claim [owner] [collateral-type] [multiplier]",
Use: "claim [owner] [multiplier]",
Short: "claim rewards for cdp owner and collateral-type",
Long: strings.TrimSpace(
fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input collateral-type and multiplier,
Example:
$ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb-a large
$ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw large
`, version.ClientName, types.ModuleName),
),
Args: cobra.ExactArgs(3),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc)
@ -54,7 +54,7 @@ func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return err
}
msg := types.NewMsgClaimReward(owner, args[1], args[2])
msg := types.NewMsgClaimUSDXMintingReward(owner, args[1])
err = msg.ValidateBasic()
if err != nil {
return err

View File

@ -3,6 +3,7 @@ package rest
import (
"fmt"
"net/http"
"strings"
"github.com/gorilla/mux"
@ -14,30 +15,33 @@ import (
)
func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc(fmt.Sprintf("/%s/claims/{%s}/{%s}", types.ModuleName, types.RestClaimOwner, types.RestClaimCollateralType), queryClaimsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/rewardperiods", types.ModuleName), queryRewardPeriodsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/%s/claimperiods", types.ModuleName), queryClaimPeriodsHandlerFn(cliCtx)).Methods("GET")
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) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return
}
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)
if !ok {
return
}
vars := mux.Vars(r)
ownerBech32 := vars[types.RestClaimOwner]
denom := vars[types.RestClaimCollateralType]
owner, err := sdk.AccAddressFromBech32(ownerBech32)
var owner sdk.AccAddress
if x := r.URL.Query().Get(types.RestClaimOwner); len(x) != 0 {
ownerStr := strings.ToLower(strings.TrimSpace(x))
owner, err = sdk.AccAddressFromBech32(ownerStr)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("cannot parse address from claim owner %s", ownerStr))
return
}
}
queryParams := types.NewQueryClaimsParams(owner, denom)
queryParams := types.NewQueryClaimsParams(page, limit, owner)
bz, err := cliCtx.Codec.MarshalJSON(queryParams)
if err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, fmt.Sprintf("failed to marshal query params: %s", err))
@ -55,46 +59,6 @@ func queryClaimsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
}
}
func queryRewardPeriodsHandlerFn(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/%s", types.QuerierRoute, types.QueryGetRewardPeriods)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryClaimPeriodsHandlerFn(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/%s", types.QuerierRoute, types.QueryGetClaimPeriods)
res, height, err := cliCtx.QueryWithData(route, nil)
if err != nil {
rest.WriteErrorResponse(w, http.StatusInternalServerError, err.Error())
return
}
cliCtx = cliCtx.WithHeight(height)
rest.PostProcessResponse(w, cliCtx, res)
}
}
func queryParamsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
cliCtx, ok := rest.ParseQueryHeightOrReturnBadRequest(w, cliCtx, r)

View File

@ -42,7 +42,7 @@ func postClaimHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}
msg := types.NewMsgClaimReward(requestBody.Sender, requestBody.CollateralType, requestBody.MultiplierName)
msg := types.NewMsgClaimUSDXMintingReward(requestBody.Sender, requestBody.MultiplierName)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return

View File

@ -24,51 +24,37 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetParams(ctx, gs.Params)
for _, r := range gs.Params.Rewards {
k.SetNextClaimPeriodID(ctx, r.CollateralType, 1)
for _, gat := range gs.PreviousAccumulationTimes {
k.SetPreviousAccrualTime(ctx, gat.CollateralType, gat.PreviousAccumulationTime)
k.SetRewardFactor(ctx, gat.CollateralType, gat.RewardFactor)
}
// 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.CollateralType, id.ID)
for _, claim := range gs.USDXMintingClaims {
k.SetClaim(ctx, claim)
}
}
// 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)
var gats GenesisAccumulationTimes
for _, rp := range params.RewardPeriods {
pat, found := k.GetPreviousAccrualTime(ctx, rp.CollateralType)
if !found {
pat = ctx.BlockTime()
}
factor, found := k.GetRewardFactor(ctx, rp.CollateralType)
if !found {
factor = sdk.ZeroDec()
}
gat := types.NewGenesisAccumulationTime(rp.CollateralType, pat, factor)
gats = append(gats, gat)
}
return types.NewGenesisState(params, gats, claims)
}

View File

@ -1,8 +1,6 @@
package incentive
import (
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -15,36 +13,20 @@ 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)
case types.MsgClaimUSDXMintingReward:
return handleMsgClaimUSDXMintingReward(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) {
func handleMsgClaimUSDXMintingReward(ctx sdk.Context, k keeper.Keeper, msg types.MsgClaimUSDXMintingReward) (*sdk.Result, error) {
claims, found := k.GetActiveClaimsByAddressAndCollateralType(ctx, msg.Sender, msg.CollateralType)
if !found {
return nil, sdkerrors.Wrapf(types.ErrNoClaimsFound, "address: %s, collateral type: %s", msg.Sender, msg.CollateralType)
}
for _, claim := range claims {
err := k.PayoutClaim(ctx, claim.Owner, claim.CollateralType, claim.ClaimPeriodID, types.MultiplierName(strings.ToLower(msg.MultiplierName)))
err := k.ClaimReward(ctx, msg.Sender, types.MultiplierName(msg.MultiplierName))
if err != nil {
return nil, err
}
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
),
)
return &sdk.Result{
Events: ctx.EventManager().Events(),
}, nil

View File

@ -13,6 +13,7 @@ import (
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
)
@ -41,7 +42,16 @@ func (suite *HandlerTestSuite) SetupTest() {
coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
}
authGS := app.NewAuthGenState(addrs, coins)
tApp.InitializeFromGenesisStates(authGS)
incentiveGS := incentive.NewGenesisState(
incentive.NewParams(
incentive.RewardPeriods{incentive.NewRewardPeriod(true, "bnb-a", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 12, 15, 14, 0, 0, 0, time.UTC), c("ukava", 122354))},
incentive.Multipliers{incentive.NewMultiplier(incentive.MultiplierName("small"), 1, d("0.25")), incentive.NewMultiplier(incentive.MultiplierName("large"), 12, d("1.0"))},
time.Date(2025, 12, 15, 14, 0, 0, 0, time.UTC),
),
incentive.DefaultGenesisAccumulationTimes,
incentive.DefaultClaims,
)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(incentiveGS)})
suite.addrs = addrs
suite.handler = incentive.NewHandler(keeper)
@ -51,15 +61,10 @@ func (suite *HandlerTestSuite) SetupTest() {
}
func (suite *HandlerTestSuite) addClaim() {
supplyKeeper := suite.app.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(suite.ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(suite.ctx, macc.GetName(), cs(c("ukava", 1000000)))
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000)))
suite.Require().NoError(err)
cp := incentive.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), incentive.Multipliers{incentive.NewMultiplier(incentive.Small, 1, sdk.MustNewDecFromStr("0.33"))})
suite.NotPanics(func() {
suite.keeper.SetClaimPeriod(suite.ctx, cp)
})
c1 := incentive.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
c1 := incentive.NewUSDXMintingClaim(suite.addrs[0], c("ukava", 1000000), types.RewardIndexes{types.NewRewardIndex("bnb-s", sdk.ZeroDec())})
suite.NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c1)
})
@ -67,7 +72,7 @@ func (suite *HandlerTestSuite) addClaim() {
func (suite *HandlerTestSuite) TestMsgClaimReward() {
suite.addClaim()
msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb", "small")
msg := incentive.NewMsgClaimUSDXMintingReward(suite.addrs[0], "small")
res, err := suite.handler(suite.ctx, msg)
suite.NoError(err)
suite.Require().NotNil(res)
@ -75,3 +80,7 @@ func (suite *HandlerTestSuite) TestMsgClaimReward() {
func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite))
}
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }

View File

@ -0,0 +1,28 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
)
// Hooks wrapper struct for hooks
type Hooks struct {
k Keeper
}
var _ cdptypes.CDPHooks = Hooks{}
// Hooks create new incentive hooks
func (k Keeper) Hooks() Hooks { return Hooks{k} }
// AfterCDPCreated function that runs after a cdp is created
func (h Hooks) AfterCDPCreated(ctx sdk.Context, cdp cdptypes.CDP) {
h.k.InitializeClaim(ctx, cdp)
}
// BeforeCDPModified function that runs before a cdp is modified
// note that this is called immediately after interest is synchronized, and so could potentially
// be called AfterCDPInterestUpdated or something like that, if we we're to expand the scope of cdp hooks
func (h Hooks) BeforeCDPModified(ctx sdk.Context, cdp cdptypes.CDP) {
h.k.SynchronizeReward(ctx, cdp)
}

View File

@ -0,0 +1,212 @@
package keeper_test
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/pricefeed"
)
func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
CollateralParams: cdp.CollateralParams{
{
Denom: "xrp",
Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000),
Prefix: 0x20,
SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd",
ConversionFactor: i(6),
},
{
Denom: "btc",
Type: "btc-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000),
Prefix: 0x21,
SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd",
ConversionFactor: i(8),
},
{
Denom: "bnb",
Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(50000000000),
Prefix: 0x22,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
ConversionFactor: i(8),
},
{
Denom: "busd",
Type: "busd-a",
LiquidationRatio: d("1.01"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.OneDec(), // %0 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(10000000000),
Prefix: 0x23,
SpotMarketID: "busd:usd",
LiquidationMarketID: "busd:usd",
ConversionFactor: i(8),
},
},
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("btc-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("xrp-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("busd-a", time.Time{}, sdk.OneDec()),
cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("btc-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("xrp-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("busd-a", sdk.ZeroInt()),
cdp.NewGenesisTotalPrincipal("bnb-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}
func NewPricefeedGenStateMulti() app.GenesisState {
pfGenesis := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "xrp:usd", BaseAsset: "xrp", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "busd:usd", BaseAsset: "busd", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "btc:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("8000.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "xrp:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("0.25"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("17.25"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "busd:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.OneDec(),
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
}
func NewIncentiveGenState(previousAccumTime, endTime time.Time, rewardPeriods ...incentive.RewardPeriod) app.GenesisState {
var accumulationTimes incentive.GenesisAccumulationTimes
for _, rp := range rewardPeriods {
accumulationTimes = append(
accumulationTimes,
incentive.NewGenesisAccumulationTime(
rp.CollateralType,
previousAccumTime,
sdk.ZeroDec(),
),
)
}
genesis := incentive.NewGenesisState(
incentive.NewParams(
rewardPeriods,
incentive.Multipliers{
incentive.NewMultiplier(incentive.Small, 1, d("0.25")),
incentive.NewMultiplier(incentive.Large, 12, d("1.0")),
},
endTime,
),
accumulationTimes,
incentive.USDXMintingClaims{},
)
return app.GenesisState{incentive.ModuleName: incentive.ModuleCdc.MustMarshalJSON(genesis)}
}
func NewCDPGenStateHighInterest() app.GenesisState {
cdpGenesis := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
CollateralParams: cdp.CollateralParams{
{
Denom: "bnb",
Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000051034942716"), // 500% APR
LiquidationPenalty: d("0.05"),
AuctionSize: i(50000000000),
Prefix: 0x22,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
ConversionFactor: i(8),
},
},
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousAccumulationTimes: cdp.GenesisAccumulationTimes{
cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec()),
},
TotalPrincipals: cdp.GenesisTotalPrincipals{
cdp.NewGenesisTotalPrincipal("bnb-a", sdk.ZeroInt()),
},
}
return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
}

View File

@ -37,182 +37,39 @@ func NewKeeper(
}
}
// GetRewardPeriod returns the reward period from the store for the input collateral type and a boolean for if it was found
func (k Keeper) GetRewardPeriod(ctx sdk.Context, collateralType string) (types.RewardPeriod, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
bz := store.Get([]byte(collateralType))
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.CollateralType), bz)
}
// DeleteRewardPeriod deletes the reward period in the store for the input collateral type,
func (k Keeper) DeleteRewardPeriod(ctx sdk.Context, collateralType string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(collateralType))
}
// 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 collateral type
func (k Keeper) GetNextClaimPeriodID(ctx sdk.Context, collateralType string) uint64 {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
bz := store.Get([]byte(collateralType))
if bz == nil {
k.SetNextClaimPeriodID(ctx, collateralType, 1)
return uint64(1)
}
return types.BytesToUint64(bz)
}
// SetNextClaimPeriodID sets the highest claim period id in the store for the input collateral type
func (k Keeper) SetNextClaimPeriodID(ctx sdk.Context, collateralType string, id uint64) {
store := prefix.NewStore(ctx.KVStore(k.key), types.NextClaimPeriodIDPrefix)
store.Set([]byte(collateralType), sdk.Uint64ToBigEndian(id))
}
// IterateClaimPeriodIDKeysAndValues iterates over the claim period id (value) and collateral type (key) of each claim period id in the store and performs a callback function
func (k Keeper) IterateClaimPeriodIDKeysAndValues(ctx sdk.Context, cb func(collateralType 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())
collateralType := string(iterator.Key())
if cb(collateralType, id) {
break
}
}
}
// GetAllClaimPeriodIDPairs returns all collateralType:nextClaimPeriodID pairs in the store
func (k Keeper) GetAllClaimPeriodIDPairs(ctx sdk.Context) types.GenesisClaimPeriodIDs {
ids := types.GenesisClaimPeriodIDs{}
k.IterateClaimPeriodIDKeysAndValues(ctx, func(collateralType string, id uint64) (stop bool) {
genID := types.GenesisClaimPeriodID{
CollateralType: collateralType,
ID: id,
}
ids = append(ids, genID)
return false
})
return ids
}
// GetClaimPeriod returns claim period in the store for the input ID and collateral type and a boolean for if it was found
func (k Keeper) GetClaimPeriod(ctx sdk.Context, id uint64, collateralType string) (types.ClaimPeriod, bool) {
var cp types.ClaimPeriod
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
bz := store.Get(types.GetClaimPeriodPrefix(collateralType, 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 collateral type
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.CollateralType, cp.ID), bz)
}
// DeleteClaimPeriod deletes the claim period in the store for the input ID and collateral type
func (k Keeper) DeleteClaimPeriod(ctx sdk.Context, id uint64, collateralType string) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimPeriodKeyPrefix)
store.Delete(types.GetClaimPeriodPrefix(collateralType, 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 collateral type and id and a boolean for if the claim was found
func (k Keeper) GetClaim(ctx sdk.Context, addr sdk.AccAddress, collateralType string, id uint64) (types.Claim, bool) {
func (k Keeper) GetClaim(ctx sdk.Context, addr sdk.AccAddress) (types.USDXMintingClaim, bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := store.Get(types.GetClaimPrefix(addr, collateralType, id))
bz := store.Get(addr)
if bz == nil {
return types.Claim{}, false
return types.USDXMintingClaim{}, false
}
var c types.Claim
var c types.USDXMintingClaim
k.cdc.MustUnmarshalBinaryBare(bz, &c)
return c, true
}
// SetClaim sets the claim in the store corresponding to the input address, collateral type, and id
func (k Keeper) SetClaim(ctx sdk.Context, c types.Claim) {
func (k Keeper) SetClaim(ctx sdk.Context, c types.USDXMintingClaim) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(c)
store.Set(types.GetClaimPrefix(c.Owner, c.CollateralType, c.ClaimPeriodID), bz)
store.Set(c.Owner, bz)
}
// DeleteClaim deletes the claim in the store corresponding to the input address, collateral type, and id
func (k Keeper) DeleteClaim(ctx sdk.Context, owner sdk.AccAddress, collateralType string, id uint64) {
func (k Keeper) DeleteClaim(ctx sdk.Context, owner sdk.AccAddress) {
store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
store.Delete(types.GetClaimPrefix(owner, collateralType, id))
store.Delete(owner)
}
// 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)) {
func (k Keeper) IterateClaims(ctx sdk.Context, cb func(c types.USDXMintingClaim) (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
var c types.USDXMintingClaim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c)
if cb(c) {
break
@ -221,28 +78,61 @@ func (k Keeper) IterateClaims(ctx sdk.Context, cb func(c types.Claim) (stop bool
}
// 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) {
func (k Keeper) GetAllClaims(ctx sdk.Context) types.USDXMintingClaims {
cs := types.USDXMintingClaims{}
k.IterateClaims(ctx, func(c types.USDXMintingClaim) (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 {
// GetPreviousAccrualTime returns the last time a collateral type accrued rewards
func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, ctype string) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BlockTimeKey)
bz := store.Get([]byte(ctype))
if bz == nil {
return time.Time{}, false
}
k.cdc.MustUnmarshalBinaryBare(b, &blockTime)
k.cdc.MustUnmarshalBinaryBare(bz, &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))
// SetPreviousAccrualTime sets the last time a collateral type accrued rewards
func (k Keeper) SetPreviousAccrualTime(ctx sdk.Context, ctype string, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BlockTimeKey)
store.Set([]byte(ctype), k.cdc.MustMarshalBinaryBare(blockTime))
}
// IterateAccrualTimes iterates over all previous accrual times and preforms a callback function
func (k Keeper) IterateAccrualTimes(ctx sdk.Context, cb func(string, time.Time) (stop bool)) {
store := prefix.NewStore(ctx.KVStore(k.key), types.BlockTimeKey)
iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close()
for ; iterator.Valid(); iterator.Next() {
var accrualTime time.Time
var collateralType string
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &collateralType)
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &accrualTime)
if cb(collateralType, accrualTime) {
break
}
}
}
// GetRewardFactor returns the current reward factor for an individual collateral type
func (k Keeper) GetRewardFactor(ctx sdk.Context, ctype string) (factor sdk.Dec, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardFactorKey)
bz := store.Get([]byte(ctype))
if bz == nil {
return sdk.ZeroDec(), false
}
k.cdc.MustUnmarshalBinaryBare(bz, &factor)
return factor, true
}
// SetRewardFactor sets the current reward factor for an individual collateral type
func (k Keeper) SetRewardFactor(ctx sdk.Context, ctype string, factor sdk.Dec) {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardFactorKey)
store.Set([]byte(ctype), k.cdc.MustMarshalBinaryBare(factor))
}

View File

@ -2,12 +2,13 @@ package keeper_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types"
@ -33,7 +34,7 @@ func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(1)
_, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp
suite.ctx = ctx
@ -51,129 +52,63 @@ func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.Modul
return sk.GetModuleAccount(suite.ctx, name)
}
func (suite *KeeperTestSuite) TestGetSetDeleteRewardPeriod() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
_, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.False(found)
suite.NotPanics(func() {
suite.keeper.SetRewardPeriod(suite.ctx, rp)
})
testRP, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.True(found)
suite.Equal(rp, testRP)
suite.NotPanics(func() {
suite.keeper.DeleteRewardPeriod(suite.ctx, "bnb")
})
_, found = suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetDeleteClaimPeriod() {
cp := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
suite.NotPanics(func() {
suite.keeper.SetClaimPeriod(suite.ctx, cp)
})
testCP, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
suite.Equal(cp, testCP)
suite.NotPanics(func() {
suite.keeper.DeleteClaimPeriod(suite.ctx, 1, "bnb")
})
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
}
func (suite *KeeperTestSuite) TestGetSetClaimPeriodID() {
suite.NotPanics(func() {
suite.keeper.GetNextClaimPeriodID(suite.ctx, "yolo")
})
suite.NotPanics(func() {
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
})
testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb")
suite.Equal(uint64(1), testID)
testID = suite.keeper.GetNextClaimPeriodID(suite.ctx, "yolo")
suite.Equal(uint64(1), testID)
}
func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
c := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
_, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
suite.NotPanics(func() {
c := types.NewUSDXMintingClaim(suite.addrs[0], c("ukava", 1000000), types.RewardIndexes{types.NewRewardIndex("bnb-a", sdk.ZeroDec())})
_, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.Require().False(found)
suite.Require().NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c)
})
testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.True(found)
suite.Equal(c, testC)
suite.NotPanics(func() {
suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0], "bnb", 1)
testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.Require().True(found)
suite.Require().Equal(c, testC)
suite.Require().NotPanics(func() {
suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0])
})
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.Require().False(found)
}
func (suite *KeeperTestSuite) TestIterateMethods() {
suite.addObjectsToStore() // adds 2 objects of each type to the store
var rewardPeriods types.RewardPeriods
suite.keeper.IterateRewardPeriods(suite.ctx, func(rp types.RewardPeriod) (stop bool) {
rewardPeriods = append(rewardPeriods, rp)
return false
func (suite *KeeperTestSuite) TestIterateClaims() {
for i := 0; i < len(suite.addrs); i++ {
c := types.NewUSDXMintingClaim(suite.addrs[i], c("ukava", 100000), types.RewardIndexes{types.NewRewardIndex("bnb-a", sdk.ZeroDec())})
suite.Require().NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c)
})
suite.Equal(2, len(rewardPeriods))
var claimPeriods types.ClaimPeriods
suite.keeper.IterateClaimPeriods(suite.ctx, func(cp types.ClaimPeriod) (stop bool) {
claimPeriods = append(claimPeriods, cp)
return false
})
suite.Equal(2, len(claimPeriods))
var claims types.Claims
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
}
claims := types.USDXMintingClaims{}
suite.keeper.IterateClaims(suite.ctx, func(c types.USDXMintingClaim) bool {
claims = append(claims, c)
return false
})
suite.Equal(2, len(claims))
var genIDs types.GenesisClaimPeriodIDs
suite.keeper.IterateClaimPeriodIDKeysAndValues(suite.ctx, func(collateralType string, id uint64) (stop bool) {
genID := types.GenesisClaimPeriodID{CollateralType: collateralType, ID: id}
genIDs = append(genIDs, genID)
return false
})
suite.Equal(2, len(genIDs))
}
func (suite *KeeperTestSuite) addObjectsToStore() {
rp1 := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
rp2 := types.NewRewardPeriod("xrp", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
suite.keeper.SetRewardPeriod(suite.ctx, rp1)
suite.keeper.SetRewardPeriod(suite.ctx, rp2)
cp1 := types.NewClaimPeriod("bnb", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
cp2 := types.NewClaimPeriod("xrp", 1, suite.ctx.BlockTime().Add(time.Hour*168), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
suite.keeper.SetClaimPeriod(suite.ctx, cp1)
suite.keeper.SetClaimPeriod(suite.ctx, cp2)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 2)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "xrp", 2)
c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
c2 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "xrp", 1)
suite.keeper.SetClaim(suite.ctx, c1)
suite.keeper.SetClaim(suite.ctx, c2)
params := types.NewParams(
true, types.Rewards{types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))}, time.Hour*7*24)},
)
suite.keeper.SetParams(suite.ctx, params)
suite.Require().Equal(len(suite.addrs), len(claims))
claims = suite.keeper.GetAllClaims(suite.ctx)
suite.Require().Equal(len(suite.addrs), len(claims))
}
func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite))
}
func createPeriodicVestingAccount(origVesting sdk.Coins, periods vesting.Periods, startTime, endTime int64) (*vesting.PeriodicVestingAccount, error) {
_, addr := app.GeneratePrivKeyAddressPairs(1)
bacc := auth.NewBaseAccountWithAddress(addr[0])
bacc.Coins = origVesting
bva, err := vesting.NewBaseVestingAccount(&bacc, origVesting, endTime)
if err != nil {
return &vesting.PeriodicVestingAccount{}, err
}
pva := vesting.NewPeriodicVestingAccountRaw(bva, startTime, periods)
err = pva.Validate()
if err != nil {
return &vesting.PeriodicVestingAccount{}, err
}
return pva, nil
}
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }

View File

@ -1,6 +1,8 @@
package keeper
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/types"
@ -17,3 +19,31 @@ func (k Keeper) GetParams(ctx sdk.Context) types.Params {
func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &params)
}
// GetRewardPeriod returns the reward period with the specified collateral type if it's found in the params
func (k Keeper) GetRewardPeriod(ctx sdk.Context, collateralType string) (types.RewardPeriod, bool) {
params := k.GetParams(ctx)
for _, rp := range params.RewardPeriods {
if rp.CollateralType == collateralType {
return rp, true
}
}
return types.RewardPeriod{}, false
}
// GetMultiplier returns the multiplier with the specified name if it's found in the params
func (k Keeper) GetMultiplier(ctx sdk.Context, name types.MultiplierName) (types.Multiplier, bool) {
params := k.GetParams(ctx)
for _, m := range params.ClaimMultipliers {
if m.Name == name {
return m, true
}
}
return types.Multiplier{}, false
}
// GetClaimEnd returns the claim end time for the params
func (k Keeper) GetClaimEnd(ctx sdk.Context) time.Time {
params := k.GetParams(ctx)
return params.ClaimEnd
}

View File

@ -1,8 +1,6 @@
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"
@ -14,42 +12,48 @@ import (
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, collateralType string, id uint64, multiplierName types.MultiplierName) error {
claim, found := k.GetClaim(ctx, addr, collateralType, id)
// ClaimReward sends the reward amount to the input address and zero's out the claim in the store
func (k Keeper) ClaimReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error {
claim, found := k.GetClaim(ctx, addr)
if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, collateral type %s, address: %s", id, collateralType, addr)
}
claimPeriod, found := k.GetClaimPeriod(ctx, id, collateralType)
if !found {
return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, collateral type: %s", id, collateralType)
return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr)
}
multiplier, found := claimPeriod.GetMultiplier(multiplierName)
multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName))
}
rewardAmount := sdk.NewDecFromInt(claim.Reward.Amount).Mul(multiplier.Factor).RoundInt()
claimEnd := k.GetClaimEnd(ctx)
if ctx.BlockTime().After(claimEnd) {
return sdkerrors.Wrapf(types.ErrClaimExpired, "block time %s > claim end time %s", ctx.BlockTime(), claimEnd)
}
claim, err := k.SynchronizeClaim(ctx, claim)
if err != nil {
return err
}
rewardAmount := claim.Reward.Amount.ToDec().Mul(multiplier.Factor).RoundInt()
if rewardAmount.IsZero() {
return types.ErrZeroClaim
}
rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount)
length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix()
err := k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), length)
err = k.SendTimeLockedCoinsToAccount(ctx, types.IncentiveMacc, addr, sdk.NewCoins(rewardCoin), length)
if err != nil {
return err
}
k.DeleteClaim(ctx, addr, collateralType, id)
k.ZeroClaim(ctx, claim)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, addr.String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()),
sdk.NewAttribute(types.AttributeKeyClaimPeriod, fmt.Sprintf("%d", claim.ClaimPeriodID)),
),
)
return nil
@ -112,75 +116,6 @@ func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule s
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.CollateralType == cp.CollateralType && c.ClaimPeriodID == cp.ID) {
return false
}
k.DeleteClaim(ctx, c.Owner, c.CollateralType, c.ClaimPeriodID)
return false
})
k.DeleteClaimPeriod(ctx, cp.ID, cp.CollateralType)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaimPeriodExpiry,
sdk.NewAttribute(types.AttributeKeyClaimPeriod, cp.String()),
),
)
return false
})
}
// GetActiveClaimsByAddressAndCollateralType returns all claims for a specific user and address and a bool for if any were found
func (k Keeper) GetActiveClaimsByAddressAndCollateralType(ctx sdk.Context, addr sdk.AccAddress, collateralType string) (claims types.Claims, found bool) {
found = false
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if cp.CollateralType != collateralType {
return false
}
c, hasClaim := k.GetClaim(ctx, addr, cp.CollateralType, cp.ID)
if !hasClaim {
return false
}
found = true
claims = append(claims, c)
return false
})
return claims, found
}
// GetAllClaimsByAddressAndCollateralType returns all claims for a specific user and address and a bool for if any were found
func (k Keeper) GetAllClaimsByAddressAndCollateralType(ctx sdk.Context, addr sdk.AccAddress, collateralType string) (claims types.AugmentedClaims, found bool) {
found = false
k.IterateClaimPeriods(ctx, func(cp types.ClaimPeriod) (stop bool) {
if cp.CollateralType != collateralType {
return false
}
c, hasClaim := k.GetClaim(ctx, addr, cp.CollateralType, cp.ID)
if !hasClaim {
return false
}
ac := types.NewAugmentedClaim(c, true)
found = true
claims = append(claims, ac)
return false
})
nextClaimID := k.GetNextClaimPeriodID(ctx, collateralType)
c, hasClaim := k.GetClaim(ctx, addr, collateralType, nextClaimID)
if !hasClaim {
return claims, found
}
ac := types.NewAugmentedClaim(c, false)
claims = append(claims, ac)
return claims, true
}
// 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) {

View File

@ -2,119 +2,154 @@ package keeper_test
import (
"errors"
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
"github.com/tendermint/tendermint/crypto"
)
func (suite *KeeperTestSuite) setupChain() {
// creates a new app state with 4 funded addresses and 1 module account
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, addrs := app.GeneratePrivKeyAddressPairs(4)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
})
tApp.InitializeFromGenesisStates(
authGS,
func (suite *KeeperTestSuite) TestPayoutClaim() {
type args struct {
ctype string
rewardsPerSecond sdk.Coin
initialTime time.Time
initialCollateral sdk.Coin
initialPrincipal sdk.Coin
multipliers types.Multipliers
multiplier types.MultiplierName
timeElapsed int
expectedBalance sdk.Coins
expectedPeriods vesting.Periods
isPeriodicVestingAccount bool
}
type errArgs struct {
expectPass bool
contains string
}
type test struct {
name string
args args
errArgs errArgs
}
testCases := []test{
{
"valid 1 day",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 10000000000),
multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
multiplier: types.MultiplierName("large"),
timeElapsed: 86400,
expectedBalance: cs(c("usdx", 10000000000), c("ukava", 10571385600)),
expectedPeriods: vesting.Periods{vesting.Period{Length: 31536000, Amount: cs(c("ukava", 10571385600))}},
isPeriodicVestingAccount: true,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"invalid zero rewards",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 0),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 10000000000),
multipliers: types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
multiplier: types.MultiplierName("large"),
timeElapsed: 86400,
expectedBalance: cs(c("usdx", 10000000000)),
expectedPeriods: vesting.Periods{},
isPeriodicVestingAccount: false,
},
errArgs{
expectPass: false,
contains: "claim amount rounds to zero",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupWithCDPGenState()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup incentive state
params := types.NewParams(
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
tc.args.multipliers,
tc.args.initialTime.Add(time.Hour*24*365*5),
)
supplyKeeper := tApp.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 600)))
suite.keeper.SetParams(suite.ctx, params)
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime)
suite.keeper.SetRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec())
// setup account state
sk := suite.app.GetSupplyKeeper()
err := sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral))
suite.Require().NoError(err)
err = sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral))
suite.Require().NoError(err)
// sets addrs[0] to be a periodic vesting account
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, addrs[0])
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
periods := vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
}
bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods)
ak.SetAccount(ctx, pva)
// setup kavadist state
err = sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000)))
suite.Require().NoError(err)
// sets addrs[2] to be a validator vesting account
acc = ak.GetAccount(ctx, addrs[2])
bacc = auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err2 = vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
vva := validatorvesting.NewValidatorVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90)
ak.SetAccount(ctx, vva)
suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx
suite.addrs = addrs
// setup cdp state
cdpKeeper := suite.app.GetCDPKeeper()
err = cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype)
suite.Require().NoError(err)
claim, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.Require().True(found)
suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor)
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.args.ctype)
suite.Require().True(found)
err = suite.keeper.AccumulateRewards(suite.ctx, rewardPeriod)
suite.Require().NoError(err)
err = suite.keeper.ClaimReward(suite.ctx, suite.addrs[0], tc.args.multiplier)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
ak := suite.app.GetAccountKeeper()
acc := ak.GetAccount(suite.ctx, suite.addrs[0])
suite.Require().Equal(tc.args.expectedBalance, acc.GetCoins())
if tc.args.isPeriodicVestingAccount {
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
suite.Require().True(ok)
suite.Require().Equal(tc.args.expectedPeriods, vacc.VestingPeriods)
}
func (suite *KeeperTestSuite) setupExpiredClaims() {
// creates a new app state with 4 funded addresses
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, addrs := app.GeneratePrivKeyAddressPairs(4)
authGS := app.NewAuthGenState(
addrs,
[]sdk.Coins{
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
claim, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
fmt.Println(claim)
suite.Require().True(found)
suite.Require().Equal(c("ukava", 0), claim.Reward)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
tApp.InitializeFromGenesisStates(
authGS,
)
// creates two claim periods, one expired, and one that expires in the future
cp1 := types.NewClaimPeriod("bnb", 1, time.Unix(90, 0), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})
cp2 := types.NewClaimPeriod("xrp", 1, time.Unix(110, 0), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})
suite.keeper = tApp.GetIncentiveKeeper()
suite.keeper.SetClaimPeriod(ctx, cp1)
suite.keeper.SetClaimPeriod(ctx, cp2)
// creates one claim for the non-expired claim period and one claim for the expired claim period
c1 := types.NewClaim(addrs[0], c("ukava", 1000000), "bnb", 1)
c2 := types.NewClaim(addrs[0], c("ukava", 1000000), "xrp", 1)
suite.keeper.SetClaim(ctx, c1)
suite.keeper.SetClaim(ctx, c2)
suite.app = tApp
suite.ctx = ctx
suite.addrs = addrs
}
func createPeriodicVestingAccount(origVesting sdk.Coins, periods vesting.Periods, startTime, endTime int64) (*vesting.PeriodicVestingAccount, error) {
_, addr := app.GeneratePrivKeyAddressPairs(1)
bacc := auth.NewBaseAccountWithAddress(addr[0])
bacc.Coins = origVesting
bva, err := vesting.NewBaseVestingAccount(&bacc, origVesting, endTime)
if err != nil {
return &vesting.PeriodicVestingAccount{}, err
}
pva := vesting.NewPeriodicVestingAccountRaw(bva, startTime, periods)
err = pva.Validate()
if err != nil {
return &vesting.PeriodicVestingAccount{}, err
}
return pva, nil
}
func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
@ -391,7 +426,7 @@ func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
}
func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
suite.setupChain()
suite.SetupWithAccountState()
// send coins to base account
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[1], cs(c("ukava", 100)), 5)
suite.Require().NoError(err)
@ -410,267 +445,59 @@ func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
}
func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() {
suite.setupChain()
suite.SetupWithAccountState()
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[2], cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
macc := suite.getModuleAccount(cdp.ModuleName)
macc := suite.getModuleAccount(cdptypes.ModuleName)
err = suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, macc.GetAddress(), cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
}
func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() {
suite.setupExpiredClaims() // creates new app state with one non-expired claim period (xrp) and one expired claim period (bnb) as well as a claim that corresponds to each claim period
// both claim periods are present
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
suite.True(found)
// both claims are present
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.True(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
suite.True(found)
// expired claim period and associated claims should get deleted
suite.NotPanics(func() {
suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
})
// expired claim period and claim are not found
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.False(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.False(found)
// non-expired claim period and claim are found
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp")
suite.True(found)
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1)
suite.True(found)
}
func (suite *KeeperTestSuite) TestPayoutClaim() {
type args struct {
claimOwner sdk.AccAddress
collateralType string
id uint64
multiplier types.MultiplierName
blockTime time.Time
rewards types.Rewards
rewardperiods types.RewardPeriods
claimPeriods types.ClaimPeriods
claims types.Claims
genIDs types.GenesisClaimPeriodIDs
active bool
validatorVesting bool
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
expectedVestingAccount bool
expectedVestingLength int64
}
type errArgs struct {
expectPass bool
contains string
}
type claimTest struct {
name string
args args
errArgs errArgs
}
testCases := []claimTest{
{
"valid small claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
collateralType: "bnb-a",
id: 1,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)},
genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}},
active: true,
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))),
expectedVestingAccount: true,
expectedVestingLength: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).AddDate(0, 1, 0).Unix() - time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
multiplier: types.Small,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid large claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
collateralType: "bnb-a",
id: 1,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)},
genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}},
active: true,
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.Coins(nil),
expectedVestingAccount: true,
expectedVestingLength: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).AddDate(0, 12, 0).Unix() - time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Unix(),
multiplier: types.Large,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"valid liquid claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
collateralType: "bnb-a",
id: 1,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)},
genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}},
active: true,
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))),
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: true,
contains: "",
},
},
{
"no matching claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
collateralType: "btcb-a",
id: 1,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)},
genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}},
active: true,
validatorVesting: false,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))),
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: false,
contains: "no claim with input id found for owner and collateral type",
},
},
{
"validator vesting claim",
args{
claimOwner: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
collateralType: "bnb-a",
id: 1,
blockTime: time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC),
rewards: types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
rewardperiods: types.RewardPeriods{types.NewRewardPeriod("bnb-a", time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), c("ukava", 1000), time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claimPeriods: types.ClaimPeriods{types.NewClaimPeriod("bnb-a", 1, time.Date(2020, 11, 1, 14, 0, 0, 0, time.UTC).Add(time.Hour*7*24), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
claims: types.Claims{types.NewClaim(sdk.AccAddress(crypto.AddressHash([]byte("test"))), sdk.NewCoin("ukava", sdk.NewInt(1000)), "bnb-a", 1)},
genIDs: types.GenesisClaimPeriodIDs{types.GenesisClaimPeriodID{CollateralType: "bnb-a", ID: 2}},
active: true,
validatorVesting: true,
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500)), sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(500))),
expectedVestingAccount: false,
expectedVestingLength: 0,
multiplier: types.Small,
},
errArgs{
expectPass: false,
contains: "account type not supported",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// create new app with one funded account
config := sdk.GetConfig()
app.SetBech32AddressPrefixes(config)
// Initialize test app and set context
func (suite *KeeperTestSuite) SetupWithAccountState() {
// creates a new app state with 4 funded addresses and 1 module account
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, addrs := app.GeneratePrivKeyAddressPairs(4)
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.claimOwner},
addrs,
[]sdk.Coins{
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
cs(c("ukava", 400)),
})
incentiveGS := types.NewGenesisState(types.NewParams(tc.args.active, tc.args.rewards), types.DefaultPreviousBlockTime, tc.args.rewardperiods, tc.args.claimPeriods, tc.args.claims, tc.args.genIDs)
tApp.InitializeFromGenesisStates(authGS, app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)})
if tc.args.validatorVesting {
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, tc.args.claimOwner)
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err := vesting.NewBaseVestingAccount(
bacc,
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(20))), time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix()+100)
suite.Require().NoError(err)
vva := validatorvesting.NewValidatorVestingAccountRaw(
bva,
time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC).Unix(),
vesting.Periods{
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))},
vesting.Period{Length: 25, Amount: cs(c("bnb", 5))}},
sdk.ConsAddress(crypto.AddressHash([]byte("test"))),
sdk.AccAddress{},
95,
tApp.InitializeFromGenesisStates(
authGS,
)
err = vva.Validate()
suite.Require().NoError(err)
ak.SetAccount(ctx, vva)
}
supplyKeeper := tApp.GetSupplyKeeper()
supplyKeeper.MintCoins(ctx, types.IncentiveMacc, sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000))))
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
err := suite.keeper.PayoutClaim(suite.ctx, tc.args.claimOwner, tc.args.collateralType, tc.args.id, tc.args.multiplier)
if tc.errArgs.expectPass {
macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 600)))
suite.Require().NoError(err)
acc := suite.getAccount(tc.args.claimOwner)
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
mAcc := suite.getModuleAccount(types.IncentiveMacc)
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
vacc, ok := acc.(*vesting.PeriodicVestingAccount)
if tc.args.expectedVestingAccount {
suite.Require().True(ok)
suite.Require().Equal(tc.args.expectedVestingLength, vacc.VestingPeriods[0].Length)
} else {
suite.Require().False(ok)
}
_, f := suite.keeper.GetClaim(ctx, tc.args.claimOwner, tc.args.collateralType, tc.args.id)
suite.Require().False(f)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
// sets addrs[0] to be a periodic vesting account
ak := tApp.GetAccountKeeper()
acc := ak.GetAccount(ctx, addrs[0])
bacc := auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
periods := vesting.Periods{
vesting.Period{Length: int64(1), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(2), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(8), Amount: cs(c("ukava", 100))},
vesting.Period{Length: int64(5), Amount: cs(c("ukava", 100))},
}
bva, err2 := vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
pva := vesting.NewPeriodicVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods)
ak.SetAccount(ctx, pva)
// sets addrs[2] to be a validator vesting account
acc = ak.GetAccount(ctx, addrs[2])
bacc = auth.NewBaseAccount(acc.GetAddress(), acc.GetCoins(), acc.GetPubKey(), acc.GetAccountNumber(), acc.GetSequence())
bva, err2 = vesting.NewBaseVestingAccount(bacc, cs(c("ukava", 400)), ctx.BlockTime().Unix()+16)
suite.Require().NoError(err2)
vva := validatorvesting.NewValidatorVestingAccountRaw(bva, ctx.BlockTime().Unix(), periods, sdk.ConsAddress{}, nil, 90)
ak.SetAccount(ctx, vva)
suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx
suite.addrs = addrs
}

View File

@ -1,6 +1,7 @@
package keeper
import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -18,10 +19,6 @@ func NewQuerier(k Keeper) sdk.Querier {
return queryGetParams(ctx, req, k)
case types.QueryGetClaims:
return queryGetClaims(ctx, req, k)
case types.QueryGetRewardPeriods:
return queryGetRewardPeriods(ctx, req, k)
case types.QueryGetClaimPeriods:
return queryGetClaimPeriods(ctx, req, k)
default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
}
@ -41,41 +38,30 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e
return bz, nil
}
// query reward periods in the store
func queryGetRewardPeriods(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
// Get params
rewardPeriods := k.GetAllRewardPeriods(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(k.cdc, rewardPeriods)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}
return bz, nil
}
// query claim periods in the store
func queryGetClaimPeriods(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
// Get params
claimPeriods := k.GetAllClaimPeriods(ctx)
// Encode results
bz, err := codec.MarshalJSONIndent(k.cdc, claimPeriods)
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.GetAllClaimsByAddressAndCollateralType(ctx, requestParams.Owner, requestParams.CollateralType)
var claims types.USDXMintingClaims
if len(requestParams.Owner) > 0 {
claim, _ := k.GetClaim(ctx, requestParams.Owner)
claims = append(claims, claim)
} else {
claims = k.GetAllClaims(ctx)
}
bz, err := codec.MarshalJSONIndent(k.cdc, claims)
var paginatedClaims types.USDXMintingClaims
start, end := client.Paginate(len(claims), requestParams.Page, requestParams.Limit, 100)
if start < 0 || end < 0 {
paginatedClaims = types.USDXMintingClaims{}
} else {
paginatedClaims = claims[start:end]
}
bz, err := codec.MarshalJSONIndent(k.cdc, paginatedClaims)
if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error())
}

View File

@ -1,48 +0,0 @@
package keeper_test
import (
"strings"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
)
func (suite *KeeperTestSuite) TestQuerier() {
suite.addObjectsToStore()
querier := keeper.NewQuerier(suite.keeper)
bz, err := querier(suite.ctx, []string{types.QueryGetParams}, abci.RequestQuery{})
suite.Require().NoError(err)
suite.NotNil(bz)
var p types.Params
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &p))
claimQueryParams := types.NewQueryClaimsParams(suite.addrs[0], "bnb")
query := abci.RequestQuery{
Path: strings.Join([]string{"custom", types.QuerierRoute, types.QueryGetClaims}, "/"),
Data: types.ModuleCdc.MustMarshalJSON(claimQueryParams),
}
bz, err = querier(suite.ctx, []string{types.QueryGetClaims}, query)
var claims types.AugmentedClaims
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &claims))
suite.Equal(1, len(claims))
suite.Equal(types.AugmentedClaims{
types.NewAugmentedClaim(types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1), true),
}, claims)
var rp types.RewardPeriods
bz, err = querier(suite.ctx, []string{types.QueryGetRewardPeriods}, abci.RequestQuery{})
suite.Require().NoError(err)
suite.NotNil(bz)
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &rp))
var cp types.ClaimPeriods
bz, err = querier(suite.ctx, []string{types.QueryGetClaimPeriods}, abci.RequestQuery{})
suite.Require().NoError(err)
suite.NotNil(bz)
suite.Nil(types.ModuleCdc.UnmarshalJSON(bz, &cp))
}

View File

@ -1,118 +1,172 @@
package keeper
import (
"math"
"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.CollateralType, rp.ClaimEnd, rp.ClaimMultipliers)
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix)
store.Delete([]byte(rp.CollateralType))
return
// AccumulateRewards updates the rewards accumulated for the input reward period
func (k Keeper) AccumulateRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error {
if !rewardPeriod.Active {
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
// CreateNewRewardPeriod creates a new reward period from the input reward
func (k Keeper) CreateNewRewardPeriod(ctx sdk.Context, reward types.Reward) {
rp := types.NewRewardPeriodFromReward(reward, ctx.BlockTime())
k.SetRewardPeriod(ctx, rp)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeRewardPeriod,
sdk.NewAttribute(types.AttributeKeyRewardPeriod, rp.String()),
),
)
}
// 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.CollateralType)
// if governance has made a reward inactive, delete the current period
if found && !r.Active {
k.DeleteRewardPeriod(ctx, r.CollateralType)
}
// 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)
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
previousBlockTime = ctx.BlockTime()
k.SetPreviousBlockTime(ctx, previousBlockTime)
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
timeElapsed := CalculateTimeElapsed(rewardPeriod, ctx.BlockTime(), previousAccrualTime)
if timeElapsed.IsZero() {
return nil
}
if rewardPeriod.RewardsPerSecond.Amount.IsZero() {
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rewardPeriod.CollateralType, types.PrincipalDenom).ToDec()
if totalPrincipal.IsZero() {
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)
cdpFactor, found := k.cdpKeeper.GetInterestFactor(ctx, rewardPeriod.CollateralType)
if !found {
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
rewardFactor := newRewards.ToDec().Mul(cdpFactor).Quo(totalPrincipal)
previousRewardFactor, found := k.GetRewardFactor(ctx, rewardPeriod.CollateralType)
if !found {
previousRewardFactor = sdk.ZeroDec()
}
newRewardFactor := previousRewardFactor.Add(rewardFactor)
k.SetRewardFactor(ctx, rewardPeriod.CollateralType, newRewardFactor)
k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
return nil
}
// InitializeClaim creates or updates a claim such that no new rewards are accrued, but any existing rewards are not lost.
// this function should be called after a cdp is created. If a user previously had a cdp, then closed it, they shouldn't
// accrue rewards during the period the cdp was closed. By setting the reward factor to the current global reward factor,
// any unclaimed rewards are preserved, but no new rewards are added.
func (k Keeper) InitializeClaim(ctx sdk.Context, cdp cdptypes.CDP) {
_, found := k.GetRewardPeriod(ctx, cdp.Type)
if !found {
// this collateral type is not incentivized, do nothing
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.CollateralType, 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
rewardFactor, found := k.GetRewardFactor(ctx, cdp.Type)
if !found {
rewardFactor = sdk.ZeroDec()
}
// the amount of rewards to pay (rewardAmount * timeElapsed)
rewardsThisPeriod := rp.Reward.Amount.Mul(timeElapsed)
id := k.GetNextClaimPeriodID(ctx, rp.CollateralType)
k.cdpKeeper.IterateCdpsByCollateralType(ctx, rp.CollateralType, func(cdp cdptypes.CDP) bool {
rewardsShare := sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Quo(sdk.NewDecFromInt(totalPrincipal))
// sanity check - don't create zero claims
if rewardsShare.IsZero() {
return false
claim, found := k.GetClaim(ctx, cdp.Owner)
if !found { // this is the owner's first usdx minting reward claim
claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, rewardFactor)})
k.SetClaim(ctx, claim)
return
}
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsThisPeriod)).RoundInt()
k.AddToClaim(ctx, cdp.Owner, rp.CollateralType, 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, collateralType string, end time.Time, multipliers types.Multipliers) {
id := k.GetNextClaimPeriodID(ctx, collateralType)
claimPeriod := types.NewClaimPeriod(collateralType, id, end, multipliers)
ctx.EventManager().EmitEvent(
sdk.NewEvent(
types.EventTypeClaimPeriod,
sdk.NewAttribute(types.AttributeKeyClaimPeriod, claimPeriod.String()),
),
)
k.SetClaimPeriod(ctx, claimPeriod)
k.SetNextClaimPeriodID(ctx, collateralType, 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, collateralType string, id uint64, amount sdk.Coin) {
claim, found := k.GetClaim(ctx, addr, collateralType, id)
if found {
claim.Reward = claim.Reward.Add(amount)
} else {
claim = types.NewClaim(addr, amount, collateralType, id)
// the owner has an existing usdx minting reward claim
index, hasRewardIndex := claim.HasRewardIndex(cdp.Type)
if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type
claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, rewardFactor))
} else { // the owner has a previous usdx minting reward for this collateral type
claim.RewardIndexes[index] = types.NewRewardIndex(cdp.Type, rewardFactor)
}
k.SetClaim(ctx, claim)
}
// SynchronizeReward updates the claim object by adding any accumulated rewards and updating the reward index value.
// this should be called before a cdp is modified, immediately after the 'SynchronizeInterest' method is called in the cdp module
func (k Keeper) SynchronizeReward(ctx sdk.Context, cdp cdptypes.CDP) {
_, found := k.GetRewardPeriod(ctx, cdp.Type)
if !found {
// this collateral type is not incentivized, do nothing
return
}
globalRewardFactor, found := k.GetRewardFactor(ctx, cdp.Type)
if !found {
globalRewardFactor = sdk.ZeroDec()
}
claim, found := k.GetClaim(ctx, cdp.Owner)
if !found {
claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, globalRewardFactor)})
k.SetClaim(ctx, claim)
return
}
// the owner has an existing usdx minting reward claim
index, hasRewardIndex := claim.HasRewardIndex(cdp.Type)
if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type
claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, globalRewardFactor))
k.SetClaim(ctx, claim)
return
}
userRewardFactor := claim.RewardIndexes[index].RewardFactor
rewardsAccumulatedFactor := globalRewardFactor.Sub(userRewardFactor)
if rewardsAccumulatedFactor.IsZero() {
return
}
claim.RewardIndexes[index].RewardFactor = globalRewardFactor
newRewardsAmount := cdp.GetTotalPrincipal().Amount.ToDec().Quo(cdp.InterestFactor).Mul(rewardsAccumulatedFactor).RoundInt()
if newRewardsAmount.IsZero() {
k.SetClaim(ctx, claim)
return
}
newRewardsCoin := sdk.NewCoin(types.USDXMintingRewardDenom, newRewardsAmount)
claim.Reward = claim.Reward.Add(newRewardsCoin)
k.SetClaim(ctx, claim)
return
}
// ZeroClaim zeroes out the claim object's rewards and returns the updated claim object
func (k Keeper) ZeroClaim(ctx sdk.Context, claim types.USDXMintingClaim) types.USDXMintingClaim {
claim.Reward = sdk.NewCoin(claim.Reward.Denom, sdk.ZeroInt())
k.SetClaim(ctx, claim)
return claim
}
// SynchronizeClaim updates the claim object by adding any rewards that have accumulated.
// Returns the updated claim object
func (k Keeper) SynchronizeClaim(ctx sdk.Context, claim types.USDXMintingClaim) (types.USDXMintingClaim, error) {
for _, ri := range claim.RewardIndexes {
cdp, found := k.cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, claim.Owner, ri.CollateralType)
if !found {
// if the cdp for this collateral type has been closed, no updates are needed
continue
}
claim = k.synchronizeRewardAndReturnClaim(ctx, cdp)
}
return claim, nil
}
// this function assumes a claim already exists, so don't call it if that's not the case
func (k Keeper) synchronizeRewardAndReturnClaim(ctx sdk.Context, cdp cdptypes.CDP) types.USDXMintingClaim {
k.SynchronizeReward(ctx, cdp)
claim, _ := k.GetClaim(ctx, cdp.Owner)
return claim
}
// CalculateTimeElapsed calculates the number of reward-eligible seconds that have passed since the previous
// time rewards were accrued, taking into account the end time of the reward period
func CalculateTimeElapsed(rewardPeriod types.RewardPeriod, blockTime time.Time, previousAccrualTime time.Time) sdk.Int {
if rewardPeriod.End.Before(blockTime) &&
(rewardPeriod.End.Before(previousAccrualTime) || rewardPeriod.End.Equal(previousAccrualTime)) {
return sdk.ZeroInt()
}
if rewardPeriod.End.Before(blockTime) {
return sdk.NewInt(int64(math.RoundToEven(
rewardPeriod.End.Sub(previousAccrualTime).Seconds(),
)))
}
return sdk.NewInt(int64(math.RoundToEven(
blockTime.Sub(previousAccrualTime).Seconds(),
)))
}

View File

@ -1,262 +1,323 @@
package keeper_test
import (
"fmt"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/cdp"
cdpkeeper "github.com/kava-labs/kava/x/cdp/keeper"
cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/pricefeed"
)
func (suite *KeeperTestSuite) TestExpireRewardPeriod() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})
suite.keeper.SetRewardPeriod(suite.ctx, rp)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
suite.NotPanics(func() {
suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
})
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb")
suite.True(found)
func (suite *KeeperTestSuite) TestAccumulateRewards() {
type args struct {
ctype string
rewardsPerSecond sdk.Coin
initialTime time.Time
initialTotalPrincipal sdk.Coin
timeElapsed int
expectedRewardFactor sdk.Dec
}
func (suite *KeeperTestSuite) TestAddToClaim() {
rp := types.NewRewardPeriod("bnb", suite.ctx.BlockTime(), suite.ctx.BlockTime().Add(time.Hour*168), c("ukava", 100000000), suite.ctx.BlockTime().Add(time.Hour*168*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})
suite.keeper.SetRewardPeriod(suite.ctx, rp)
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1)
suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp)
c1 := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
suite.keeper.SetClaim(suite.ctx, c1)
suite.NotPanics(func() {
suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "bnb", 1, c("ukava", 1000000))
})
testC, _ := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1)
suite.Equal(c("ukava", 2000000), testC.Reward)
suite.NotPanics(func() {
suite.keeper.AddToClaim(suite.ctx, suite.addrs[0], "xpr", 1, c("ukava", 1000000))
})
}
func (suite *KeeperTestSuite) TestCreateRewardPeriod() {
reward := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)
suite.NotPanics(func() {
suite.keeper.CreateNewRewardPeriod(suite.ctx, reward)
})
_, found := suite.keeper.GetRewardPeriod(suite.ctx, "bnb")
suite.True(found)
}
func (suite *KeeperTestSuite) TestCreateAndDeleteRewardsPeriods() {
reward1 := types.NewReward(true, "bnb", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)
reward2 := types.NewReward(false, "xrp", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)
reward3 := types.NewReward(false, "btc", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)
// add a reward period to the store for a non-active reward
suite.NotPanics(func() {
suite.keeper.CreateNewRewardPeriod(suite.ctx, reward3)
})
params := types.NewParams(true, types.Rewards{reward1, reward2, reward3})
suite.keeper.SetParams(suite.ctx, params)
suite.NotPanics(func() {
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
testCases := []struct {
type test struct {
name string
arg string
expectFound bool
}{
args args
}
testCases := []test{
{
"active reward period",
"bnb",
true,
"7 seconds",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialTotalPrincipal: c("usdx", 1000000000000),
timeElapsed: 7,
expectedRewardFactor: d("0.000000856478000000"),
},
},
{
"attempt to add inactive reward period",
"xrp",
false,
"1 day",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialTotalPrincipal: c("usdx", 1000000000000),
timeElapsed: 86400,
expectedRewardFactor: d("0.0105713856"),
},
},
{
"remove inactive reward period",
"btc",
false,
"0 seconds",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialTotalPrincipal: c("usdx", 1000000000000),
timeElapsed: 0,
expectedRewardFactor: d("0.0"),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
_, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.arg)
if tc.expectFound {
suite.True(found)
} else {
suite.False(found)
}
suite.SetupWithCDPGenState()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup cdp state
cdpKeeper := suite.app.GetCDPKeeper()
cdpKeeper.SetTotalPrincipal(suite.ctx, tc.args.ctype, cdptypes.DefaultStableDenom, tc.args.initialTotalPrincipal.Amount)
// setup incentive state
params := types.NewParams(
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
tc.args.initialTime.Add(time.Hour*24*365*5),
)
suite.keeper.SetParams(suite.ctx, params)
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime)
suite.keeper.SetRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec())
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * tc.args.timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.args.ctype)
suite.Require().True(found)
err := suite.keeper.AccumulateRewards(suite.ctx, rewardPeriod)
suite.Require().NoError(err)
rewardFactor, found := suite.keeper.GetRewardFactor(suite.ctx, tc.args.ctype)
suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor)
})
}
}
func (suite *KeeperTestSuite) TestApplyRewardsToCdps() {
suite.setupCdpChain() // creates a test app with 3 BNB cdps and usdx incentives for bnb - each reward period is one week
// move the context forward by 100 periods
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
// apply rewards to BNB cdps
suite.NotPanics(func() {
suite.keeper.ApplyRewardsToCdps(suite.ctx)
})
// each cdp should have a claim
claims := types.Claims{}
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
claims = append(claims, c)
return false
})
suite.Equal(3, len(claims))
// there should be no associated claim period, because the reward period has not ended yet
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb-a")
suite.False(found)
// move ctx to the reward period expiry and check that the claim period has been created and the next claim period id has increased
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 7))
suite.NotPanics(func() {
// apply rewards to cdps
suite.keeper.ApplyRewardsToCdps(suite.ctx)
// delete the old reward period amd create a new one
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb-a")
suite.True(found)
testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb-a")
suite.Equal(uint64(2), testID)
// move the context forward by 100 periods
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100))
// run the begin blocker functions
suite.NotPanics(func() {
suite.keeper.DeleteExpiredClaimsAndClaimPeriods(suite.ctx)
suite.keeper.ApplyRewardsToCdps(suite.ctx)
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
})
// each cdp should now have two claims
claims = types.Claims{}
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) {
claims = append(claims, c)
return false
})
suite.Equal(6, len(claims))
func (suite *KeeperTestSuite) TestSyncRewards() {
type args struct {
ctype string
rewardsPerSecond sdk.Coin
initialTime time.Time
initialCollateral sdk.Coin
initialPrincipal sdk.Coin
blockTimes []int
expectedRewardFactor sdk.Dec
expectedRewards sdk.Coin
}
type test struct {
name string
args args
}
func (suite *KeeperTestSuite) setupCdpChain() {
// creates a new test app with bnb as the only asset the pricefeed and cdp modules
// funds three addresses and creates 3 cdps, funded with 100 BNB, 1000 BNB, and 10000 BNB
// each CDP draws 10, 100, and 1000 USDX respectively
// adds usdx incentives for bnb - 1000 KAVA per week with a 1 year time lock
testCases := []test{
{
"10 blocks",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 10000000000),
blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
expectedRewardFactor: d("0.001223540000000000"),
expectedRewards: c("ukava", 12235400),
},
},
{
"10 blocks - long block time",
args{
ctype: "bnb-a",
rewardsPerSecond: c("ukava", 122354),
initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
initialCollateral: c("bnb", 1000000000000),
initialPrincipal: c("usdx", 10000000000),
blockTimes: []int{86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400, 86400},
expectedRewardFactor: d("10.57138560000000000"),
expectedRewards: c("ukava", 105713856000),
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupWithCDPGenState()
suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
// setup incentive state
params := types.NewParams(
types.RewardPeriods{types.NewRewardPeriod(true, tc.args.ctype, tc.args.initialTime, tc.args.initialTime.Add(time.Hour*24*365*4), tc.args.rewardsPerSecond)},
types.Multipliers{types.NewMultiplier(types.MultiplierName("small"), 1, d("0.25")), types.NewMultiplier(types.MultiplierName("large"), 12, d("1.0"))},
tc.args.initialTime.Add(time.Hour*24*365*5),
)
suite.keeper.SetParams(suite.ctx, params)
suite.keeper.SetPreviousAccrualTime(suite.ctx, tc.args.ctype, tc.args.initialTime)
suite.keeper.SetRewardFactor(suite.ctx, tc.args.ctype, sdk.ZeroDec())
// setup account state
sk := suite.app.GetSupplyKeeper()
sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral))
sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral))
// setup cdp state
cdpKeeper := suite.app.GetCDPKeeper()
err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype)
suite.Require().NoError(err)
claim, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.Require().True(found)
suite.Require().Equal(sdk.ZeroDec(), claim.RewardIndexes[0].RewardFactor)
var timeElapsed int
previousBlockTime := suite.ctx.BlockTime()
for _, t := range tc.args.blockTimes {
timeElapsed += t
updatedBlockTime := previousBlockTime.Add(time.Duration(int(time.Second) * t))
previousBlockTime = updatedBlockTime
blockCtx := suite.ctx.WithBlockTime(updatedBlockTime)
rewardPeriod, found := suite.keeper.GetRewardPeriod(blockCtx, tc.args.ctype)
suite.Require().True(found)
err := suite.keeper.AccumulateRewards(blockCtx, rewardPeriod)
suite.Require().NoError(err)
}
updatedBlockTime := suite.ctx.BlockTime().Add(time.Duration(int(time.Second) * timeElapsed))
suite.ctx = suite.ctx.WithBlockTime(updatedBlockTime)
cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(suite.ctx, suite.addrs[0], tc.args.ctype)
suite.Require().True(found)
suite.Require().NotPanics(func() {
suite.keeper.SynchronizeReward(suite.ctx, cdp)
})
rewardFactor, found := suite.keeper.GetRewardFactor(suite.ctx, tc.args.ctype)
suite.Require().Equal(tc.args.expectedRewardFactor, rewardFactor)
claim, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
fmt.Println(claim)
suite.Require().True(found)
suite.Require().Equal(tc.args.expectedRewardFactor, claim.RewardIndexes[0].RewardFactor)
suite.Require().Equal(tc.args.expectedRewards, claim.Reward)
})
}
}
func TestRewardCalculation(t *testing.T) {
// Test Params
ctype := "bnb-a"
initialTime := time.Date(1998, 1, 1, 0, 0, 0, 0, time.UTC)
rewardsPerSecond := c("ukava", 122_354)
initialCollateral := c("bnb", 10_000_000_000)
initialPrincipal := c("usdx", 100_000_000)
oneYear := time.Hour * 24 * 365
rewardPeriod := types.NewRewardPeriod(
true,
ctype,
initialTime,
initialTime.Add(4*oneYear),
rewardsPerSecond,
)
// Setup app and module params
_, addrs := app.GeneratePrivKeyAddressPairs(5)
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: initialTime})
tApp.InitializeFromGenesisStates(
app.NewAuthGenState(addrs[:1], []sdk.Coins{cs(initialCollateral)}),
NewPricefeedGenStateMulti(),
NewCDPGenStateHighInterest(),
NewIncentiveGenState(initialTime, initialTime.Add(oneYear), rewardPeriod),
)
// Create a CDP
cdpKeeper := tApp.GetCDPKeeper()
err := cdpKeeper.AddCdp(
ctx,
addrs[0],
initialCollateral,
initialPrincipal,
ctype,
)
require.NoError(t, err)
// Calculate expected cdp reward using iteration
// Use 10 blocks, each a very long 630720s, to total 6307200s or 1/5th of a year
// The cdp stability fee is set to the max value 500%, so this time ensures the debt increases a significant amount (doubles)
// High stability fees increase the chance of catching calculation bugs.
blockTimes := newRepeatingSliceInt(630720, 10)
expectedCDPReward := sdk.ZeroDec() //c(rewardPeriod.RewardsPerSecond.Denom, 0)
for _, bt := range blockTimes {
ctx = ctx.WithBlockTime(ctx.BlockTime().Add(time.Duration(int(time.Second) * bt)))
// run cdp and incentive begin blockers to update factors
tApp.BeginBlocker(ctx, abci.RequestBeginBlock{})
// calculate expected cdp reward
cdpBlockReward, err := calculateCDPBlockReward(ctx, cdpKeeper, addrs[0], ctype, sdk.NewInt(int64(bt)), rewardPeriod)
require.NoError(t, err)
expectedCDPReward = expectedCDPReward.Add(cdpBlockReward)
}
// calculate cdp reward using factor
cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, addrs[0], ctype)
require.True(t, found)
incentiveKeeper := tApp.GetIncentiveKeeper()
require.NotPanics(t, func() {
incentiveKeeper.SynchronizeReward(ctx, cdp)
})
claim, found := incentiveKeeper.GetClaim(ctx, addrs[0])
require.True(t, found)
// Compare two methods of calculation
relativeError := expectedCDPReward.Sub(claim.Reward.Amount.ToDec()).Quo(expectedCDPReward).Abs()
maxError := d("0.0001")
require.Truef(t, relativeError.LT(maxError),
"percent diff %s > %s , expected: %s, actual %s,", relativeError, maxError, expectedCDPReward, claim.Reward.Amount,
)
}
// calculateCDPBlockReward computes the reward that should be distributed to a cdp for the current block.
func calculateCDPBlockReward(ctx sdk.Context, cdpKeeper cdpkeeper.Keeper, owner sdk.AccAddress, ctype string, timeElapsed sdk.Int, rewardPeriod types.RewardPeriod) (sdk.Dec, error) {
// Calculate total rewards to distribute this block
newRewards := timeElapsed.Mul(rewardPeriod.RewardsPerSecond.Amount)
// Calculate cdp's share of total debt
totalPrincipal := cdpKeeper.GetTotalPrincipal(ctx, ctype, types.PrincipalDenom).ToDec()
// cdpDebt
cdp, found := cdpKeeper.GetCdpByOwnerAndCollateralType(ctx, owner, ctype)
if !found {
return sdk.Dec{}, fmt.Errorf("couldn't find cdp for owner '%s' and collateral type '%s'", owner, ctype)
}
accumulatedInterest := cdpKeeper.CalculateNewInterest(ctx, cdp)
cdpDebt := cdp.Principal.Add(cdp.AccumulatedFees).Add(accumulatedInterest).Amount
// Calculate cdp's reward
return newRewards.Mul(cdpDebt).ToDec().Quo(totalPrincipal), nil
}
func (suite *KeeperTestSuite) SetupWithCDPGenState() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// need pricefeed and cdp gen state with one collateral
pricefeedGS := pricefeed.GenesisState{
Params: pricefeed.Params{
Markets: []pricefeed.Market{
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeed.PostedPrice{
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: d("12.29"),
Expiry: time.Now().Add(100000 * time.Hour),
},
},
}
// need incentive params for one collateral
cdpGS := cdp.GenesisState{
Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{
{
Denom: "bnb",
Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"),
AuctionSize: i(10000000000),
Prefix: 0x20,
SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd",
ConversionFactor: i(8),
},
},
DebtParam: cdp.DebtParam{
Denom: "usdx",
ReferenceAsset: "usd",
ConversionFactor: i(6),
DebtFloor: i(10000000),
SavingsRate: d("0.95"),
},
},
StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{},
PreviousDistributionTime: cdp.DefaultPreviousDistributionTime,
}
incentiveGS := types.NewGenesisState(
types.NewParams(
true, types.Rewards{types.NewReward(true, "bnb-a", c("ukava", 1000000000), time.Hour*7*24, types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))}, time.Hour*7*24)},
),
types.DefaultPreviousBlockTime,
types.RewardPeriods{types.NewRewardPeriod("bnb-a", ctx.BlockTime(), ctx.BlockTime().Add(time.Hour*7*24), c("ukava", 1000), ctx.BlockTime().Add(time.Hour*7*24*2), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))})},
types.ClaimPeriods{},
types.Claims{},
types.GenesisClaimPeriodIDs{})
pricefeedAppGs := app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)}
cdpAppGs := app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGS)}
incentiveAppGs := app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(incentiveGS)}
_, addrs := app.GeneratePrivKeyAddressPairs(3)
authGS := app.NewAuthGenState(
addrs[0:3],
[]sdk.Coins{
cs(c("bnb", 10000000000)),
cs(c("bnb", 100000000000)),
cs(c("bnb", 1000000000000)),
})
tApp.InitializeFromGenesisStates(
authGS,
pricefeedAppGs,
incentiveAppGs,
cdpAppGs,
NewPricefeedGenStateMulti(),
NewCDPGenStateMulti(),
)
_, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx
// create 3 cdps
cdpKeeper := tApp.GetCDPKeeper()
err := cdpKeeper.AddCdp(suite.ctx, addrs[0], c("bnb", 10000000000), c("usdx", 10000000), "bnb-a")
suite.Require().NoError(err)
err = cdpKeeper.AddCdp(suite.ctx, addrs[1], c("bnb", 100000000000), c("usdx", 100000000), "bnb-a")
suite.Require().NoError(err)
err = cdpKeeper.AddCdp(suite.ctx, addrs[2], c("bnb", 1000000000000), c("usdx", 1000000000), "bnb-a")
suite.Require().NoError(err)
// total usd is 1110
// set the previous block time
suite.keeper.SetPreviousBlockTime(suite.ctx, suite.ctx.BlockTime())
suite.keeper = keeper
suite.addrs = addrs
}
// Avoid cluttering test cases with long function names
func i(in int64) sdk.Int { return sdk.NewInt(in) }
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) }
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) }
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) }
// newRepeatingSliceInt creates a slice of the specified length containing a single repeating element.
func newRepeatingSliceInt(element int, length int) []int {
slice := make([]int, length)
for i := 0; i < length; i++ {
slice[i] = element
}
return slice
}

View File

@ -2,11 +2,11 @@ package simulation
import (
"bytes"
"encoding/binary"
"fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/libs/kv"
@ -16,35 +16,25 @@ import (
// DecodeStore unmarshals the KVPair's Value to the module's corresponding type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch {
case bytes.Equal(kvA.Key[:1], types.RewardPeriodKeyPrefix):
var rewardPeriodA, rewardPeriodB types.RewardPeriod
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &rewardPeriodA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &rewardPeriodB)
return fmt.Sprintf("%v\n%v", rewardPeriodA, rewardPeriodB)
case bytes.Equal(kvA.Key[:1], types.ClaimPeriodKeyPrefix):
var claimPeriodA, claimPeriodB types.ClaimPeriod
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimPeriodA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimPeriodB)
return fmt.Sprintf("%v\n%v", claimPeriodA, claimPeriodB)
case bytes.Equal(kvA.Key[:1], types.ClaimKeyPrefix):
var claimA, claimB types.Claim
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimB)
var claimA, claimB types.USDXMintingClaim
cdc.MustUnmarshalBinaryBare(kvA.Value, &claimA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &claimB)
return fmt.Sprintf("%v\n%v", claimA, claimB)
case bytes.Equal(kvA.Key[:1], types.NextClaimPeriodIDPrefix):
claimPeriodIDA := binary.BigEndian.Uint64(kvA.Value)
claimPeriodIDB := binary.BigEndian.Uint64(kvB.Value)
return fmt.Sprintf("%d\n%d", claimPeriodIDA, claimPeriodIDB)
case bytes.Equal(kvA.Key[:1], types.PreviousBlockTimeKey):
case bytes.Equal(kvA.Key[:1], types.BlockTimeKey):
var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB)
cdc.MustUnmarshalBinaryBare(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, timeB)
case bytes.Equal(kvA.Key[:1], types.RewardFactorKey):
var factorA, factorB sdk.Dec
cdc.MustUnmarshalBinaryBare(kvA.Value, &factorA)
cdc.MustUnmarshalBinaryBare(kvB.Value, &factorB)
return fmt.Sprintf("%s\n%s", factorA, factorB)
default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1]))
}

View File

@ -24,21 +24,15 @@ func makeTestCodec() (cdc *codec.Codec) {
func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec()
// Set up RewardPeriod, ClaimPeriod, Claim, and previous block time
rewardPeriod := types.NewRewardPeriod("btc", time.Now().UTC(), time.Now().Add(time.Hour*1).UTC(),
sdk.NewCoin("ukava", sdk.NewInt(10000000000)), time.Now().Add(time.Hour*2).UTC(), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
claimPeriod := types.NewClaimPeriod("btc", 1, time.Now().Add(time.Hour*24).UTC(), types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33"))})
addr, _ := sdk.AccAddressFromBech32("kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw")
claim := types.NewClaim(addr, sdk.NewCoin("ukava", sdk.NewInt(1000000)), "bnb", 1)
claim := types.NewUSDXMintingClaim(addr, sdk.NewCoin("ukava", sdk.NewInt(1000000)), types.RewardIndexes{types.NewRewardIndex("bnb-a", sdk.ZeroDec())})
prevBlockTime := time.Now().Add(time.Hour * -1).UTC()
factor := sdk.ZeroDec()
kvPairs := kv.Pairs{
kv.Pair{Key: types.RewardPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&rewardPeriod)},
kv.Pair{Key: types.ClaimPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claimPeriod)},
kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claim)},
kv.Pair{Key: types.NextClaimPeriodIDPrefix, Value: sdk.Uint64ToBigEndian(10)},
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryBare(claim)},
kv.Pair{Key: []byte(types.BlockTimeKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
kv.Pair{Key: []byte(types.RewardFactorKey), Value: cdc.MustMarshalBinaryBare(factor)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
}
@ -46,11 +40,9 @@ func TestDecodeDistributionStore(t *testing.T) {
name string
expectedLog string
}{
{"RewardPeriod", fmt.Sprintf("%v\n%v", rewardPeriod, rewardPeriod)},
{"ClaimPeriod", fmt.Sprintf("%v\n%v", claimPeriod, claimPeriod)},
{"Claim", fmt.Sprintf("%v\n%v", claim, claim)},
{"NextClaimPeriodID", "10\n10"},
{"PreviousBlockTime", fmt.Sprintf("%v\n%v", prevBlockTime, prevBlockTime)},
{"RewardFactor", fmt.Sprintf("%v\n%v", factor, factor)},
{"other", ""},
}
for i, tt := range tests {

View File

@ -2,15 +2,11 @@ package simulation
import (
"fmt"
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/incentive/types"
)
@ -23,115 +19,9 @@ var (
// RandomizedGenState generates a random GenesisState for incentive module
func RandomizedGenState(simState *module.SimulationState) {
// Get collateral asset denoms from existing CDP genesis state and pass to incentive params
var cdpGenesis cdp.GenesisState
simState.Cdc.MustUnmarshalJSON(simState.GenState[cdp.ModuleName], &cdpGenesis)
if len(CollateralDenoms) == 0 {
for _, collateral := range cdpGenesis.Params.CollateralParams {
CollateralDenoms = append(CollateralDenoms, collateral.Type)
}
}
params := genParams(simState.Rand)
// Generate random reward and claim periods
rewardPeriods := genRewardPeriods(simState.Rand, simState.GenTimestamp, params.Rewards)
claimPeriods := genClaimPeriods(rewardPeriods)
claimPeriodIDs := genNextClaimPeriodIds(claimPeriods)
// New genesis state holds valid, linked reward periods, claim periods, and claim period IDs
incentiveGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime,
rewardPeriods, claimPeriods, types.Claims{}, claimPeriodIDs)
if err := incentiveGenesis.Validate(); err != nil {
panic(err)
}
incentiveGenesis := types.DefaultGenesisState()
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, incentiveGenesis))
simState.GenState[types.ModuleName] = simState.Cdc.MustMarshalJSON(incentiveGenesis)
}
// genParams generates random rewards and is active by default
func genParams(r *rand.Rand) types.Params {
params := types.NewParams(true, genRewards(r))
if err := params.Validate(); err != nil {
panic(err)
}
return params
}
// genRewards generates rewards for each specified collateral type
func genRewards(r *rand.Rand) types.Rewards {
rewards := make(types.Rewards, len(CollateralDenoms))
for i, denom := range CollateralDenoms {
active := true
// total reward is in range (half max total reward, max total reward)
amount := simulation.RandIntBetween(r, int(MaxTotalAssetReward.Int64()/2), int(MaxTotalAssetReward.Int64()))
totalRewards := sdk.NewInt64Coin(RewardDenom, int64(amount))
// generate a random number of months for lockups
numMonthsSmall := simulation.RandIntBetween(r, 0, 6)
numMonthsLarge := simulation.RandIntBetween(r, 7, 12)
multiplierSmall := types.NewMultiplier(types.Small, int64(numMonthsSmall), sdk.MustNewDecFromStr("0.33"))
multiplierLarge := types.NewMultiplier(types.Large, int64(numMonthsLarge), sdk.MustNewDecFromStr("1.0"))
duration := time.Duration(time.Hour * time.Duration(simulation.RandIntBetween(r, 1, 48)))
claimDuration := time.Hour * time.Duration(simulation.RandIntBetween(r, 1, 48)) // twice as long as duration
rewards[i] = types.NewReward(active, denom, totalRewards, duration, types.Multipliers{multiplierSmall, multiplierLarge}, claimDuration)
}
return rewards
}
// genRewardPeriods generates chronological reward periods for each given reward type
func genRewardPeriods(r *rand.Rand, timestamp time.Time, rewards types.Rewards) types.RewardPeriods {
rewardPeriods := make(types.RewardPeriods, len(rewards))
rewardPeriodStart := timestamp
for i, reward := range rewards {
// Set up reward period parameters
start := rewardPeriodStart
end := start.Add(reward.Duration).UTC()
baseRewardAmount := reward.AvailableRewards.Amount.Quo(sdk.NewInt(100)) // base period reward is 1/100 total reward
// Earlier periods have larger rewards
amount := sdk.NewCoin("ukava", baseRewardAmount.Mul(sdk.NewInt(int64(i))))
claimEnd := end.Add(reward.ClaimDuration)
claimMultipliers := reward.ClaimMultipliers
// Create reward period and append to array
rewardPeriods[i] = types.NewRewardPeriod(reward.CollateralType, start, end, amount, claimEnd, claimMultipliers)
// Update start time of next reward period
rewardPeriodStart = end
}
return rewardPeriods
}
// genClaimPeriods loads valid claim periods for an array of reward periods
func genClaimPeriods(rewardPeriods types.RewardPeriods) types.ClaimPeriods {
denomRewardPeriodsCount := make(map[string]uint64)
claimPeriods := make(types.ClaimPeriods, len(rewardPeriods))
for i, rewardPeriod := range rewardPeriods {
// Increment reward period count for this denom (this is our claim period's ID)
denom := rewardPeriod.CollateralType
numbRewardPeriods := denomRewardPeriodsCount[denom] + 1
denomRewardPeriodsCount[denom] = numbRewardPeriods
// Set end and timelock from the associated reward period
end := rewardPeriod.ClaimEnd
claimMultipliers := rewardPeriod.ClaimMultipliers
// Create the new claim period for this reward period
claimPeriods[i] = types.NewClaimPeriod(denom, numbRewardPeriods, end, claimMultipliers)
}
return claimPeriods
}
// genNextClaimPeriodIds returns an array of the most recent claim period IDs for each denom
func genNextClaimPeriodIds(cps types.ClaimPeriods) types.GenesisClaimPeriodIDs {
// Build a map of the most recent claim periods by denom
mostRecentClaimPeriodByDenom := make(map[string]uint64)
var claimPeriodIDs types.GenesisClaimPeriodIDs
for _, cp := range cps {
if cp.ID <= mostRecentClaimPeriodByDenom[cp.CollateralType] {
continue
}
claimPeriodIDs = append(claimPeriodIDs, types.GenesisClaimPeriodID{CollateralType: cp.CollateralType, ID: cp.ID})
mostRecentClaimPeriodByDenom[cp.CollateralType] = cp.ID
}
return claimPeriodIDs
}

View File

@ -1,21 +1,17 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/cosmos/cosmos-sdk/x/simulation"
appparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
)
// Simulation operation weights constants
@ -49,100 +45,7 @@ func SimulateMsgClaimReward(ak auth.AccountKeeper, sk types.SupplyKeeper, k keep
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// Load only account types that can claim rewards
validAccounts := make(map[string]bool)
for _, acc := range accs {
account := ak.GetAccount(ctx, acc.Address)
switch account.(type) {
case *vesting.PeriodicVestingAccount, *auth.BaseAccount: // Valid: BaseAccount, PeriodicVestingAccount
validAccounts[account.GetAddress().String()] = true
default: // Invalid: ValidatorVestingAccount, DelayedVestingAccount, ContinuousVestingAccount
break
}
}
// Load open claims and shuffle them to randomize
openClaims := types.Claims{}
k.IterateClaims(ctx, func(claim types.Claim) bool {
openClaims = append(openClaims, claim)
return false
})
r.Shuffle(len(openClaims), func(i, j int) {
openClaims[i], openClaims[j] = openClaims[j], openClaims[i]
})
kavadistMacc := sk.GetModuleAccount(ctx, kavadist.KavaDistMacc)
kavadistBalance := kavadistMacc.SpendableCoins(ctx.BlockTime())
// Find address that has a claim of the same reward denom, then confirm it's distributable
claimer, claim, found := findValidAccountClaimPair(accs, openClaims, func(acc simulation.Account, claim types.Claim) bool {
if validAccounts[acc.Address.String()] { // Address must be valid type
if claim.Owner.Equals(acc.Address) { // Account must be claim owner
allClaims, found := k.GetActiveClaimsByAddressAndCollateralType(ctx, claim.Owner, claim.CollateralType)
if found { // found should always be true
var rewards sdk.Coins
for _, individualClaim := range allClaims {
rewards = rewards.Add(individualClaim.Reward)
}
if rewards.AmountOf(claim.Reward.Denom).IsPositive() { // Can't distribute 0 coins
// Validate that kavadist module has enough coins to distribute rewards
if kavadistBalance.AmountOf(claim.Reward.Denom).GTE(rewards.AmountOf(claim.Reward.Denom)) {
return true
}
}
}
}
}
return false
})
if !found {
return simulation.NewOperationMsgBasic(types.ModuleName,
"no-operation (no accounts currently have fulfillable claims)", "", false, nil), nil, nil
}
claimerAcc := ak.GetAccount(ctx, claimer.Address)
if claimerAcc == nil {
return simulation.NoOpMsg(types.ModuleName), nil, fmt.Errorf("couldn't find account %s", claimer.Address)
}
multiplierName := types.Small
if simulation.RandIntBetween(r, 0, 1) == 0 {
multiplierName = types.Large
}
msg := types.NewMsgClaimReward(claimer.Address, claim.CollateralType, string(multiplierName))
tx := helpers.GenTx(
[]sdk.Msg{msg},
sdk.NewCoins(),
helpers.DefaultGenTxGas,
chainID,
[]uint64{claimerAcc.GetAccountNumber()},
[]uint64{claimerAcc.GetSequence()},
claimer.PrivKey,
)
_, result, err := app.Deliver(tx)
if err != nil {
// to aid debugging, add the stack trace to the comment field of the returned opMsg
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
// to aid debugging, add the result log to the comment field
return simulation.NewOperationMsg(msg, true, result.Log), nil, nil
}
}
// findValidAccountClaimPair finds an account and reward claim for which the callback func returns true
func findValidAccountClaimPair(accounts []simulation.Account, claims types.Claims,
cb func(simulation.Account, types.Claim) bool) (simulation.Account, types.Claim, bool) {
for _, claim := range claims {
for _, acc := range accounts {
if isValid := cb(acc, claim); isValid {
return acc, claim, true
}
}
}
return simulation.Account{}, types.Claim{}, false
}

View File

@ -1,12 +1,9 @@
package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/incentive/types"
)
const (
@ -23,16 +20,5 @@ func genActive(r *rand.Rand) bool {
// ParamChanges defines the parameters that can be modified by param change proposals
func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []simulation.ParamChange{
simulation.NewSimParamChange(types.ModuleName, keyActive,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%t\"", genActive(r))
},
),
simulation.NewSimParamChange(types.ModuleName, keyRewards,
func(r *rand.Rand) string {
return fmt.Sprintf("\"%v\"", genRewards(r))
},
),
}
return []simulation.ParamChange{}
}

112
x/incentive/types/claims.go Normal file
View File

@ -0,0 +1,112 @@
package types
import (
"errors"
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// USDXMintingClaim stores the usdx mintng rewards that can be claimed by owner
type USDXMintingClaim struct {
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Reward sdk.Coin `json:"reward" yaml:"reward"`
RewardIndexes RewardIndexes `json:"reward_indexes" yaml:"reward_indexes"`
}
// NewUSDXMintingClaim returns a new USDXMintingClaim
func NewUSDXMintingClaim(owner sdk.AccAddress, reward sdk.Coin, rewardIndexes RewardIndexes) USDXMintingClaim {
return USDXMintingClaim{
Owner: owner,
Reward: reward,
RewardIndexes: rewardIndexes,
}
}
// Validate performs a basic check of a Claim fields.
func (c USDXMintingClaim) Validate() error {
if c.Owner.Empty() {
return errors.New("claim owner cannot be empty")
}
if !c.Reward.IsValid() {
return fmt.Errorf("invalid reward amount: %s", c.Reward)
}
return c.RewardIndexes.Validate()
}
// String implements fmt.Stringer
func (c USDXMintingClaim) String() string {
return fmt.Sprintf(`Claim:
Owner: %s,
Reward: %s,
Reward Indexes: %s,
`, c.Owner, c.Reward, c.RewardIndexes)
}
// HasRewardIndex check if a claim has a reward index for the input collateral type
func (c USDXMintingClaim) HasRewardIndex(collateralType string) (int64, bool) {
for index, ri := range c.RewardIndexes {
if ri.CollateralType == collateralType {
return int64(index), true
}
}
return 0, false
}
// USDXMintingClaims array of USDXMintingClaim
type USDXMintingClaims []USDXMintingClaim
// Validate checks if all the claims are valid and there are no duplicated
// entries.
func (cs USDXMintingClaims) Validate() error {
for _, c := range cs {
if err := c.Validate(); err != nil {
return err
}
}
return nil
}
// RewardIndex stores reward accumulation information
type RewardIndex struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
RewardFactor sdk.Dec `json:"reward_factor" yaml:"reward_factor"`
}
// NewRewardIndex returns a new RewardIndex
func NewRewardIndex(collateralType string, factor sdk.Dec) RewardIndex {
return RewardIndex{
CollateralType: collateralType,
RewardFactor: factor,
}
}
func (ri RewardIndex) String() string {
return fmt.Sprintf(`Collateral Type: %s, RewardFactor: %s`, ri.CollateralType, ri.RewardFactor)
}
// Validate validates reward index
func (ri RewardIndex) Validate() error {
if ri.RewardFactor.IsNegative() {
return fmt.Errorf("reward factor value should be positive, is %s for %s", ri.RewardFactor, ri.CollateralType)
}
if strings.TrimSpace(ri.CollateralType) == "" {
return fmt.Errorf("collateral type should not be empty")
}
return nil
}
// RewardIndexes slice of RewardIndex
type RewardIndexes []RewardIndex
// Validate validation for reward indexes
func (ris RewardIndexes) Validate() error {
for _, ri := range ris {
if err := ri.Validate(); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,66 @@
package types
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestClaimsValidate(t *testing.T) {
owner := sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1")))
testCases := []struct {
msg string
claims USDXMintingClaims
expPass bool
}{
{
"valid",
USDXMintingClaims{
NewUSDXMintingClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), RewardIndexes{NewRewardIndex("bnb-a", sdk.ZeroDec())}),
},
true,
},
{
"invalid owner",
USDXMintingClaims{
{Owner: nil},
},
false,
},
{
"invalid reward",
USDXMintingClaims{
{
Owner: owner,
Reward: sdk.Coin{Denom: "", Amount: sdk.ZeroInt()},
},
},
false,
},
{
"invalid collateral type",
USDXMintingClaims{
{
Owner: owner,
Reward: sdk.NewCoin("bnb", sdk.OneInt()),
RewardIndexes: []RewardIndex{{"", sdk.ZeroDec()}},
},
},
false,
},
}
for _, tc := range testCases {
err := tc.claims.Validate()
if tc.expPass {
require.NoError(t, err, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
}
}

View File

@ -14,9 +14,5 @@ func init() {
// 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)
cdc.RegisterConcrete(MsgClaimUSDXMintingReward{}, "incentive/MsgClaimUSDXMintingReward", nil)
}

View File

@ -9,11 +9,12 @@ import (
// Incentive module errors
var (
ErrClaimNotFound = sdkerrors.Register(ModuleName, 2, "no claim with input id found for owner and collateral type")
ErrClaimPeriodNotFound = sdkerrors.Register(ModuleName, 3, "no claim period found for id and collateral type")
ErrRewardPeriodNotFound = sdkerrors.Register(ModuleName, 3, "no reward period found for collateral type")
ErrInvalidAccountType = sdkerrors.Register(ModuleName, 4, "account type not supported")
ErrNoClaimsFound = sdkerrors.Register(ModuleName, 5, "no claims with collateral type found for address")
ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 6, "module account has insufficient balance to pay claim")
ErrAccountNotFound = sdkerrors.Register(ModuleName, 7, "account not found")
ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 8, "invalid rewards multiplier")
ErrZeroClaim = sdkerrors.Register(ModuleName, 9, "cannot claim - claim amount rounds to zero")
ErrClaimExpired = sdkerrors.Register(ModuleName, 10, "claim has expired")
)

View File

@ -17,8 +17,9 @@ type SupplyKeeper interface {
// CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface {
IterateCdpsByCollateralType(ctx sdk.Context, collateralType string, cb func(cdp cdptypes.CDP) (stop bool))
GetTotalPrincipal(ctx sdk.Context, collateralType string, principalDenom string) (total sdk.Int)
GetCdpByOwnerAndCollateralType(ctx sdk.Context, owner sdk.AccAddress, collateralType string) (cdptypes.CDP, bool)
GetInterestFactor(ctx sdk.Context, collateralType string) (sdk.Dec, bool)
}
// AccountKeeper defines the expected keeper interface for interacting with account
@ -26,3 +27,9 @@ type AccountKeeper interface {
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
SetAccount(ctx sdk.Context, acc authexported.Account)
}
// CDPHooks event hooks for other keepers to run code in response to CDP modifications
type CDPHooks interface {
AfterCDPCreated(ctx sdk.Context, cdp cdptypes.CDP)
BeforeCDPModified(ctx sdk.Context, cdp cdptypes.CDP)
}

View File

@ -2,71 +2,25 @@ package types
import (
"bytes"
"errors"
"fmt"
"strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
)
// GenesisClaimPeriodID stores the next claim id and its corresponding collateral type
type GenesisClaimPeriodID struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
ID uint64 `json:"id" yaml:"id"`
}
// Validate performs a basic check of a GenesisClaimPeriodID fields.
func (gcp GenesisClaimPeriodID) Validate() error {
if gcp.ID == 0 {
return errors.New("genesis claim period id cannot be 0")
}
if strings.TrimSpace(gcp.CollateralType) == "" {
return fmt.Errorf("collateral type cannot be blank: %v", gcp)
}
return nil
}
// GenesisClaimPeriodIDs array of GenesisClaimPeriodID
type GenesisClaimPeriodIDs []GenesisClaimPeriodID
// Validate checks if all the GenesisClaimPeriodIDs are valid and there are no duplicated
// entries.
func (gcps GenesisClaimPeriodIDs) Validate() error {
seenIDS := make(map[string]bool)
var key string
for _, gcp := range gcps {
key = gcp.CollateralType + fmt.Sprint(gcp.ID)
if seenIDS[key] {
return fmt.Errorf("duplicated genesis claim period with id %d and collateral type %s", gcp.ID, gcp.CollateralType)
}
if err := gcp.Validate(); err != nil {
return err
}
seenIDS[key] = true
}
return nil
}
// 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"`
PreviousAccumulationTimes GenesisAccumulationTimes `json:"previous_accumulation_times" yaml:"previous_accumulation_times"`
USDXMintingClaims USDXMintingClaims `json:"usdx_minting_claims" yaml:"usdx_minting_claims"`
}
// NewGenesisState returns a new genesis state
func NewGenesisState(params Params, previousBlockTime time.Time, rp RewardPeriods, cp ClaimPeriods, c Claims, ids GenesisClaimPeriodIDs) GenesisState {
func NewGenesisState(params Params, prevAccumTimes GenesisAccumulationTimes, c USDXMintingClaims) GenesisState {
return GenesisState{
Params: params,
PreviousBlockTime: previousBlockTime,
RewardPeriods: rp,
ClaimPeriods: cp,
Claims: c,
NextClaimPeriodIDs: ids,
PreviousAccumulationTimes: prevAccumTimes,
USDXMintingClaims: c,
}
}
@ -74,11 +28,8 @@ func NewGenesisState(params Params, previousBlockTime time.Time, rp RewardPeriod
func DefaultGenesisState() GenesisState {
return GenesisState{
Params: DefaultParams(),
PreviousBlockTime: DefaultPreviousBlockTime,
RewardPeriods: RewardPeriods{},
ClaimPeriods: ClaimPeriods{},
Claims: Claims{},
NextClaimPeriodIDs: GenesisClaimPeriodIDs{},
PreviousAccumulationTimes: GenesisAccumulationTimes{},
USDXMintingClaims: DefaultClaims,
}
}
@ -88,19 +39,11 @@ func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil {
return err
}
if gs.PreviousBlockTime.IsZero() {
return errors.New("previous block time cannot be 0")
}
if err := gs.RewardPeriods.Validate(); err != nil {
if err := gs.PreviousAccumulationTimes.Validate(); err != nil {
return err
}
if err := gs.ClaimPeriods.Validate(); err != nil {
return err
}
if err := gs.Claims.Validate(); err != nil {
return err
}
return gs.NextClaimPeriodIDs.Validate()
return gs.USDXMintingClaims.Validate()
}
// Equal checks whether two gov GenesisState structs are equivalent
@ -114,3 +57,43 @@ func (gs GenesisState) Equal(gs2 GenesisState) bool {
func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{})
}
// GenesisAccumulationTime stores the previous reward distribution time and its corresponding collateral type
type GenesisAccumulationTime struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
PreviousAccumulationTime time.Time `json:"previous_accumulation_time" yaml:"previous_accumulation_time"`
RewardFactor sdk.Dec `json:"reward_factor" yaml:"reward_factor"`
}
// NewGenesisAccumulationTime returns a new GenesisAccumulationTime
func NewGenesisAccumulationTime(ctype string, prevTime time.Time, factor sdk.Dec) GenesisAccumulationTime {
return GenesisAccumulationTime{
CollateralType: ctype,
PreviousAccumulationTime: prevTime,
RewardFactor: factor,
}
}
// GenesisAccumulationTimes slice of GenesisAccumulationTime
type GenesisAccumulationTimes []GenesisAccumulationTime
// Validate performs validation of GenesisAccumulationTimes
func (gats GenesisAccumulationTimes) Validate() error {
for _, gat := range gats {
if err := gat.Validate(); err != nil {
return err
}
}
return nil
}
// Validate performs validation of GenesisAccumulationTime
func (gat GenesisAccumulationTime) Validate() error {
if gat.RewardFactor == (sdk.Dec{}) {
return fmt.Errorf("reward factor not initialized for %s", gat.CollateralType)
}
if gat.RewardFactor.LT(sdk.ZeroDec()) {
return fmt.Errorf("reward factor should be ≥ 0.0, is %s for %s", gat.RewardFactor, gat.CollateralType)
}
return nil
}

View File

@ -1,166 +1,145 @@
package types
import (
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/tendermint/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types"
)
func TestGenesisClaimPeriodIDsValidate(t *testing.T) {
testCases := []struct {
msg string
genesisClaimPeriodIDs GenesisClaimPeriodIDs
expPass bool
}{
{
"valid",
GenesisClaimPeriodIDs{
{CollateralType: "bnb", ID: 1},
},
true,
},
{
"invalid collateral type",
GenesisClaimPeriodIDs{
{CollateralType: "", ID: 1},
},
false,
},
{
"invalid ID",
GenesisClaimPeriodIDs{
{CollateralType: "bnb", ID: 0},
},
false,
},
{
"duplicate",
GenesisClaimPeriodIDs{
{CollateralType: "bnb", ID: 1},
{CollateralType: "bnb", ID: 1},
},
false,
},
}
for _, tc := range testCases {
err := tc.genesisClaimPeriodIDs.Validate()
if tc.expPass {
require.NoError(t, err, tc.msg)
} else {
require.Error(t, err, tc.msg)
}
}
}
func TestGenesisStateValidate(t *testing.T) {
now := time.Now()
mockPrivKey := tmtypes.NewMockPV()
pubkey, err := mockPrivKey.GetPubKey()
require.NoError(t, err)
owner := sdk.AccAddress(pubkey.Address())
rewards := Rewards{
NewReward(
true, "bnb", sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
time.Hour*24*7, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}, time.Hour*24*14,
),
type args struct {
params Params
genAccTimes GenesisAccumulationTimes
claims USDXMintingClaims
}
type errArgs struct {
expectPass bool
contains string
}
rewardPeriods := RewardPeriods{NewRewardPeriod("bnb", now, now.Add(time.Hour), sdk.NewCoin("bnb", sdk.OneInt()), now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))})}
claimPeriods := ClaimPeriods{NewClaimPeriod("bnb", 10, now, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))})}
claims := Claims{NewClaim(owner, sdk.NewCoin("bnb", sdk.OneInt()), "bnb", 10)}
gcps := GenesisClaimPeriodIDs{{CollateralType: "bnb", ID: 1}}
testCases := []struct {
msg string
genesisState GenesisState
expPass bool
name string
args args
errArgs errArgs
}{
{
msg: "default",
genesisState: DefaultGenesisState(),
expPass: true,
name: "default",
args: args{
params: DefaultParams(),
genAccTimes: DefaultGenesisAccumulationTimes,
claims: DefaultClaims,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
msg: "valid genesis",
genesisState: NewGenesisState(
NewParams(true, rewards),
now, rewardPeriods, claimPeriods, claims, gcps,
),
expPass: true,
name: "valid",
args: args{
params: NewParams(RewardPeriods{NewRewardPeriod(true, "bnb-a", time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC), sdk.NewCoin("ukava", sdk.NewInt(25000)))}, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}, time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC)),
genAccTimes: GenesisAccumulationTimes{GenesisAccumulationTime{
CollateralType: "bnb-a",
PreviousAccumulationTime: time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
RewardFactor: sdk.ZeroDec(),
}},
claims: USDXMintingClaims{
{
Owner: sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))),
Reward: sdk.NewCoin("ukava", sdk.NewInt(100000000)),
RewardIndexes: []RewardIndex{
{
CollateralType: "bnb-a",
RewardFactor: sdk.ZeroDec(),
},
},
},
},
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
},
{
msg: "invalid Params",
genesisState: GenesisState{
Params: Params{
Active: true,
Rewards: Rewards{
Reward{},
name: "invalid genesis accumulation time",
args: args{
params: DefaultParams(),
genAccTimes: GenesisAccumulationTimes{
{
CollateralType: "btcb-a",
RewardFactor: sdk.MustNewDecFromStr("-0.1"),
},
},
claims: DefaultClaims,
},
errArgs: errArgs{
expectPass: false,
contains: "reward factor should be ≥ 0.0",
},
expPass: false,
},
{
msg: "zero PreviousBlockTime",
genesisState: GenesisState{
PreviousBlockTime: time.Time{},
name: "invalid genesis accumulation time",
args: args{
params: DefaultParams(),
genAccTimes: GenesisAccumulationTimes{
{
CollateralType: "btcb-a",
PreviousAccumulationTime: time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
RewardFactor: sdk.Dec{},
},
},
claims: DefaultClaims,
},
errArgs: errArgs{
expectPass: false,
contains: "reward factor not initialized",
},
expPass: false,
},
{
msg: "invalid RewardsPeriod",
genesisState: GenesisState{
PreviousBlockTime: now,
RewardPeriods: RewardPeriods{
{Start: time.Time{}},
},
},
expPass: false,
},
name: "invalid claim",
args: args{
params: DefaultParams(),
genAccTimes: DefaultGenesisAccumulationTimes,
claims: USDXMintingClaims{
{
msg: "invalid ClaimPeriods",
genesisState: GenesisState{
PreviousBlockTime: now,
ClaimPeriods: ClaimPeriods{
{ID: 0},
},
},
expPass: false,
},
Owner: sdk.AccAddress{},
Reward: sdk.NewCoin("ukava", sdk.NewInt(100000000)),
RewardIndexes: []RewardIndex{
{
msg: "invalid Claims",
genesisState: GenesisState{
PreviousBlockTime: now,
Claims: Claims{
{ClaimPeriodID: 0},
CollateralType: "bnb-a",
RewardFactor: sdk.ZeroDec(),
},
},
expPass: false,
},
{
msg: "invalid NextClaimPeriodIds",
genesisState: GenesisState{
PreviousBlockTime: now,
NextClaimPeriodIDs: GenesisClaimPeriodIDs{
{ID: 0},
},
},
expPass: false,
},
errArgs: errArgs{
expectPass: false,
contains: "claim owner cannot be empty",
},
},
}
for _, tc := range testCases {
err := tc.genesisState.Validate()
if tc.expPass {
require.NoError(t, err, tc.msg)
t.Run(tc.name, func(t *testing.T) {
gs := NewGenesisState(tc.args.params, tc.args.genAccTimes, tc.args.claims)
err := gs.Validate()
if tc.errArgs.expectPass {
require.NoError(t, err, tc.name)
} else {
require.Error(t, err, tc.msg)
}
require.Error(t, err, tc.name)
require.True(t, strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}
}

View File

@ -1,11 +1,5 @@
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"
@ -25,37 +19,9 @@ const (
// 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
ClaimKeyPrefix = []byte{0x01} // prefix for keys that store claims
BlockTimeKey = []byte{0x02} // prefix for key that stores the blocktime
RewardFactorKey = []byte{0x03} // prefix for key that stores reward factors
USDXMintingRewardDenom = "ukava"
)
// Keys
// 0x00:CollateralType <> RewardPeriod the current active reward period (max 1 reward period per collateral type)
// 0x01:CollateralType:ID <> ClaimPeriod object for that ID, indexed by collateral type and ID
// 0x02:CollateralType:ID:Owner <> Claim object, indexed by collateral type, ID and owner
// 0x03:CollateralType <> NextClaimPeriodIDPrefix the ID of the next claim period, indexed by collateral type
// BytesToUint64 returns uint64 format from a byte array
func BytesToUint64(bz []byte) uint64 {
return binary.BigEndian.Uint64(bz)
}
// GetClaimPeriodPrefix returns the key (collateral type + id) for a claim prefix
func GetClaimPeriodPrefix(collateralType string, id uint64) []byte {
return createKey([]byte(collateralType), sdk.Uint64ToBigEndian(id))
}
// GetClaimPrefix returns the key (collateral type + id + address) for a claim
func GetClaimPrefix(addr sdk.AccAddress, collateralType string, id uint64) []byte {
return createKey([]byte(collateralType), sdk.Uint64ToBigEndian(id), addr)
}
func createKey(bytes ...[]byte) (r []byte) {
for _, b := range bytes {
r = append(r, b...)
}
return
}

View File

@ -1,7 +1,6 @@
package types
import (
"fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
@ -9,48 +8,43 @@ import (
)
// ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgClaimReward{}
var _ sdk.Msg = &MsgClaimUSDXMintingReward{}
// MsgClaimReward message type used to claim rewards
type MsgClaimReward struct {
// MsgClaimUSDXMintingReward message type used to claim rewards
type MsgClaimUSDXMintingReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
}
// NewMsgClaimReward returns a new MsgClaimReward.
func NewMsgClaimReward(sender sdk.AccAddress, collateralType, multiplierName string) MsgClaimReward {
return MsgClaimReward{
// NewMsgClaimUSDXMintingReward returns a new MsgClaimUSDXMintingReward.
func NewMsgClaimUSDXMintingReward(sender sdk.AccAddress, multiplierName string) MsgClaimUSDXMintingReward {
return MsgClaimUSDXMintingReward{
Sender: sender,
CollateralType: collateralType,
MultiplierName: multiplierName,
}
}
// Route return the message type used for routing the message.
func (msg MsgClaimReward) Route() string { return RouterKey }
func (msg MsgClaimUSDXMintingReward) 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" }
func (msg MsgClaimUSDXMintingReward) Type() string { return "claim_reward" }
// ValidateBasic does a simple validation check that doesn't require access to state.
func (msg MsgClaimReward) ValidateBasic() error {
func (msg MsgClaimUSDXMintingReward) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if strings.TrimSpace(msg.CollateralType) == "" {
return fmt.Errorf("collateral type cannot be blank")
}
return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid()
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgClaimReward) GetSignBytes() []byte {
func (msg MsgClaimUSDXMintingReward) 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 {
func (msg MsgClaimUSDXMintingReward) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}

View File

@ -14,7 +14,6 @@ import (
type msgTest struct {
from sdk.AccAddress
collateralType string
multiplierName string
expectPass bool
}
@ -29,25 +28,26 @@ func (suite *MsgTestSuite) SetupTest() {
tests := []msgTest{
{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
collateralType: "bnb",
multiplierName: "large",
expectPass: true,
},
{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
collateralType: "",
multiplierName: "medium",
expectPass: true,
},
{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
multiplierName: "small",
expectPass: false,
expectPass: true,
},
{
from: sdk.AccAddress{},
collateralType: "bnb",
multiplierName: "medium",
expectPass: false,
},
{
from: sdk.AccAddress(crypto.AddressHash([]byte("KavaTest1"))),
collateralType: "bnb",
multiplierName: "huge",
expectPass: false,
},
@ -57,7 +57,7 @@ func (suite *MsgTestSuite) SetupTest() {
func (suite *MsgTestSuite) TestMsgValidation() {
for _, t := range suite.tests {
msg := types.NewMsgClaimReward(t.from, t.collateralType, t.multiplierName)
msg := types.NewMsgClaimUSDXMintingReward(t.from, t.multiplierName)
err := msg.ValidateBasic()
if t.expectPass {
suite.Require().NoError(err)

View File

@ -1,6 +1,7 @@
package types
import (
"errors"
"fmt"
"strings"
"time"
@ -24,10 +25,15 @@ const (
// Parameter keys and default values
var (
KeyActive = []byte("Active")
KeyRewards = []byte("Rewards")
KeyRewards = []byte("RewardPeriods")
KeyClaimEnd = []byte("ClaimEnd")
KeyMultipliers = []byte("ClaimMultipliers")
DefaultActive = false
DefaultRewards = Rewards{}
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
DefaultRewardPeriods = RewardPeriods{}
DefaultMultipliers = Multipliers{}
DefaultClaims = USDXMintingClaims{}
DefaultGenesisAccumulationTimes = GenesisAccumulationTimes{}
DefaultClaimEnd = tmtime.Canonical(time.Unix(0, 0))
GovDenom = cdptypes.DefaultGovDenom
PrincipalDenom = "usdx"
IncentiveMacc = kavadistTypes.ModuleName
@ -35,28 +41,32 @@ var (
// 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"`
RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"`
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
}
// NewParams returns a new params object
func NewParams(active bool, rewards Rewards) Params {
func NewParams(rewards RewardPeriods, multipliers Multipliers, claimEnd time.Time) Params {
return Params{
Active: active,
Rewards: rewards,
RewardPeriods: rewards,
ClaimMultipliers: multipliers,
ClaimEnd: claimEnd,
}
}
// DefaultParams returns default params for incentive module
func DefaultParams() Params {
return NewParams(DefaultActive, DefaultRewards)
return NewParams(DefaultRewardPeriods, DefaultMultipliers, DefaultClaimEnd)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Active: %t
Rewards: %s`, p.Active, p.Rewards)
Rewards: %s
Claim Multipliers :%s
Claim End Time: %s
`, p.RewardPeriods, p.ClaimMultipliers, p.ClaimEnd)
}
// ParamKeyTable Key declaration for parameters
@ -67,18 +77,19 @@ func ParamKeyTable() params.KeyTable {
// 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),
params.NewParamSetPair(KeyRewards, &p.RewardPeriods, validateRewardsParam),
params.NewParamSetPair(KeyClaimEnd, &p.ClaimEnd, validateClaimEndParam),
params.NewParamSetPair(KeyMultipliers, &p.ClaimMultipliers, validateMultipliersParam),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateActiveParam(p.Active); err != nil {
if err := validateMultipliersParam(p.ClaimMultipliers); err != nil {
return err
}
return validateRewardsParam(p.Rewards)
return validateRewardsParam(p.RewardPeriods)
}
func validateActiveParam(i interface{}) error {
@ -90,7 +101,7 @@ func validateActiveParam(i interface{}) error {
}
func validateRewardsParam(i interface{}) error {
rewards, ok := i.(Rewards)
rewards, ok := i.(RewardPeriods)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
@ -98,93 +109,97 @@ func validateRewardsParam(i interface{}) error {
return rewards.Validate()
}
// 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
CollateralType string `json:"collateral_type" yaml:"collateral_type"` // the collateral type 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
ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"` // the reward multiplier and timelock schedule - applied at the time users claim rewards
ClaimDuration time.Duration `json:"claim_duration" yaml:"claim_duration"` // how long users have after the period ends to claim their rewards
func validateMultipliersParam(i interface{}) error {
multipliers, ok := i.(Multipliers)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
return multipliers.Validate()
}
// NewReward returns a new Reward
func NewReward(active bool, collateralType string, reward sdk.Coin, duration time.Duration, multiplier Multipliers, claimDuration time.Duration) Reward {
return Reward{
func validateClaimEndParam(i interface{}) error {
endTime, ok := i.(time.Time)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if endTime.IsZero() {
return fmt.Errorf("end time should not be zero")
}
return nil
}
// RewardPeriod stores the state of an ongoing reward
type RewardPeriod struct {
Active bool `json:"active" yaml:"active"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
Start time.Time `json:"start" yaml:"start"`
End time.Time `json:"end" yaml:"end"`
RewardsPerSecond sdk.Coin `json:"rewards_per_second" yaml:"rewards_per_second"` // per second reward payouts
}
// String implements fmt.Stringer
func (rp RewardPeriod) String() string {
return fmt.Sprintf(`Reward Period:
Collateral Type: %s,
Start: %s,
End: %s,
Rewards Per Second: %s,
Active %t,
`, rp.CollateralType, rp.Start, rp.End, rp.RewardsPerSecond, rp.Active)
}
// NewRewardPeriod returns a new RewardPeriod
func NewRewardPeriod(active bool, collateralType string, start time.Time, end time.Time, reward sdk.Coin) RewardPeriod {
return RewardPeriod{
Active: active,
CollateralType: collateralType,
AvailableRewards: reward,
Duration: duration,
ClaimMultipliers: multiplier,
ClaimDuration: claimDuration,
Start: start,
End: end,
RewardsPerSecond: reward,
}
}
// String implements fmt.Stringer
func (r Reward) String() string {
return fmt.Sprintf(`Reward:
Active: %t,
CollateralType: %s,
Available Rewards: %s,
Duration: %s,
%s,
Claim Duration: %s`,
r.Active, r.CollateralType, r.AvailableRewards, r.Duration, r.ClaimMultipliers, r.ClaimDuration)
// Validate performs a basic check of a RewardPeriod fields.
func (rp RewardPeriod) Validate() error {
if rp.Start.IsZero() {
return errors.New("reward period start time cannot be 0")
}
// Validate performs a basic check of a reward fields.
func (r Reward) Validate() error {
if !r.AvailableRewards.IsValid() {
return fmt.Errorf("invalid reward coins %s for %s", r.AvailableRewards, r.CollateralType)
if rp.End.IsZero() {
return errors.New("reward period end time cannot be 0")
}
if !r.AvailableRewards.IsPositive() {
return fmt.Errorf("reward amount must be positive, is %s for %s", r.AvailableRewards, r.CollateralType)
if rp.Start.After(rp.End) {
return fmt.Errorf("end period time %s cannot be before start time %s", rp.End, rp.Start)
}
if r.Duration <= 0 {
return fmt.Errorf("reward duration must be positive, is %s for %s", r.Duration, r.CollateralType)
if !rp.RewardsPerSecond.IsValid() {
return fmt.Errorf("invalid reward amount: %s", rp.RewardsPerSecond)
}
if err := r.ClaimMultipliers.Validate(); err != nil {
return err
}
if r.ClaimDuration <= 0 {
return fmt.Errorf("claim duration must be positive, is %s for %s", r.ClaimDuration, r.CollateralType)
}
if strings.TrimSpace(r.CollateralType) == "" {
return fmt.Errorf("collateral type cannot be blank: %s", r)
if strings.TrimSpace(rp.CollateralType) == "" {
return fmt.Errorf("reward period collateral type cannot be blank: %s", rp)
}
return nil
}
// Rewards array of Reward
type Rewards []Reward
// RewardPeriods array of RewardPeriod
type RewardPeriods []RewardPeriod
// Validate checks if all the rewards are valid and there are no duplicated
// Validate checks if all the RewardPeriods are valid and there are no duplicated
// entries.
func (rs Rewards) Validate() error {
rewardCollateralTypes := make(map[string]bool)
for _, r := range rs {
if rewardCollateralTypes[r.CollateralType] {
return fmt.Errorf("cannot have duplicate reward collateral types: %s", r.CollateralType)
func (rps RewardPeriods) Validate() error {
seenPeriods := make(map[string]bool)
for _, rp := range rps {
if seenPeriods[rp.CollateralType] {
return fmt.Errorf("duplicated reward period with collateral type %s", rp.CollateralType)
}
if err := r.Validate(); err != nil {
if err := rp.Validate(); err != nil {
return err
}
rewardCollateralTypes[r.CollateralType] = true
seenPeriods[rp.CollateralType] = true
}
return nil
}
// String implements fmt.Stringer
func (rs Rewards) String() string {
out := "Rewards\n"
for _, r := range rs {
out += fmt.Sprintf("%s\n", r)
}
return out
}
// Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked
type Multiplier struct {
Name MultiplierName `json:"name" yaml:"name"`
@ -209,6 +224,9 @@ func (m Multiplier) Validate() error {
if m.MonthsLockup < 0 {
return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup)
}
if m.Factor == (sdk.Dec{}) {
return fmt.Errorf("claim multiplier factor not initialized for %s", m.Name)
}
if m.Factor.IsNegative() {
return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String())
}

View File

@ -5,212 +5,104 @@ import (
"testing"
"time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types"
)
type paramTest struct {
name string
params types.Params
errResult errResult
type ParamTestSuite struct {
suite.Suite
}
type errResult struct {
func (suite *ParamTestSuite) SetupTest() {}
func (suite *ParamTestSuite) TestParamValidation() {
type args struct {
rewardPeriods types.RewardPeriods
multipliers types.Multipliers
end time.Time
}
type errArgs struct {
expectPass bool
contains string
}
type ParamTestSuite struct {
suite.Suite
tests []paramTest
type test struct {
name string
args args
errArgs errArgs
}
func (suite *ParamTestSuite) SetupTest() {
suite.tests = []paramTest{
testCases := []test{
{
name: "valid - active",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
"default",
args{
rewardPeriods: types.DefaultRewardPeriods,
multipliers: types.DefaultMultipliers,
end: types.DefaultClaimEnd,
},
},
},
errResult: errResult{
errArgs{
expectPass: true,
contains: "",
},
},
{
name: "valid - inactive",
params: types.Params{
Active: false,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
"valid",
args{
rewardPeriods: types.RewardPeriods{types.NewRewardPeriod(
true, "bnb-a", time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)))},
multipliers: types.Multipliers{
types.NewMultiplier(
types.Small, 1, sdk.MustNewDecFromStr("0.25"),
),
types.NewMultiplier(
types.Large, 1, sdk.MustNewDecFromStr("1.0"),
),
},
end: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC),
},
},
errResult: errResult{
errArgs{
expectPass: true,
contains: "",
},
},
{
name: "duplicate reward",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
"invalid: empty reward factor",
args{
rewardPeriods: types.RewardPeriods{types.NewRewardPeriod(
true, "bnb-a", time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC), time.Date(2024, 10, 15, 14, 0, 0, 0, time.UTC),
sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)))},
multipliers: types.Multipliers{
types.NewMultiplier(
types.Small, 1, sdk.MustNewDecFromStr("0.25"),
),
types.NewMultiplier(
types.Large, 1, sdk.Dec{},
),
},
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
end: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC),
},
},
},
errResult: errResult{
errArgs{
expectPass: false,
contains: "cannot have duplicate reward collateral type",
contains: "claim multiplier factor not initialized",
},
},
{
name: "negative reward duration",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * -24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "reward duration must be positive",
},
},
{
name: "negative time lock",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, -1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "expected non-negative lockup",
},
},
{
name: "zero claim duration",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 0,
},
},
},
errResult: errResult{
expectPass: false,
contains: "claim duration must be positive",
},
},
{
name: "zero reward",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "bnb-a",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(0)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "reward amount must be positive",
},
},
{
name: "empty reward collateral type",
params: types.Params{
Active: true,
Rewards: types.Rewards{
types.Reward{
Active: true,
CollateralType: "",
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(1)),
Duration: time.Hour * 24 * 7,
ClaimMultipliers: types.Multipliers{types.NewMultiplier(types.Small, 1, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Large, 12, sdk.MustNewDecFromStr("1.0"))},
ClaimDuration: time.Hour * 24 * 14,
},
},
},
errResult: errResult{
expectPass: false,
contains: "collateral type cannot be blank",
},
},
}
}
func (suite *ParamTestSuite) TestParamValidation() {
for _, t := range suite.tests {
suite.Run(t.name, func() {
err := t.params.Validate()
if t.errResult.expectPass {
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(
tc.args.rewardPeriods, tc.args.multipliers, tc.args.end,
)
err := params.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err)
} else {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), t.errResult.contains))
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
}
})
}

View File

@ -17,15 +17,17 @@ const (
// QueryClaimsParams params for query /incentive/claims
type QueryClaimsParams struct {
Page int `json:"page" yaml:"page"`
Limit int `json:"limit" yaml:"limit"`
Owner sdk.AccAddress
CollateralType string
}
// NewQueryClaimsParams returns QueryClaimsParams
func NewQueryClaimsParams(owner sdk.AccAddress, collateralType string) QueryClaimsParams {
func NewQueryClaimsParams(page, limit int, owner sdk.AccAddress) QueryClaimsParams {
return QueryClaimsParams{
Page: page,
Limit: limit,
Owner: owner,
CollateralType: collateralType,
}
}
@ -33,6 +35,5 @@ func NewQueryClaimsParams(owner sdk.AccAddress, collateralType string) QueryClai
type PostClaimReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
}

Some files were not shown because too many files have changed in this diff Show More