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, auction.ModuleName: nil,
cdp.ModuleName: {supply.Minter, supply.Burner}, cdp.ModuleName: {supply.Minter, supply.Burner},
cdp.LiquidatorMacc: {supply.Minter, supply.Burner}, cdp.LiquidatorMacc: {supply.Minter, supply.Burner},
cdp.SavingsRateMacc: {supply.Minter},
bep3.ModuleName: {supply.Minter, supply.Burner}, bep3.ModuleName: {supply.Minter, supply.Burner},
kavadist.ModuleName: {supply.Minter}, kavadist.ModuleName: {supply.Minter},
issuance.ModuleAccountName: {supply.Minter, supply.Burner}, 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, app.supplyKeeper,
auctionSubspace, auctionSubspace,
) )
app.cdpKeeper = cdp.NewKeeper( cdpKeeper := cdp.NewKeeper(
app.cdc, app.cdc,
keys[cdp.StoreKey], keys[cdp.StoreKey],
cdpSubspace, cdpSubspace,
@ -370,7 +369,7 @@ func NewApp(logger log.Logger, db dbm.DB, traceStore io.Writer, appOpts AppOptio
keys[incentive.StoreKey], keys[incentive.StoreKey],
incentiveSubspace, incentiveSubspace,
app.supplyKeeper, app.supplyKeeper,
app.cdpKeeper, &cdpKeeper,
app.accountKeeper, app.accountKeeper,
) )
app.issuanceKeeper = issuance.NewKeeper( 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( app.stakingKeeper = *stakingKeeper.SetHooks(
staking.NewMultiStakingHooks(app.distrKeeper.Hooks(), app.slashingKeeper.Hooks())) 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 // create the module manager (Note: Any module instantiated in the module manager that is later modified
// must be passed by reference here.) // must be passed by reference here.)
app.mm = module.NewManager( 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" 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_11bep3 "github.com/kava-labs/kava/x/bep3"
v0_9bep3 "github.com/kava-labs/kava/x/bep3/legacy/v0_9" 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_9cdp "github.com/kava-labs/kava/x/cdp/legacy/v0_9"
v0_11committee "github.com/kava-labs/kava/x/committee" v0_11committee "github.com/kava-labs/kava/x/committee"
v0_9committee "github.com/kava-labs/kava/x/committee/legacy/v0_9" v0_9committee "github.com/kava-labs/kava/x/committee/legacy/v0_9"
v0_11harvest "github.com/kava-labs/kava/x/hard" v0_11harvest "github.com/kava-labs/kava/x/hard/legacy/v0_11"
v0_11incentive "github.com/kava-labs/kava/x/incentive" 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_9incentive "github.com/kava-labs/kava/x/incentive/legacy/v0_9"
v0_11issuance "github.com/kava-labs/kava/x/issuance" v0_11issuance "github.com/kava-labs/kava/x/issuance"
v0_11pricefeed "github.com/kava-labs/kava/x/pricefeed" 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) delete(v0_9AppState, v0_9pricefeed.ModuleName)
v0_11AppState[v0_9pricefeed.ModuleName] = v0_11Codec.MustMarshalJSON(MigratePricefeed(pricefeedGenState)) v0_11AppState[v0_9pricefeed.ModuleName] = v0_11Codec.MustMarshalJSON(MigratePricefeed(pricefeedGenState))
} }
// v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest()) v0_11AppState[v0_11harvest.ModuleName] = v0_11Codec.MustMarshalJSON(MigrateHarvest())
v0_11AppState[v0_11issuance.ModuleName] = v0_11Codec.MustMarshalJSON(v0_11issuance.DefaultGenesisState()) v0_11AppState[v0_11issuance.ModuleName] = v0_11Codec.MustMarshalJSON(v0_11issuance.DefaultGenesisState())
return v0_11AppState return v0_11AppState
} }
@ -308,7 +308,6 @@ func MigrateCommittee(oldGenState v0_9committee.GenesisState) v0_11committee.Gen
DebtFloor: oldDebtParam.DebtFloor, DebtFloor: oldDebtParam.DebtFloor,
Denom: oldDebtParam.Denom, Denom: oldDebtParam.Denom,
ReferenceAsset: oldDebtParam.ReferenceAsset, ReferenceAsset: oldDebtParam.ReferenceAsset,
SavingsRate: oldDebtParam.SavingsRate,
} }
oldAssetParam := subPermission.AllowedAssetParams[0] oldAssetParam := subPermission.AllowedAssetParams[0]
newAssetParam := v0_11committee.AllowedAssetParam{ newAssetParam := v0_11committee.AllowedAssetParam{
@ -633,34 +632,29 @@ func MigrateGov(oldGenState v39_1gov.GenesisState) v39_1gov.GenesisState {
return oldGenState return oldGenState
} }
// // MigrateHarvest initializes the harvest genesis state for kava-4 // MigrateHarvest initializes the harvest genesis state for kava-4
// func MigrateHarvest() v0_11harvest.GenesisState { func MigrateHarvest() v0_11harvest.GenesisState {
// // total HARD per second for lps (week one): 633761 // total HARD per second for lps (week one): 633761
// // HARD per second for delegators (week one): 1267522 // HARD per second for delegators (week one): 1267522
// incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC) incentiveGoLiveDate := time.Date(2020, 10, 16, 14, 0, 0, 0, time.UTC)
// incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC) incentiveEndDate := time.Date(2024, 10, 16, 14, 0, 0, 0, time.UTC)
// claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC) claimEndDate := time.Date(2025, 10, 16, 14, 0, 0, 0, time.UTC)
// harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams( harvestGS := v0_11harvest.NewGenesisState(v0_11harvest.NewParams(
// true, true,
// v0_11harvest.DistributionSchedules{ v0_11harvest.DistributionSchedules{
// v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), v0_11harvest.NewDistributionSchedule(true, "usdx", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(310543)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
// v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), v0_11harvest.NewDistributionSchedule(true, "hard", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(285193)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
// v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), v0_11harvest.NewDistributionSchedule(true, "bnb", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(12675)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
// v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(25350)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
// }, },
// v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule( v0_11harvest.DelegatorDistributionSchedules{v0_11harvest.NewDelegatorDistributionSchedule(
// v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}), v0_11harvest.NewDistributionSchedule(true, "ukava", incentiveGoLiveDate, incentiveEndDate, sdk.NewCoin("hard", sdk.NewInt(1267522)), claimEndDate, v0_11harvest.Multipliers{v0_11harvest.NewMultiplier(v0_11harvest.Small, 1, sdk.MustNewDecFromStr("0.33")), v0_11harvest.NewMultiplier(v0_11harvest.Large, 12, sdk.OneDec())}),
// time.Hour*24, time.Hour*24,
// ), ),
// }, },
// v0_11harvest.BlockLimits{ ), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes)
// v0_11harvest.NewBlockLimit("usdx", sdk.Dec(0.9)), return harvestGS
// v0_11harvest.NewBlockLimit("ukava", sdk.Dec(0.6)), }
// v0_11harvest.NewBlockLimit("bnb", sdk.Dec(0.9)),
// },
// ), v0_11harvest.DefaultPreviousBlockTime, v0_11harvest.DefaultDistributionTimes)
// return harvestGS
// }
// MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state // MigrateCDP migrates from a v0.9 (or v0.10) cdp genesis state to a v0.11 cdp genesis state
func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState { func MigrateCDP(oldGenState v0_9cdp.GenesisState) v0_11cdp.GenesisState {

View File

@ -12,11 +12,8 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
v39_1auth "github.com/cosmos/cosmos-sdk/x/auth" v39_1auth "github.com/cosmos/cosmos-sdk/x/auth"
v39_1auth_vesting "github.com/cosmos/cosmos-sdk/x/auth/vesting" 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" v39_1supply "github.com/cosmos/cosmos-sdk/x/supply"
tmtypes "github.com/tendermint/tendermint/types"
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
v38_5auth "github.com/kava-labs/kava/migrate/v0_11/legacy/cosmos-sdk/v0.38.5/auth" 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" 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() err = newGenState.Validate()
require.NoError(t, err) 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 End Time: %s
Max End Time: %s Max End Time: %s
Max Bid %s Max Bid %s
LotReturns %s`, LotReturns %s
Corresponding Debt %s`,
a.GetID(), a.Initiator, a.Lot, a.GetID(), a.Initiator, a.Lot,
a.Bidder, a.Bid, a.GetEndTime().String(), 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) { func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
params := k.GetParams(ctx) params := k.GetParams(ctx)
previousDistTime, found := k.GetPreviousSavingsDistribution(ctx)
if !found {
previousDistTime = ctx.BlockTime()
k.SetPreviousSavingsDistribution(ctx, previousDistTime)
}
for _, cp := range params.CollateralParams { for _, cp := range params.CollateralParams {
ok := k.UpdatePricefeedStatus(ctx, cp.SpotMarketID) ok := k.UpdatePricefeedStatus(ctx, cp.SpotMarketID)
if !ok { if !ok {
@ -31,7 +25,12 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
continue 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 { if err != nil {
panic(err) panic(err)
} }
@ -46,16 +45,4 @@ func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k Keeper) {
if err != nil { if err != nil {
panic(err) 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) 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) cdp, _ := suite.keeper.GetCDP(suite.ctx, "xrp-a", 1)
err = suite.keeper.SeizeCollateral(suite.ctx, cdp) err = suite.keeper.SeizeCollateral(suite.ctx, cdp)

View File

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

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -41,8 +40,6 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
QueryCdpDepositsCmd(queryRoute, cdc), QueryCdpDepositsCmd(queryRoute, cdc),
QueryParamsCmd(queryRoute, cdc), QueryParamsCmd(queryRoute, cdc),
QueryGetAccounts(queryRoute, cdc), QueryGetAccounts(queryRoute, cdc),
QueryGetSavingsRateDistributed(queryRoute, cdc),
QueryGetSavingsRateDistTime(queryRoute, cdc),
)...) )...)
return cdpQueryCmd 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), GetCmdWithdraw(cdc),
GetCmdDraw(cdc), GetCmdDraw(cdc),
GetCmdRepay(cdc), GetCmdRepay(cdc),
GetCmdLiquidate(cdc),
)...) )...)
return cdpTxCmd 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) { func registerQueryRoutes(cliCtx context.CLIContext, r *mux.Router) {
r.HandleFunc("/cdp/accounts", getAccountsHandlerFn(cliCtx)).Methods("GET") r.HandleFunc("/cdp/accounts", getAccountsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc("/cdp/parameters", getParamsHandlerFn(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/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"), queryCdpsHandlerFn(cliCtx)).Methods("GET")
r.HandleFunc(fmt.Sprintf("/cdp/cdps/collateralType/{%s}", types.RestCollateralType), queryCdpsByCollateralTypeHandlerFn(cliCtx)).Methods("GET") // legacy 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 { func queryCdpsHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
_, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0) _, page, limit, err := rest.ParseHTTPArgsWithLimit(r, 0)

View File

@ -65,3 +65,10 @@ type PostRepayReq struct {
CollateralType string `json:"collateral_type" yaml:"collateral_type"` CollateralType string `json:"collateral_type" yaml:"collateral_type"`
Payment sdk.Coin `json:"payment" yaml:"payment"` 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}/withdraw", postWithdrawHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/cdp/{owner}/{collateralType}/draw", postDrawHandlerFn(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}/repay", postRepayHandlerFn(cliCtx)).Methods("POST")
r.HandleFunc("/cdp/{owner}/collateralType}/liquidate", postLiquidateHandlerFn(cliCtx)).Methods("POST")
} }
func postCdpHandlerFn(cliCtx context.CLIContext) http.HandlerFunc { 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}) 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 { if liqModuleAcc == nil {
panic(fmt.Sprintf("%s module account has not been set", LiquidatorMacc)) 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, // validate denoms - check that any collaterals in the params are in the pricefeed,
// pricefeed MUST call InitGenesis before cdp // 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) k.SetParams(ctx, gs.Params)
// set the per second fee rate for each collateral type for _, gat := range gs.PreviousAccumulationTimes {
for _, cp := range gs.Params.CollateralParams { k.SetInterestFactor(ctx, gat.CollateralType, gat.InterestFactor)
k.SetTotalPrincipal(ctx, cp.Type, gs.Params.DebtParam.Denom, sdk.ZeroInt()) 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 // add cdps
for _, cdp := range gs.CDPs { for _, cdp := range gs.CDPs {
if cdp.ID == gs.StartingCdpID { 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) k.IndexCdpByOwner(ctx, cdp)
ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) ratio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal())
k.IndexCdpByCollateralRatio(ctx, cdp.Type, cdp.ID, ratio) k.IndexCdpByCollateralRatio(ctx, cdp.Type, cdp.ID, ratio)
k.IncrementTotalPrincipal(ctx, cdp.Type, cdp.GetTotalPrincipal())
} }
k.SetNextCdpID(ctx, gs.StartingCdpID) k.SetNextCdpID(ctx, gs.StartingCdpID)
k.SetDebtDenom(ctx, gs.DebtDenom) k.SetDebtDenom(ctx, gs.DebtDenom)
k.SetGovDenom(ctx, gs.GovDenom) 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 { for _, d := range gs.Deposits {
k.SetDeposit(ctx, d) k.SetDeposit(ctx, d)
} }
k.SetSavingsRateDistributed(ctx, gs.SavingsRateDistributed)
} }
// ExportGenesis export genesis state for cdp module // ExportGenesis export genesis state for cdp module
@ -110,12 +105,22 @@ func ExportGenesis(ctx sdk.Context, k Keeper) GenesisState {
cdpID := k.GetNextCdpID(ctx) cdpID := k.GetNextCdpID(ctx)
debtDenom := k.GetDebtDenom(ctx) debtDenom := k.GetDebtDenom(ctx)
govDenom := k.GetGovDenom(ctx) govDenom := k.GetGovDenom(ctx)
savingsRateDist := k.GetSavingsRateDistributed(ctx)
previousDistributionTime, found := k.GetPreviousSavingsDistribution(ctx) var previousAccumTimes types.GenesisAccumulationTimes
if !found { var totalPrincipals types.GenesisTotalPrincipals
previousDistributionTime = DefaultPreviousDistributionTime
for _, cp := range params.CollateralParams {
interestFactor, found := k.GetInterestFactor(ctx, cp.Type)
if !found {
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

@ -22,14 +22,15 @@ type GenesisTestSuite struct {
func (suite *GenesisTestSuite) TestInvalidGenState() { func (suite *GenesisTestSuite) TestInvalidGenState() {
type args struct { type args struct {
params cdp.Params params cdp.Params
cdps cdp.CDPs cdps cdp.CDPs
deposits cdp.Deposits deposits cdp.Deposits
startingID uint64 startingID uint64
debtDenom string debtDenom string
govDenom string govDenom string
prevDistTime time.Time savingsRateDist sdk.Int
savingsRateDist sdk.Int genAccumTimes cdp.GenesisAccumulationTimes
genTotalPrincipals cdp.GenesisTotalPrincipals
} }
type errArgs struct { type errArgs struct {
expectPass bool expectPass bool
@ -48,13 +49,14 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
{ {
name: "empty debt denom", name: "empty debt denom",
args: args{ args: args{
params: cdp.DefaultParams(), params: cdp.DefaultParams(),
cdps: cdp.CDPs{}, cdps: cdp.CDPs{},
deposits: cdp.Deposits{}, deposits: cdp.Deposits{},
debtDenom: "", debtDenom: "",
govDenom: cdp.DefaultGovDenom, govDenom: cdp.DefaultGovDenom,
prevDistTime: cdp.DefaultPreviousDistributionTime, savingsRateDist: cdp.DefaultSavingsRateDistributed,
savingsRateDist: cdp.DefaultSavingsRateDistributed, genAccumTimes: cdp.DefaultGenesisState().PreviousAccumulationTimes,
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
}, },
errArgs: errArgs{ errArgs: errArgs{
expectPass: false, expectPass: false,
@ -64,13 +66,14 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
{ {
name: "empty gov denom", name: "empty gov denom",
args: args{ args: args{
params: cdp.DefaultParams(), params: cdp.DefaultParams(),
cdps: cdp.CDPs{}, cdps: cdp.CDPs{},
deposits: cdp.Deposits{}, deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom, debtDenom: cdp.DefaultDebtDenom,
govDenom: "", govDenom: "",
prevDistTime: cdp.DefaultPreviousDistributionTime, savingsRateDist: cdp.DefaultSavingsRateDistributed,
savingsRateDist: cdp.DefaultSavingsRateDistributed, genAccumTimes: cdp.DefaultGenesisState().PreviousAccumulationTimes,
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
}, },
errArgs: errArgs{ errArgs: errArgs{
expectPass: false, expectPass: false,
@ -78,48 +81,49 @@ func (suite *GenesisTestSuite) TestInvalidGenState() {
}, },
}, },
{ {
name: "empty distribution time", name: "interest factor below one",
args: args{ args: args{
params: cdp.DefaultParams(), params: cdp.DefaultParams(),
cdps: cdp.CDPs{}, cdps: cdp.CDPs{},
deposits: cdp.Deposits{}, deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom, debtDenom: cdp.DefaultDebtDenom,
govDenom: cdp.DefaultGovDenom, govDenom: cdp.DefaultGovDenom,
prevDistTime: time.Time{}, savingsRateDist: sdk.NewInt(100),
savingsRateDist: cdp.DefaultSavingsRateDistributed, genAccumTimes: cdp.GenesisAccumulationTimes{cdp.NewGenesisAccumulationTime("bnb-a", time.Time{}, sdk.OneDec().Sub(sdk.SmallestDec()))},
genTotalPrincipals: cdp.DefaultGenesisState().TotalPrincipals,
}, },
errArgs: errArgs{ errArgs: errArgs{
expectPass: false, 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{ args: args{
params: cdp.DefaultParams(), params: cdp.DefaultParams(),
cdps: cdp.CDPs{}, cdps: cdp.CDPs{},
deposits: cdp.Deposits{}, deposits: cdp.Deposits{},
debtDenom: cdp.DefaultDebtDenom, debtDenom: cdp.DefaultDebtDenom,
govDenom: cdp.DefaultGovDenom, 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "savings rate distributed should not be negative", contains: "total principal should be positive",
}, },
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {
gs := cdp.NewGenesisState(tc.args.params, tc.args.cdps, tc.args.deposits, tc.args.startingID, 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() err := gs.Validate()
if tc.errArgs.expectPass { if tc.errArgs.expectPass {
suite.Require().NoError(err) suite.Require().NoError(err)
} else { } else {
suite.Require().Error(err) suite.Require().Error(err)
suite.T().Log(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains)) 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) return handleMsgDrawDebt(ctx, k, msg)
case MsgRepayDebt: case MsgRepayDebt:
return handleMsgRepayDebt(ctx, k, msg) return handleMsgRepayDebt(ctx, k, msg)
case MsgLiquidate:
return handleMsgLiquidate(ctx, k, msg)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
} }
@ -110,3 +112,19 @@ func handleMsgRepayDebt(ctx sdk.Context, k Keeper, msg MsgRepayDebt) (*sdk.Resul
) )
return &sdk.Result{Events: ctx.EventManager().Events()}, nil 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/app"
"github.com/kava-labs/kava/x/cdp" "github.com/kava-labs/kava/x/cdp"
"github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/pricefeed" "github.com/kava-labs/kava/x/pricefeed"
) )
@ -40,25 +41,26 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.Params{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot, SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold, DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot, DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: asset, Denom: asset,
Type: asset + "-a", Type: asset + "-a",
LiquidationRatio: liquidationRatio, LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(1000000000), AuctionSize: i(1000000000),
Prefix: 0x20, Prefix: 0x20,
ConversionFactor: i(6), ConversionFactor: i(6),
SpotMarketID: asset + ":usd", SpotMarketID: asset + ":usd",
LiquidationMarketID: asset + ":usd", LiquidationMarketID: asset + ":usd",
KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
}, },
}, },
DebtParam: cdp.DebtParam{ DebtParam: cdp.DebtParam{
@ -66,14 +68,18 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}, },
}, },
StartingCdpID: cdp.DefaultCdpStartingID, StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom, DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom, GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{}, 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)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
@ -106,38 +112,41 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.Params{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot, SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold, DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot, DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"), LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000), AuctionSize: i(7000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: i(6), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
}, },
{ {
Denom: "btc", Denom: "btc",
Type: "btc-a", Type: "btc-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"), LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000), AuctionSize: i(10000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "btc:usd", SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd", LiquidationMarketID: "btc:usd",
ConversionFactor: i(8), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
}, },
}, },
DebtParam: cdp.DebtParam{ DebtParam: cdp.DebtParam{
@ -145,24 +154,30 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}, },
}, },
StartingCdpID: cdp.DefaultCdpStartingID, StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom, DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom, GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{}, 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)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
func cdps() (cdps cdp.CDPs) { func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3) _, 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())) 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())) 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())) 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())) 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) cdps = append(cdps, c1, c2, c3, c4)
return return
} }

View File

@ -11,9 +11,6 @@ import (
"github.com/kava-labs/kava/x/cdp/types" "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 // 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 { func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coin, principal sdk.Coin, collateralType string) error {
// validation // validation
@ -21,6 +18,10 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
if err != nil { if err != nil {
return err return err
} }
err = k.ValidateBalance(ctx, collateral, owner)
if err != nil {
return err
}
_, found := k.GetCdpByOwnerAndCollateralType(ctx, owner, collateralType) _, found := k.GetCdpByOwnerAndCollateralType(ctx, owner, collateralType)
if found { if found {
return sdkerrors.Wrapf(types.ErrCdpAlreadyExists, "owner %s, denom %s", owner, collateral.Denom) 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 // send coins from the owners account to the cdp module
id := k.GetNextCdpID(ctx) 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) deposit := types.NewDeposit(cdp.ID, owner, collateral)
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(collateral)) err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, owner, types.ModuleName, sdk.NewCoins(collateral))
if err != nil { 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.SetDeposit(ctx, deposit)
k.SetNextCdpID(ctx, id+1) k.SetNextCdpID(ctx, id+1)
k.hooks.AfterCDPCreated(ctx, cdp)
// emit events for cdp creation, deposit, and draw // emit events for cdp creation, deposit, and draw
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
@ -102,6 +111,31 @@ func (k Keeper) AddCdp(ctx sdk.Context, owner sdk.AccAddress, collateral sdk.Coi
return nil 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 // 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 { func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ratio sdk.Dec) error {
err := k.SetCDP(ctx, cdp) err := k.SetCDP(ctx, cdp)
@ -112,6 +146,16 @@ func (k Keeper) SetCdpAndCollateralRatioIndex(ctx sdk.Context, cdp types.CDP, ra
return nil 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 // MintDebtCoins mints debt coins in the cdp module account
func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coin) error { func (k Keeper) MintDebtCoins(ctx sdk.Context, moduleAccount string, denom string, principalCoins sdk.Coin) error {
debtCoins := sdk.NewCoins(sdk.NewCoin(denom, principalCoins.Amount)) 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 // BurnDebtCoins burns debt coins from the cdp module account
func (k Keeper) BurnDebtCoins(ctx sdk.Context, moduleAccount string, denom string, paymentCoins sdk.Coin) error { 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) return k.supplyKeeper.BurnCoins(ctx, moduleAccount, debtCoins)
} }
@ -419,6 +466,20 @@ func (k Keeper) ValidateCollateralizationRatio(ctx sdk.Context, collateral sdk.C
return nil 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 // 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 { func (k Keeper) CalculateCollateralToDebtRatio(ctx sdk.Context, collateral sdk.Coin, collateralType string, debt sdk.Coin) sdk.Dec {
debtTotal := k.convertDebtToBaseUnits(ctx, debt) 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 // LoadAugmentedCDP creates a new augmented CDP from an existing CDP
func (k Keeper) LoadAugmentedCDP(ctx sdk.Context, cdp types.CDP) types.AugmentedCDP { 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 // calculate collateralization ratio
collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Type, cdp.Principal, cdp.AccumulatedFees, liquidation) collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral, cdp.Type, cdp.Principal, cdp.AccumulatedFees, liquidation)
if err != nil { if err != nil {

View File

@ -131,7 +131,7 @@ func (suite *CdpTestSuite) TestGetNextCdpID() {
func (suite *CdpTestSuite) TestGetSetCdp() { func (suite *CdpTestSuite) TestGetSetCdp() {
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, 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) err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err) suite.NoError(err)
@ -147,7 +147,7 @@ func (suite *CdpTestSuite) TestGetSetCdp() {
func (suite *CdpTestSuite) TestGetSetCdpId() { func (suite *CdpTestSuite) TestGetSetCdpId() {
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, 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) err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err) suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp) suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
@ -162,7 +162,7 @@ func (suite *CdpTestSuite) TestGetSetCdpId() {
func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndCollateralType() { func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndCollateralType() {
_, addrs := app.GeneratePrivKeyAddressPairs(2) _, 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) err := suite.keeper.SetCDP(suite.ctx, cdp)
suite.NoError(err) suite.NoError(err)
suite.keeper.IndexCdpByOwner(suite.ctx, cdp) suite.keeper.IndexCdpByOwner(suite.ctx, cdp)
@ -178,17 +178,17 @@ func (suite *CdpTestSuite) TestGetSetCdpByOwnerAndCollateralType() {
func (suite *CdpTestSuite) TestCalculateCollateralToDebtRatio() { func (suite *CdpTestSuite) TestCalculateCollateralToDebtRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, 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) cr := suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("3.0"), cr) 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) cr = suite.keeper.CalculateCollateralToDebtRatio(suite.ctx, cdp.Collateral, cdp.Type, cdp.Principal)
suite.Equal(sdk.MustNewDecFromStr("0.5"), cr) suite.Equal(sdk.MustNewDecFromStr("0.5"), cr)
} }
func (suite *CdpTestSuite) TestSetCdpByCollateralRatio() { func (suite *CdpTestSuite) TestSetCdpByCollateralRatio() {
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, 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) 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) }) 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 { if !found {
return sdkerrors.Wrapf(types.ErrCdpNotFound, "owner %s, collateral %s", owner, collateralType) 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) deposit, found := k.GetDeposit(ctx, cdp.ID, depositor)
if found { if found {
@ -32,6 +38,12 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner, depositor sdk.AccAddre
if err != nil { if err != nil {
return err 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( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeCdpDeposit, types.EventTypeCdpDeposit,
@ -40,14 +52,7 @@ func (k Keeper) DepositCollateral(ctx sdk.Context, owner, depositor sdk.AccAddre
), ),
) )
k.SetDeposit(ctx, deposit) return k.UpdateCdpAndCollateralRatioIndex(ctx, cdp, collateralToDebtRatio)
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)
} }
// WithdrawCollateral removes collateral from a cdp if it does not put the cdp below the liquidation ratio // 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) { if collateral.Amount.GT(deposit.Amount.Amount) {
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "collateral %s, deposit %s", collateral, deposit.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) collateralizationRatio, err := k.CalculateCollateralizationRatio(ctx, cdp.Collateral.Sub(collateral), cdp.Type, cdp.Principal, cdp.AccumulatedFees, spot)
if err != nil { if err != nil {
@ -76,24 +83,15 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner, depositor sdk.AccAddr
if collateralizationRatio.LT(liquidationRatio) { if collateralizationRatio.LT(liquidationRatio) {
return sdkerrors.Wrapf(types.ErrInvalidCollateralRatio, "collateral %s, collateral ratio %s, liquidation ration %s", collateral.Denom, collateralizationRatio, 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)) err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, depositor, sdk.NewCoins(collateral))
if err != nil { if err != nil {
panic(err) 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) cdp.Collateral = cdp.Collateral.Sub(collateral)
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) 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 { if err != nil {
return err return err
} }
@ -105,6 +103,15 @@ func (k Keeper) WithdrawCollateral(ctx sdk.Context, owner, depositor sdk.AccAddr
} else { } else {
k.SetDeposit(ctx, deposit) 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 return nil
} }

View File

@ -25,6 +25,8 @@ func (k Keeper) AddPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateralTy
if err != nil { if err != nil {
return err 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) err = k.ValidateCollateralizationRatio(ctx, cdp.Collateral, cdp.Type, cdp.Principal.Add(principal), cdp.AccumulatedFees)
if err != nil { 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 // update cdp state
cdp.Principal = cdp.Principal.Add(principal) 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 // set cdp state and indexes in the store
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) 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 // RepayPrincipal removes debt from the cdp
@ -85,6 +83,13 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
return err 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 // Note: assumes cdp.Principal and cdp.AccumulatedFees don't change during calculations
totalPrincipal := cdp.GetTotalPrincipal() 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 // 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 // update cdp state
if !principalPayment.IsZero() { 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 // and remove the cdp and indexes from the store
if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() { if cdp.Principal.IsZero() && cdp.AccumulatedFees.IsZero() {
k.ReturnCollateral(ctx, cdp) 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 return err
} }
k.RemoveCdpOwnerIndex(ctx, cdp)
// emit cdp close event // emit cdp close event
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
@ -168,7 +171,7 @@ func (k Keeper) RepayPrincipal(ctx sdk.Context, owner sdk.AccAddress, collateral
// set cdp state and update indexes // set cdp state and update indexes
collateralToDebtRatio := k.CalculateCollateralToDebtRatio(ctx, cdp.Collateral, cdp.Type, cdp.GetTotalPrincipal()) 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 // ValidatePaymentCoins validates that the input coins are valid for repaying debt

View File

@ -129,37 +129,6 @@ func (suite *DrawTestSuite) TestRepayPrincipalOverpay() {
suite.False(found) 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() { func (suite *DrawTestSuite) TestPricefeedFailure() {
ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2)) ctx := suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 2))
pfk := suite.app.GetPriceFeedKeeper() 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

@ -40,25 +40,26 @@ func NewPricefeedGenState(asset string, price sdk.Dec) app.GenesisState {
func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState { func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.Params{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot, SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold, DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot, DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: asset, Denom: asset,
Type: asset + "-a", Type: asset + "-a",
LiquidationRatio: liquidationRatio, LiquidationRatio: liquidationRatio,
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(100), AuctionSize: i(100),
Prefix: 0x20, Prefix: 0x20,
ConversionFactor: i(6), SpotMarketID: asset + ":usd",
SpotMarketID: asset + ":usd", LiquidationMarketID: asset + ":usd",
LiquidationMarketID: asset + ":usd", KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
}, },
}, },
DebtParam: cdp.DebtParam{ DebtParam: cdp.DebtParam{
@ -66,14 +67,18 @@ func NewCDPGenState(asset string, liquidationRatio sdk.Dec) app.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.9"),
}, },
}, },
StartingCdpID: cdp.DefaultCdpStartingID, StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom, DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom, GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{}, 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)} 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: "btc:usd", BaseAsset: "btc", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "xrp:usd", BaseAsset: "xrp", 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: "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{ PostedPrices: []pricefeed.PostedPrice{
@ -106,6 +112,12 @@ func NewPricefeedGenStateMulti() app.GenesisState {
Price: sdk.MustNewDecFromStr("17.25"), Price: sdk.MustNewDecFromStr("17.25"),
Expiry: time.Now().Add(1 * time.Hour), 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)} return app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pfGenesis)}
@ -113,51 +125,71 @@ func NewPricefeedGenStateMulti() app.GenesisState {
func NewCDPGenStateMulti() app.GenesisState { func NewCDPGenStateMulti() app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.Params{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 1500000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot, SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold, DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot, DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"), LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000), AuctionSize: i(7000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: i(6), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
}, },
{ {
Denom: "btc", Denom: "btc",
Type: "btc-a", Type: "btc-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"), LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000), AuctionSize: i(10000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "btc:usd", SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd", LiquidationMarketID: "btc:usd",
ConversionFactor: i(8), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
}, },
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 500000000000), DebtLimit: sdk.NewInt64Coin("usdx", 500000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(50000000000), AuctionSize: i(50000000000),
Prefix: 0x22, Prefix: 0x22,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: i(8), 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),
}, },
}, },
DebtParam: cdp.DebtParam{ DebtParam: cdp.DebtParam{
@ -165,14 +197,24 @@ func NewCDPGenStateMulti() app.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}, },
}, },
StartingCdpID: cdp.DefaultCdpStartingID, StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom, DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom, GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{}, 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)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
@ -180,38 +222,41 @@ func NewCDPGenStateMulti() app.GenesisState {
func NewCDPGenStateHighDebtLimit() app.GenesisState { func NewCDPGenStateHighDebtLimit() app.GenesisState {
cdpGenesis := cdp.GenesisState{ cdpGenesis := cdp.GenesisState{
Params: cdp.Params{ Params: cdp.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: cdp.DefaultSurplusThreshold, SurplusAuctionThreshold: cdp.DefaultSurplusThreshold,
SurplusAuctionLot: cdp.DefaultSurplusLot, SurplusAuctionLot: cdp.DefaultSurplusLot,
DebtAuctionThreshold: cdp.DefaultDebtThreshold, DebtAuctionThreshold: cdp.DefaultDebtThreshold,
DebtAuctionLot: cdp.DefaultDebtLot, DebtAuctionLot: cdp.DefaultDebtLot,
SavingsDistributionFrequency: cdp.DefaultSavingsDistributionFrequency,
CollateralParams: cdp.CollateralParams{ CollateralParams: cdp.CollateralParams{
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("2.0"), LiquidationRatio: sdk.MustNewDecFromStr("2.0"),
DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), // %5 apr
LiquidationPenalty: d("0.05"), LiquidationPenalty: d("0.05"),
AuctionSize: i(7000000000), AuctionSize: i(7000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: i(6), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(6),
}, },
{ {
Denom: "btc", Denom: "btc",
Type: "btc-a", Type: "btc-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 50000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr StabilityFee: sdk.MustNewDecFromStr("1.000000000782997609"), // %2.5 apr
LiquidationPenalty: d("0.025"), LiquidationPenalty: d("0.025"),
AuctionSize: i(10000000), AuctionSize: i(10000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "btc:usd", SpotMarketID: "btc:usd",
LiquidationMarketID: "btc:usd", LiquidationMarketID: "btc:usd",
ConversionFactor: i(8), KeeperRewardPercentage: d("0.01"),
CheckCollateralizationIndexCount: i(10),
ConversionFactor: i(8),
}, },
}, },
DebtParam: cdp.DebtParam{ DebtParam: cdp.DebtParam{
@ -219,24 +264,30 @@ func NewCDPGenStateHighDebtLimit() app.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.95"),
}, },
}, },
StartingCdpID: cdp.DefaultCdpStartingID, StartingCdpID: cdp.DefaultCdpStartingID,
DebtDenom: cdp.DefaultDebtDenom, DebtDenom: cdp.DefaultDebtDenom,
GovDenom: cdp.DefaultGovDenom, GovDenom: cdp.DefaultGovDenom,
CDPs: cdp.CDPs{}, 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)} return app.GenesisState{cdp.ModuleName: cdp.ModuleCdc.MustMarshalJSON(cdpGenesis)}
} }
func cdps() (cdps cdp.CDPs) { func cdps() (cdps cdp.CDPs) {
_, addrs := app.GeneratePrivKeyAddressPairs(3) _, 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())) 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())) 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())) 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())) 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) cdps = append(cdps, c1, c2, c3, c4)
return 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 ( import (
"fmt" "fmt"
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/store/prefix" "github.com/cosmos/cosmos-sdk/store/prefix"
@ -20,6 +21,7 @@ type Keeper struct {
supplyKeeper types.SupplyKeeper supplyKeeper types.SupplyKeeper
auctionKeeper types.AuctionKeeper auctionKeeper types.AuctionKeeper
accountKeeper types.AccountKeeper accountKeeper types.AccountKeeper
hooks types.CDPHooks
maccPerms map[string][]string maccPerms map[string][]string
} }
@ -38,10 +40,20 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramstore subspace.Subspace,
auctionKeeper: ak, auctionKeeper: ak,
supplyKeeper: sk, supplyKeeper: sk,
accountKeeper: ack, accountKeeper: ack,
hooks: nil,
maccPerms: maccs, 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 // CdpDenomIndexIterator returns an sdk.Iterator for all cdps with matching collateral denom
func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, collateralType string) sdk.Iterator { func (k Keeper) CdpDenomIndexIterator(ctx sdk.Context, collateralType string) sdk.Iterator {
store := prefix.NewStore(ctx.KVStore(k.key), types.CdpKeyPrefix) 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 // GetSliceOfCDPsByRatioAndType returns a slice of cdps of size equal to the input cutoffCount
func (k Keeper) SetSavingsRateDistributed(ctx sdk.Context, totalDistributed sdk.Int) { // sorted by target ratio in ascending order (ie, the lowest collateral:debt ratio cdps are returned first)
store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey) func (k Keeper) GetSliceOfCDPsByRatioAndType(ctx sdk.Context, cutoffCount sdk.Int, targetRatio sdk.Dec, collateralType string) (cdps types.CDPs) {
bz := k.cdc.MustMarshalBinaryLengthPrefixed(totalDistributed) count := sdk.ZeroInt()
store.Set([]byte{}, bz) 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 // GetPreviousAccrualTime returns the last time an individual market accrued interest
func (k Keeper) GetSavingsRateDistributed(ctx sdk.Context) sdk.Int { func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, ctype string) (time.Time, bool) {
savingsRateDistributed := sdk.ZeroInt() store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousAccrualTimePrefix)
store := prefix.NewStore(ctx.KVStore(k.key), types.SavingsRateDistributedKey) bz := store.Get([]byte(ctype))
bz := store.Get([]byte{})
if bz == nil { if bz == nil {
return savingsRateDistributed return time.Time{}, false
} }
var previousAccrualTime time.Time
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &savingsRateDistributed) k.cdc.MustUnmarshalBinaryBare(bz, &previousAccrualTime)
return savingsRateDistributed return previousAccrualTime, true
}
// 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.ctx = ctx
suite.keeper = keeper 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 package keeper
import ( import (
"fmt"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
"github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client"
@ -32,10 +30,6 @@ func NewQuerier(keeper Keeper) sdk.Querier {
return queryGetParams(ctx, req, keeper) return queryGetParams(ctx, req, keeper)
case types.QueryGetAccounts: case types.QueryGetAccounts:
return queryGetAccounts(ctx, req, keeper) return queryGetAccounts(ctx, req, keeper)
case types.QueryGetSavingsRateDistributed:
return queryGetSavingsRateDistributed(ctx, req, keeper)
case types.QueryGetPreviousSavingsDistributionTime:
return queryGetPreviousSavingsDistributionTime(ctx, req, keeper)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint %s", types.ModuleName, path[0]) 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) { func queryGetAccounts(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
cdpAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.ModuleName) cdpAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.ModuleName)
liquidatorAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc) liquidatorAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.LiquidatorMacc)
savingsRateAccAccount := keeper.supplyKeeper.GetModuleAccount(ctx, types.SavingsRateMacc)
accounts := []supply.ModuleAccount{ accounts := []supply.ModuleAccount{
*cdpAccAccount.(*supply.ModuleAccount), *cdpAccAccount.(*supply.ModuleAccount),
*liquidatorAccAccount.(*supply.ModuleAccount), *liquidatorAccAccount.(*supply.ModuleAccount),
*savingsRateAccAccount.(*supply.ModuleAccount),
} }
// Encode results // Encode results
@ -188,36 +180,6 @@ func queryGetAccounts(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]
return bz, nil 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 // query cdps in store and filter by request params
func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) { func queryGetCdps(ctx sdk.Context, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
var params types.QueryCdpsParams var params types.QueryCdpsParams

View File

@ -287,7 +287,7 @@ func (suite *QuerierTestSuite) TestQueryAccounts() {
var accounts []supply.ModuleAccount var accounts []supply.ModuleAccount
suite.Require().Nil(supply.ModuleCdc.UnmarshalJSON(bz, &accounts)) 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 { findByName := func(name string) bool {
for _, account := range accounts { for _, account := range accounts {
@ -300,18 +300,6 @@ func (suite *QuerierTestSuite) TestQueryAccounts() {
suite.Require().True(findByName("cdp")) suite.Require().True(findByName("cdp"))
suite.Require().True(findByName("liquidator")) 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() { 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" "fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/kava-labs/kava/x/cdp/types" "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. // SeizeCollateral liquidates the collateral in the input cdp.
// the following operations are performed: // the following operations are performed:
// 1. Collateral for all deposits is sent from the cdp module to the liquidator module account // 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) normalizedRatio := sdk.OneDec().Quo(priceDivLiqRatio)
cdpsToLiquidate := k.GetAllCdpsByCollateralTypeAndRatio(ctx, collateralType, normalizedRatio) cdpsToLiquidate := k.GetAllCdpsByCollateralTypeAndRatio(ctx, collateralType, normalizedRatio)
for _, c := range cdpsToLiquidate { for _, c := range cdpsToLiquidate {
k.hooks.BeforeCDPModified(ctx, c)
err := k.SeizeCollateral(ctx, c) err := k.SeizeCollateral(ctx, c)
if err != nil { if err != nil {
return err return err
@ -93,7 +117,53 @@ func (k Keeper) ApplyLiquidationPenalty(ctx sdk.Context, collateralType string,
return sdk.NewDecFromInt(debt).Mul(penalty).RoundInt() 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 { func (k Keeper) getModAccountDebt(ctx sdk.Context, accountName string) sdk.Int {
macc := k.supplyKeeper.GetModuleAccount(ctx, accountName) macc := k.supplyKeeper.GetModuleAccount(ctx, accountName)
return macc.GetCoins().AmountOf(k.GetDebtDenom(ctx)) 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 ( import (
"errors" "errors"
"math/rand" "math/rand"
"strings"
"testing" "testing"
"time" "time"
@ -205,6 +206,258 @@ func (suite *SeizeTestSuite) TestApplyLiquidationPenalty() {
suite.Panics(func() { suite.keeper.ApplyLiquidationPenalty(suite.ctx, "lol-a", i(1000)) }) 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) { func TestSeizeTestSuite(t *testing.T) {
suite.Run(t, new(SeizeTestSuite)) suite.Run(t, new(SeizeTestSuite))
} }

View File

@ -8,12 +8,68 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" 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 ( var (
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
minCollateralPrefix = 0 KeyCollateralParams = []byte("CollateralParams")
maxCollateralPrefix = 255 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. // 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 { func (cdp CDP) Validate() error {
if cdp.ID == 0 { if cdp.ID == 0 {
return errors.New("cdp id cannot be 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" "time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
tmtime "github.com/tendermint/tendermint/types/time"
) )
const ( const (
ModuleName = "cdp" 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. // CDP is the state of a single collateralized debt position.
type CDP struct { type CDP struct {
ID uint64 `json:"id" yaml:"id"` // unique id for cdp ID uint64 `json:"id" yaml:"id"` // unique id for cdp

View File

@ -4,7 +4,6 @@ import (
"bytes" "bytes"
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"time"
"github.com/tendermint/tendermint/libs/kv" "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) cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &totalB)
return fmt.Sprintf("%s\n%s", totalA, 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: default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1])) 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} deposit := types.Deposit{CdpID: 1, Amount: oneCoins}
principal := sdk.OneInt() principal := sdk.OneInt()
prevDistTime := time.Now().UTC() 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{ kvPairs := kv.Pairs{
kv.Pair{Key: types.CdpIDKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(cdpIds)}, 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.GovDenomKey), Value: cdc.MustMarshalBinaryLengthPrefixed(denom)},
kv.Pair{Key: []byte(types.DepositKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(deposit)}, 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.PrincipalKeyPrefix), Value: cdc.MustMarshalBinaryLengthPrefixed(principal)},
kv.Pair{Key: []byte(types.PreviousDistributionTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevDistTime)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, 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)}, {"GovDenom", fmt.Sprintf("%s\n%s", denom, denom)},
{"DepositKeyPrefix", fmt.Sprintf("%v\n%v", deposit, deposit)}, {"DepositKeyPrefix", fmt.Sprintf("%v\n%v", deposit, deposit)},
{"Principal", fmt.Sprintf("%v\n%v", principal, principal)}, {"Principal", fmt.Sprintf("%v\n%v", principal, principal)},
{"PreviousDistributionTime", fmt.Sprintf("%s\n%s", prevDistTime, prevDistTime)},
{"other", ""}, {"other", ""},
} }
for i, tt := range tests { for i, tt := range tests {

View File

@ -70,12 +70,11 @@ func randomCdpGenState(selection int) types.GenesisState {
case 0: case 0:
return types.GenesisState{ return types.GenesisState{
Params: types.Params{ Params: types.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: types.DefaultSurplusThreshold, SurplusAuctionThreshold: types.DefaultSurplusThreshold,
SurplusAuctionLot: types.DefaultSurplusLot, SurplusAuctionLot: types.DefaultSurplusLot,
DebtAuctionLot: types.DefaultDebtLot, DebtAuctionLot: types.DefaultDebtLot,
DebtAuctionThreshold: types.DefaultDebtThreshold, DebtAuctionThreshold: types.DefaultDebtThreshold,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
CollateralParams: types.CollateralParams{ CollateralParams: types.CollateralParams{
{ {
Denom: "xrp", Denom: "xrp",
@ -122,24 +121,21 @@ func randomCdpGenState(selection int) types.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
}, },
StartingCdpID: types.DefaultCdpStartingID, StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom, DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom, GovDenom: types.DefaultGovDenom,
CDPs: types.CDPs{}, CDPs: types.CDPs{},
PreviousDistributionTime: types.DefaultPreviousDistributionTime,
} }
case 1: case 1:
return types.GenesisState{ return types.GenesisState{
Params: types.Params{ Params: types.Params{
GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000), GlobalDebtLimit: sdk.NewInt64Coin("usdx", 100000000000000),
SurplusAuctionThreshold: types.DefaultSurplusThreshold, SurplusAuctionThreshold: types.DefaultSurplusThreshold,
DebtAuctionThreshold: types.DefaultDebtThreshold, DebtAuctionThreshold: types.DefaultDebtThreshold,
SurplusAuctionLot: types.DefaultSurplusLot, SurplusAuctionLot: types.DefaultSurplusLot,
DebtAuctionLot: types.DefaultDebtLot, DebtAuctionLot: types.DefaultDebtLot,
SavingsDistributionFrequency: types.DefaultSavingsDistributionFrequency,
CollateralParams: types.CollateralParams{ CollateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
@ -160,14 +156,12 @@ func randomCdpGenState(selection int) types.GenesisState {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
}, },
StartingCdpID: types.DefaultCdpStartingID, StartingCdpID: types.DefaultCdpStartingID,
DebtDenom: types.DefaultDebtDenom, DebtDenom: types.DefaultDebtDenom,
GovDenom: types.DefaultGovDenom, GovDenom: types.DefaultGovDenom,
CDPs: types.CDPs{}, CDPs: types.CDPs{},
PreviousDistributionTime: types.DefaultPreviousDistributionTime,
} }
default: default:
panic("invalid genesis state selector") 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) { if shouldDraw(r) {
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.Amount), randCollateralParam.ConversionFactor.Neg()) collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.Amount), randCollateralParam.ConversionFactor.Neg())
collateralValue := collateralShifted.Mul(priceShifted) collateralValue := collateralShifted.Mul(priceShifted)
newFeesAccumulated := k.CalculateFees(ctx, existingCDP.Principal, sdk.NewInt(ctx.BlockTime().Unix()-existingCDP.FeesUpdated.Unix()), randCollateralParam.Type).Amount newFeesAccumulated := k.CalculateNewInterest(ctx, existingCDP)
totalFees := existingCDP.AccumulatedFees.Amount.Add(newFeesAccumulated) totalFees := existingCDP.AccumulatedFees.Add(newFeesAccumulated)
// given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio // 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) 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()) { if maxDebt.LTE(sdk.OneInt()) {
// debt in cdp is maxed out // 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 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 { type CDP struct {
ID uint64 ID uint64
Owner sdk.AccAddress Owner sdk.AccAddress
Type string
Collateral sdk.Coin Collateral sdk.Coin
Principal sdk.Coin Principal sdk.Coin
AccumulatedFees sdk.Coin AccumulatedFees sdk.Coin
FeesUpdated time.Time 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 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 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 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 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"` // Amount of stable coin drawn from this CDP 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 // 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()) fees := sdk.NewCoin(principal.Denom, sdk.ZeroInt())
return CDP{ return CDP{
ID: id, ID: id,
@ -32,11 +33,12 @@ func NewCDP(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collateralType
Principal: principal, Principal: principal,
AccumulatedFees: fees, AccumulatedFees: fees,
FeesUpdated: time, FeesUpdated: time,
InterestFactor: interestFactor,
} }
} }
// NewCDPWithFees creates a new CDP object, for use during migration // 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{ return CDP{
ID: id, ID: id,
Owner: owner, Owner: owner,
@ -45,6 +47,7 @@ func NewCDPWithFees(id uint64, owner sdk.AccAddress, collateral sdk.Coin, collat
Principal: principal, Principal: principal,
AccumulatedFees: fees, AccumulatedFees: fees,
FeesUpdated: time, FeesUpdated: time,
InterestFactor: interestFactor,
} }
} }
@ -57,7 +60,8 @@ func (cdp CDP) String() string {
Collateral: %s Collateral: %s
Principal: %s Principal: %s
AccumulatedFees: %s AccumulatedFees: %s
Fees Last Updated: %s`, Fees Last Updated: %s
Interest Factor: %s`,
cdp.Owner, cdp.Owner,
cdp.ID, cdp.ID,
cdp.Type, cdp.Type,
@ -65,6 +69,7 @@ func (cdp CDP) String() string {
cdp.Principal, cdp.Principal,
cdp.AccumulatedFees, cdp.AccumulatedFees,
cdp.FeesUpdated, cdp.FeesUpdated,
cdp.InterestFactor,
)) ))
} }
@ -139,6 +144,7 @@ func NewAugmentedCDP(cdp CDP, collateralValue sdk.Coin, collateralizationRatio s
Principal: cdp.Principal, Principal: cdp.Principal,
AccumulatedFees: cdp.AccumulatedFees, AccumulatedFees: cdp.AccumulatedFees,
FeesUpdated: cdp.FeesUpdated, FeesUpdated: cdp.FeesUpdated,
InterestFactor: cdp.InterestFactor,
}, },
CollateralValue: collateralValue, CollateralValue: collateralValue,
CollateralizationRatio: collateralizationRatio, CollateralizationRatio: collateralizationRatio,
@ -157,6 +163,7 @@ func (augCDP AugmentedCDP) String() string {
Principal: %s Principal: %s
Fees: %s Fees: %s
Fees Last Updated: %s Fees Last Updated: %s
Interest Factor: %s
Collateralization ratio: %s`, Collateralization ratio: %s`,
augCDP.Owner, augCDP.Owner,
augCDP.ID, augCDP.ID,
@ -166,6 +173,7 @@ func (augCDP AugmentedCDP) String() string {
augCDP.Principal, augCDP.Principal,
augCDP.AccumulatedFees, augCDP.AccumulatedFees,
augCDP.FeesUpdated, augCDP.FeesUpdated,
augCDP.InterestFactor,
augCDP.CollateralizationRatio, augCDP.CollateralizationRatio,
)) ))
} }

View File

@ -42,7 +42,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}{ }{
{ {
name: "valid cdp", 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{ errArgs: errArgs{
expectPass: true, expectPass: true,
contains: "", contains: "",
@ -50,7 +50,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid cdp id", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "cdp id cannot be 0", contains: "cdp id cannot be 0",
@ -58,7 +58,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid collateral", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "invalid coins: collateral", contains: "invalid coins: collateral",
@ -66,7 +66,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid prinicpal", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "invalid coins: principal", contains: "invalid coins: principal",
@ -74,7 +74,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid fees", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "invalid coins: accumulated fees", contains: "invalid coins: accumulated fees",
@ -82,7 +82,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid fees updated", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "cdp updated fee time cannot be zero", contains: "cdp updated fee time cannot be zero",
@ -90,7 +90,7 @@ func (suite *CdpValidationSuite) TestCdpValidation() {
}, },
{ {
name: "invalid type", 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{ errArgs: errArgs{
expectPass: false, expectPass: false,
contains: "cdp type cannot be empty", contains: "cdp type cannot be empty",

View File

@ -45,4 +45,10 @@ var (
ErrPricefeedDown = sdkerrors.Register(ModuleName, 19, "no price found for collateral") 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 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") 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)) IterateAccounts(ctx sdk.Context, cb func(account authexported.Account) (stop bool))
GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account 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

@ -10,28 +10,29 @@ import (
// GenesisState is the state that must be provided at genesis. // GenesisState is the state that must be provided at genesis.
type GenesisState struct { type GenesisState struct {
Params Params `json:"params" yaml:"params"` Params Params `json:"params" yaml:"params"`
CDPs CDPs `json:"cdps" yaml:"cdps"` CDPs CDPs `json:"cdps" yaml:"cdps"`
Deposits Deposits `json:"deposits" yaml:"deposits"` Deposits Deposits `json:"deposits" yaml:"deposits"`
StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"` StartingCdpID uint64 `json:"starting_cdp_id" yaml:"starting_cdp_id"`
DebtDenom string `json:"debt_denom" yaml:"debt_denom"` DebtDenom string `json:"debt_denom" yaml:"debt_denom"`
GovDenom string `json:"gov_denom" yaml:"gov_denom"` GovDenom string `json:"gov_denom" yaml:"gov_denom"`
PreviousDistributionTime time.Time `json:"previous_distribution_time" yaml:"previous_distribution_time"` PreviousAccumulationTimes GenesisAccumulationTimes `json:"previous_accumulation_times" yaml:"previous_accumulation_times"`
SavingsRateDistributed sdk.Int `json:"savings_rate_distributed" yaml:"savings_rate_distributed"` TotalPrincipals GenesisTotalPrincipals `json:"total_principals" yaml:"total_principals"`
} }
// NewGenesisState returns a new genesis state // NewGenesisState returns a new genesis state
func NewGenesisState(params Params, cdps CDPs, deposits Deposits, startingCdpID uint64, 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{ return GenesisState{
Params: params, Params: params,
CDPs: cdps, CDPs: cdps,
Deposits: deposits, Deposits: deposits,
StartingCdpID: startingCdpID, StartingCdpID: startingCdpID,
DebtDenom: debtDenom, DebtDenom: debtDenom,
GovDenom: govDenom, GovDenom: govDenom,
PreviousDistributionTime: previousDistTime, PreviousAccumulationTimes: prevAccumTimes,
SavingsRateDistributed: savingsRateDist, TotalPrincipals: totalPrincipals,
} }
} }
@ -44,8 +45,8 @@ func DefaultGenesisState() GenesisState {
DefaultCdpStartingID, DefaultCdpStartingID,
DefaultDebtDenom, DefaultDebtDenom,
DefaultGovDenom, DefaultGovDenom,
DefaultPreviousDistributionTime, GenesisAccumulationTimes{},
DefaultSavingsRateDistributed, GenesisTotalPrincipals{},
) )
} }
@ -65,11 +66,11 @@ func (gs GenesisState) Validate() error {
return err return err
} }
if gs.PreviousDistributionTime.IsZero() { if err := gs.PreviousAccumulationTimes.Validate(); err != nil {
return fmt.Errorf("previous distribution time not set") return err
} }
if err := validateSavingsRateDistributed(gs.SavingsRateDistributed); err != nil { if err := gs.TotalPrincipals.Validate(); err != nil {
return err return err
} }
@ -108,3 +109,75 @@ func (gs GenesisState) Equal(gs2 GenesisState) bool {
func (gs GenesisState) IsEmpty() bool { func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{}) 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 module account for liquidator
LiquidatorMacc = "liquidator" LiquidatorMacc = "liquidator"
// SavingsRateMacc module account for savings rate
SavingsRateMacc = "savings"
) )
var sep = []byte(":") var sep = []byte(":")
@ -51,17 +48,17 @@ var sep = []byte(":")
// KVStore key prefixes // KVStore key prefixes
var ( var (
CdpIDKeyPrefix = []byte{0x00} CdpIDKeyPrefix = []byte{0x01}
CdpKeyPrefix = []byte{0x01} CdpKeyPrefix = []byte{0x02}
CollateralRatioIndexPrefix = []byte{0x02} CollateralRatioIndexPrefix = []byte{0x03}
CdpIDKey = []byte{0x03} CdpIDKey = []byte{0x04}
DebtDenomKey = []byte{0x04} DebtDenomKey = []byte{0x05}
GovDenomKey = []byte{0x05} GovDenomKey = []byte{0x06}
DepositKeyPrefix = []byte{0x06} DepositKeyPrefix = []byte{0x07}
PrincipalKeyPrefix = []byte{0x07} PrincipalKeyPrefix = []byte{0x08}
PreviousDistributionTimeKey = []byte{0x08} PricefeedStatusKeyPrefix = []byte{0x10}
PricefeedStatusKeyPrefix = []byte{0x09} PreviousAccrualTimePrefix = []byte{0x12}
SavingsRateDistributedKey = []byte{0x10} InterestFactorPrefix = []byte{0x13}
) )
// GetCdpIDBytes returns the byte representation of the cdpID // GetCdpIDBytes returns the byte representation of the cdpID

View File

@ -314,3 +314,59 @@ func (msg MsgRepayDebt) String() string {
Payment: %s Payment: %s
`, msg.Sender, msg.CollateralType, msg.Payment) `, 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,64 +3,55 @@ package types
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params"
tmtime "github.com/tendermint/tendermint/types/time"
) )
// Parameter keys // Parameter keys
var ( var (
KeyGlobalDebtLimit = []byte("GlobalDebtLimit") KeyGlobalDebtLimit = []byte("GlobalDebtLimit")
KeyCollateralParams = []byte("CollateralParams") KeyCollateralParams = []byte("CollateralParams")
KeyDebtParam = []byte("DebtParam") KeyDebtParam = []byte("DebtParam")
KeyDistributionFrequency = []byte("DistributionFrequency") KeyCircuitBreaker = []byte("CircuitBreaker")
KeyCircuitBreaker = []byte("CircuitBreaker") KeyDebtThreshold = []byte("DebtThreshold")
KeyDebtThreshold = []byte("DebtThreshold") KeyDebtLot = []byte("DebtLot")
KeyDebtLot = []byte("DebtLot") KeySurplusThreshold = []byte("SurplusThreshold")
KeySurplusThreshold = []byte("SurplusThreshold") KeySurplusLot = []byte("SurplusLot")
KeySurplusLot = []byte("SurplusLot") DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt())
KeySavingsRateDistributed = []byte("SavingsRateDistributed") DefaultCircuitBreaker = false
DefaultGlobalDebt = sdk.NewCoin(DefaultStableDenom, sdk.ZeroInt()) DefaultCollateralParams = CollateralParams{}
DefaultCircuitBreaker = false DefaultDebtParam = DebtParam{
DefaultCollateralParams = CollateralParams{}
DefaultDebtParam = DebtParam{
Denom: "usdx", Denom: "usdx",
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
} }
DefaultCdpStartingID = uint64(1) DefaultCdpStartingID = uint64(1)
DefaultDebtDenom = "debt" DefaultDebtDenom = "debt"
DefaultGovDenom = "ukava" DefaultGovDenom = "ukava"
DefaultStableDenom = "usdx" DefaultStableDenom = "usdx"
DefaultSurplusThreshold = sdk.NewInt(500000000000) DefaultSurplusThreshold = sdk.NewInt(500000000000)
DefaultDebtThreshold = sdk.NewInt(100000000000) DefaultDebtThreshold = sdk.NewInt(100000000000)
DefaultSurplusLot = sdk.NewInt(10000000000) DefaultSurplusLot = sdk.NewInt(10000000000)
DefaultDebtLot = sdk.NewInt(10000000000) DefaultDebtLot = sdk.NewInt(10000000000)
DefaultPreviousDistributionTime = tmtime.Canonical(time.Unix(0, 0)) DefaultSavingsRateDistributed = sdk.NewInt(0)
DefaultSavingsDistributionFrequency = time.Hour * 12 minCollateralPrefix = 0
DefaultSavingsRateDistributed = sdk.NewInt(0) maxCollateralPrefix = 255
minCollateralPrefix = 0 stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
maxCollateralPrefix = 255
stabilityFeeMax = sdk.MustNewDecFromStr("1.000000051034942716") // 500% APR
) )
// Params governance parameters for cdp module // Params governance parameters for cdp module
type Params struct { type Params struct {
CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"` CollateralParams CollateralParams `json:"collateral_params" yaml:"collateral_params"`
DebtParam DebtParam `json:"debt_param" yaml:"debt_param"` DebtParam DebtParam `json:"debt_param" yaml:"debt_param"`
GlobalDebtLimit sdk.Coin `json:"global_debt_limit" yaml:"global_debt_limit"` GlobalDebtLimit sdk.Coin `json:"global_debt_limit" yaml:"global_debt_limit"`
SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"` SurplusAuctionThreshold sdk.Int `json:"surplus_auction_threshold" yaml:"surplus_auction_threshold"`
SurplusAuctionLot sdk.Int `json:"surplus_auction_lot" yaml:"surplus_auction_lot"` SurplusAuctionLot sdk.Int `json:"surplus_auction_lot" yaml:"surplus_auction_lot"`
DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"` DebtAuctionThreshold sdk.Int `json:"debt_auction_threshold" yaml:"debt_auction_threshold"`
DebtAuctionLot sdk.Int `json:"debt_auction_lot" yaml:"debt_auction_lot"` 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"`
CircuitBreaker bool `json:"circuit_breaker" yaml:"circuit_breaker"`
} }
// String implements fmt.Stringer // String implements fmt.Stringer
@ -73,28 +64,26 @@ func (p Params) String() string {
Surplus Auction Lot: %s Surplus Auction Lot: %s
Debt Auction Threshold: %s Debt Auction Threshold: %s
Debt Auction Lot: %s Debt Auction Lot: %s
Savings Distribution Frequency: %s
Circuit Breaker: %t`, Circuit Breaker: %t`,
p.GlobalDebtLimit, p.CollateralParams, p.DebtParam, p.SurplusAuctionThreshold, p.SurplusAuctionLot, 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 // NewParams returns a new params object
func NewParams( func NewParams(
debtLimit sdk.Coin, collateralParams CollateralParams, debtParam DebtParam, surplusThreshold, 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 { ) Params {
return Params{ return Params{
GlobalDebtLimit: debtLimit, GlobalDebtLimit: debtLimit,
CollateralParams: collateralParams, CollateralParams: collateralParams,
DebtParam: debtParam, DebtParam: debtParam,
SurplusAuctionThreshold: surplusThreshold, SurplusAuctionThreshold: surplusThreshold,
SurplusAuctionLot: surplusLot, SurplusAuctionLot: surplusLot,
DebtAuctionThreshold: debtThreshold, DebtAuctionThreshold: debtThreshold,
DebtAuctionLot: debtLot, DebtAuctionLot: debtLot,
SavingsDistributionFrequency: distributionFreq, CircuitBreaker: breaker,
CircuitBreaker: breaker,
} }
} }
@ -102,40 +91,46 @@ func NewParams(
func DefaultParams() Params { func DefaultParams() Params {
return NewParams( return NewParams(
DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParam, DefaultSurplusThreshold, DefaultGlobalDebt, DefaultCollateralParams, DefaultDebtParam, DefaultSurplusThreshold,
DefaultSurplusLot, DefaultDebtThreshold, DefaultDebtLot, DefaultSavingsDistributionFrequency, DefaultSurplusLot, DefaultDebtThreshold, DefaultDebtLot,
DefaultCircuitBreaker, DefaultCircuitBreaker,
) )
} }
// CollateralParam governance parameters for each collateral type within the cdp module // CollateralParam governance parameters for each collateral type within the cdp module
type CollateralParam struct { type CollateralParam struct {
Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type Denom string `json:"denom" yaml:"denom"` // Coin name of collateral type
Type string `json:"type" yaml:"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 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 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 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. 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 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"` 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 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 LiquidationMarketID string `json:"liquidation_market_id" yaml:"liquidation_market_id"` // marketID of the pricefeed used for liquidation
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` // factor for converting internal units to one base unit of collateral 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 // 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{ return CollateralParam{
Denom: denom, Denom: denom,
Type: ctype, Type: ctype,
LiquidationRatio: liqRatio, LiquidationRatio: liqRatio,
DebtLimit: debtLimit, DebtLimit: debtLimit,
StabilityFee: stabilityFee, StabilityFee: stabilityFee,
AuctionSize: auctionSize, AuctionSize: auctionSize,
LiquidationPenalty: liqPenalty, LiquidationPenalty: liqPenalty,
Prefix: prefix, Prefix: prefix,
SpotMarketID: spotMarketID, SpotMarketID: spotMarketID,
LiquidationMarketID: liquidationMarketID, LiquidationMarketID: liquidationMarketID,
ConversionFactor: conversionFactor, KeeperRewardPercentage: keeperReward,
CheckCollateralizationIndexCount: checkIndexCount,
ConversionFactor: conversionFactor,
} }
} }
@ -152,8 +147,12 @@ func (cp CollateralParam) String() string {
Prefix: %b Prefix: %b
Spot Market ID: %s Spot Market ID: %s
Liquidation Market ID: %s Liquidation Market ID: %s
Keeper Reward Percentage: %s
Check Collateralization Count: %s
Conversion Factor: %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 // CollateralParams array of CollateralParam
@ -173,18 +172,16 @@ type DebtParam struct {
Denom string `json:"denom" yaml:"denom"` Denom string `json:"denom" yaml:"denom"`
ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"` ReferenceAsset string `json:"reference_asset" yaml:"reference_asset"`
ConversionFactor sdk.Int `json:"conversion_factor" yaml:"conversion_factor"` 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 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 // 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{ return DebtParam{
Denom: denom, Denom: denom,
ReferenceAsset: refAsset, ReferenceAsset: refAsset,
ConversionFactor: conversionFactor, ConversionFactor: conversionFactor,
DebtFloor: debtFloor, DebtFloor: debtFloor,
SavingsRate: savingsRate,
} }
} }
@ -194,8 +191,7 @@ func (dp DebtParam) String() string {
Reference Asset: %s Reference Asset: %s
Conversion Factor: %s Conversion Factor: %s
Debt Floor %s Debt Floor %s
Savings Rate %s `, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor)
`, dp.Denom, dp.ReferenceAsset, dp.ConversionFactor, dp.DebtFloor, dp.SavingsRate)
} }
// DebtParams array of DebtParam // DebtParams array of DebtParam
@ -228,7 +224,6 @@ func (p *Params) ParamSetPairs() params.ParamSetPairs {
params.NewParamSetPair(KeySurplusLot, &p.SurplusAuctionLot, validateSurplusAuctionLotParam), params.NewParamSetPair(KeySurplusLot, &p.SurplusAuctionLot, validateSurplusAuctionLotParam),
params.NewParamSetPair(KeyDebtThreshold, &p.DebtAuctionThreshold, validateDebtAuctionThresholdParam), params.NewParamSetPair(KeyDebtThreshold, &p.DebtAuctionThreshold, validateDebtAuctionThresholdParam),
params.NewParamSetPair(KeyDebtLot, &p.DebtAuctionLot, validateDebtAuctionLotParam), params.NewParamSetPair(KeyDebtLot, &p.DebtAuctionLot, validateDebtAuctionLotParam),
params.NewParamSetPair(KeyDistributionFrequency, &p.SavingsDistributionFrequency, validateSavingsDistributionFrequencyParam),
} }
} }
@ -266,10 +261,6 @@ func (p Params) Validate() error {
return err return err
} }
if err := validateSavingsDistributionFrequencyParam(p.SavingsDistributionFrequency); err != nil {
return err
}
if len(p.CollateralParams) == 0 { // default value OK if len(p.CollateralParams) == 0 { // default value OK
return nil return nil
} }
@ -381,6 +372,12 @@ func validateCollateralParams(i interface{}) error {
if cp.StabilityFee.LT(sdk.OneDec()) || cp.StabilityFee.GT(stabilityFeeMax) { 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) 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 return nil
@ -395,9 +392,6 @@ func validateDebtParam(i interface{}) error {
return fmt.Errorf("debt denom invalid %s", debtParam.Denom) 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 return nil
} }
@ -461,16 +455,3 @@ func validateDebtAuctionLotParam(i interface{}) error {
return nil 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 ( import (
"strings" "strings"
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -28,7 +27,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot sdk.Int surplusLot sdk.Int
debtThreshold sdk.Int debtThreshold sdk.Int
debtLot sdk.Int debtLot sdk.Int
distributionFreq time.Duration
breaker bool breaker bool
} }
type errArgs struct { type errArgs struct {
@ -51,7 +49,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -65,17 +62,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -83,13 +82,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -103,17 +100,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -121,13 +120,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -141,17 +138,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -159,13 +158,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -179,30 +176,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -210,13 +211,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -230,30 +229,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -261,13 +264,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -281,30 +282,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 4000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("susd", 2000000000000), DebtLimit: sdk.NewInt64Coin("susd", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(6), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(6),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -312,13 +317,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -332,16 +335,18 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "", Denom: "",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -349,13 +354,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -369,17 +372,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "", SpotMarketID: "",
LiquidationMarketID: "", LiquidationMarketID: "",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -387,13 +392,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -407,30 +410,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -438,13 +445,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -458,30 +463,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-b", Type: "bnb-b",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x21, Prefix: 0x21,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -489,13 +498,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -509,30 +516,34 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
{ {
Denom: "xrp", Denom: "xrp",
Type: "xrp-a", Type: "xrp-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "xrp:usd", SpotMarketID: "xrp:usd",
LiquidationMarketID: "xrp:usd", LiquidationMarketID: "xrp:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -540,13 +551,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -560,17 +569,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.Coin{}, DebtLimit: sdk.Coin{},
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -578,13 +589,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -598,17 +607,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("1.05"), LiquidationPenalty: sdk.MustNewDecFromStr("1.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -616,13 +627,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -636,17 +645,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.ZeroInt(), AuctionSize: sdk.ZeroInt(),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -654,13 +665,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -674,17 +683,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 1000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.1"), StabilityFee: sdk.MustNewDecFromStr("1.1"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -692,13 +703,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -712,17 +721,19 @@ func (suite *ParamsTestSuite) TestParamValidation() {
globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), globalDebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
collateralParams: types.CollateralParams{ collateralParams: types.CollateralParams{
{ {
Denom: "bnb", Denom: "bnb",
Type: "bnb-a", Type: "bnb-a",
LiquidationRatio: sdk.MustNewDecFromStr("1.5"), LiquidationRatio: sdk.MustNewDecFromStr("1.5"),
DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000), DebtLimit: sdk.NewInt64Coin("usdx", 2000000000000),
StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"), StabilityFee: sdk.MustNewDecFromStr("1.000000001547125958"),
LiquidationPenalty: sdk.MustNewDecFromStr("0.05"), LiquidationPenalty: sdk.MustNewDecFromStr("0.05"),
AuctionSize: sdk.NewInt(50000000000), AuctionSize: sdk.NewInt(50000000000),
Prefix: 0x20, Prefix: 0x20,
SpotMarketID: "bnb:usd", SpotMarketID: "bnb:usd",
LiquidationMarketID: "bnb:usd", LiquidationMarketID: "bnb:usd",
ConversionFactor: sdk.NewInt(8), KeeperRewardPercentage: sdk.MustNewDecFromStr("0.01"),
ConversionFactor: sdk.NewInt(8),
CheckCollateralizationIndexCount: sdk.NewInt(10),
}, },
}, },
debtParam: types.DebtParam{ debtParam: types.DebtParam{
@ -730,13 +741,11 @@ func (suite *ParamsTestSuite) TestParamValidation() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: sdk.NewInt(6), ConversionFactor: sdk.NewInt(6),
DebtFloor: sdk.NewInt(10000000), DebtFloor: sdk.NewInt(10000000),
SavingsRate: sdk.MustNewDecFromStr("0.95"),
}, },
surplusThreshold: types.DefaultSurplusThreshold, surplusThreshold: types.DefaultSurplusThreshold,
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -744,44 +753,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
contains: "debt denom invalid", 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", name: "nil debt limit",
args: args{ args: args{
@ -792,7 +763,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -800,24 +770,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
contains: "invalid coins: global debt limit", 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", name: "zero surplus auction threshold",
args: args{ args: args{
@ -828,7 +780,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -846,7 +797,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: sdk.ZeroInt(), debtThreshold: sdk.ZeroInt(),
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -864,7 +814,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: sdk.ZeroInt(), surplusLot: sdk.ZeroInt(),
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: types.DefaultDebtLot, debtLot: types.DefaultDebtLot,
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -882,7 +831,6 @@ func (suite *ParamsTestSuite) TestParamValidation() {
surplusLot: types.DefaultSurplusLot, surplusLot: types.DefaultSurplusLot,
debtThreshold: types.DefaultDebtThreshold, debtThreshold: types.DefaultDebtThreshold,
debtLot: sdk.ZeroInt(), debtLot: sdk.ZeroInt(),
distributionFreq: types.DefaultSavingsDistributionFrequency,
breaker: types.DefaultCircuitBreaker, breaker: types.DefaultCircuitBreaker,
}, },
errArgs: errArgs{ errArgs: errArgs{
@ -893,7 +841,7 @@ func (suite *ParamsTestSuite) TestParamValidation() {
} }
for _, tc := range testCases { for _, tc := range testCases {
suite.Run(tc.name, func() { 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() err := params.Validate()
if tc.errArgs.expectPass { if tc.errArgs.expectPass {
suite.Require().NoError(err) suite.Require().NoError(err)

View File

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

View File

@ -14,7 +14,7 @@ import (
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade" upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types" 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" "github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
) )

View File

@ -14,7 +14,7 @@ import (
upgrade "github.com/cosmos/cosmos-sdk/x/upgrade" upgrade "github.com/cosmos/cosmos-sdk/x/upgrade"
bep3types "github.com/kava-labs/kava/x/bep3/types" 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" "github.com/kava-labs/kava/x/pricefeed"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
) )

View File

@ -542,7 +542,6 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
ReferenceAsset: "usd", ReferenceAsset: "usd",
ConversionFactor: i(6), ConversionFactor: i(6),
DebtFloor: i(10000000), DebtFloor: i(10000000),
SavingsRate: d("0.95"),
} }
newDenomDP := testDP newDenomDP := testDP
newDenomDP.Denom = "usdz" newDenomDP.Denom = "usdz"
@ -564,8 +563,7 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
{ {
name: "allowed change", name: "allowed change",
allowed: AllowedDebtParam{ allowed: AllowedDebtParam{
DebtFloor: true, DebtFloor: true,
SavingsRate: true,
}, },
current: testDP, current: testDP,
incoming: newDebtFloorDP, incoming: newDebtFloorDP,
@ -574,8 +572,7 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
{ {
name: "un-allowed change", name: "un-allowed change",
allowed: AllowedDebtParam{ allowed: AllowedDebtParam{
DebtFloor: true, DebtFloor: true,
SavingsRate: true,
}, },
current: testDP, current: testDP,
incoming: newDenomDP, incoming: newDenomDP,
@ -584,8 +581,7 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
{ {
name: "allowed no change", name: "allowed no change",
allowed: AllowedDebtParam{ allowed: AllowedDebtParam{
DebtFloor: true, DebtFloor: true,
SavingsRate: true,
}, },
current: testDP, current: testDP,
incoming: testDP, // no change incoming: testDP, // no change
@ -594,8 +590,7 @@ func (suite *PermissionsTestSuite) TestAllowedDebtParam_Allows() {
{ {
name: "un-allowed change with allowed change", name: "un-allowed change with allowed change",
allowed: AllowedDebtParam{ allowed: AllowedDebtParam{
DebtFloor: true, DebtFloor: true,
SavingsRate: true,
}, },
current: testDP, current: testDP,
incoming: newDenomAndDebtFloorDP, incoming: newDenomAndDebtFloorDP,

View File

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

View File

@ -1,4 +1,4 @@
package v0_12 package v0_11
import ( import (
"errors" "errors"
@ -10,6 +10,22 @@ import (
"github.com/cosmos/cosmos-sdk/x/params" "github.com/cosmos/cosmos-sdk/x/params"
cdptypes "github.com/kava-labs/kava/x/cdp/types" 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 // Parameter keys and default values
@ -21,10 +37,58 @@ var (
DefaultGovSchedules = DistributionSchedules{} DefaultGovSchedules = DistributionSchedules{}
DefaultLPSchedules = DistributionSchedules{} DefaultLPSchedules = DistributionSchedules{}
DefaultDelegatorSchedules = DelegatorDistributionSchedules{} DefaultDelegatorSchedules = DelegatorDistributionSchedules{}
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0))
DefaultDistributionTimes = GenesisDistributionTimes{}
GovDenom = cdptypes.DefaultGovDenom 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 { type Params struct {
Active bool `json:"active" yaml:"active"` Active bool `json:"active" yaml:"active"`
LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"` LiquidityProviderSchedules DistributionSchedules `json:"liquidity_provider_schedules" yaml:"liquidity_provider_schedules"`
@ -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 { func DefaultParams() Params {
return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules) return NewParams(DefaultActive, DefaultLPSchedules, DefaultDelegatorSchedules)
} }
@ -342,7 +406,7 @@ func (dt DepositType) IsValid() error {
return fmt.Errorf("invalid deposit type: %s", dt) 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 { type Deposit struct {
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"` Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
Amount sdk.Coin `json:"amount" yaml:"amount"` Amount sdk.Coin `json:"amount" yaml:"amount"`

View File

@ -8,7 +8,10 @@ import (
// BeginBlocker runs at the start of every block // BeginBlocker runs at the start of every block
func BeginBlocker(ctx sdk.Context, k keeper.Keeper) { func BeginBlocker(ctx sdk.Context, k keeper.Keeper) {
k.DeleteExpiredClaimsAndClaimPeriods(ctx) for _, rp := range k.GetParams(ctx).RewardPeriods {
k.ApplyRewardsToCdps(ctx) err := k.AccumulateRewards(ctx, rp)
k.CreateAndDeleteRewardPeriods(ctx) if err != nil {
panic(err)
}
}
} }

View File

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

View File

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/context"
"github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/flags"
@ -25,35 +26,42 @@ func GetQueryCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
incentiveQueryCmd.AddCommand(flags.GetCommands( incentiveQueryCmd.AddCommand(flags.GetCommands(
queryParamsCmd(queryRoute, cdc), queryParamsCmd(queryRoute, cdc),
queryClaimsCmd(queryRoute, cdc), queryClaimsCmd(queryRoute, cdc),
queryRewardPeriodsCmd(queryRoute, cdc),
queryClaimPeriodsCmd(queryRoute, cdc),
)...) )...)
return incentiveQueryCmd return incentiveQueryCmd
} }
const (
flagOwner = "owner"
)
func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command { func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ cmd := &cobra.Command{
Use: "claims [owner-addr] [collateral-type]", Use: "claims ",
Short: "get claims by owner and collateral-type", Short: "query USDX minting claims",
Long: strings.TrimSpace( 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: Example:
$ %s query %s claims kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb-a`, version.ClientName, types.ModuleName)), $ %s query %s claims
Args: cobra.ExactArgs(2), $ %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 { RunE: func(cmd *cobra.Command, args []string) error {
cliCtx := context.NewCLIContext().WithCodec(cdc) cliCtx := context.NewCLIContext().WithCodec(cdc)
strOwner := viper.GetString(flagOwner)
page := viper.GetInt(flags.FlagPage)
limit := viper.GetInt(flags.FlagLimit)
// Prepare params for querier // Prepare params for querier
ownerAddress, err := sdk.AccAddressFromBech32(args[0]) owner, err := sdk.AccAddressFromBech32(strOwner)
if err != nil { if err != nil {
return err return err
} }
bz, err := cdc.MarshalJSON(types.QueryClaimsParams{ params := types.NewQueryClaimsParams(page, limit, owner)
Owner: ownerAddress, bz, err := cdc.MarshalJSON(params)
CollateralType: args[1],
})
if err != nil { if err != nil {
return err return err
} }
@ -66,7 +74,7 @@ func queryClaimsCmd(queryRoute string, cdc *codec.Codec) *cobra.Command {
} }
cliCtx = cliCtx.WithHeight(height) cliCtx = cliCtx.WithHeight(height)
var claims types.AugmentedClaims var claims types.USDXMintingClaims
if err := cdc.UnmarshalJSON(res, &claims); err != nil { if err := cdc.UnmarshalJSON(res, &claims); err != nil {
return fmt.Errorf("failed to unmarshal claims: %w", err) 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 { 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 { func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{ return &cobra.Command{
Use: "claim [owner] [collateral-type] [multiplier]", Use: "claim [owner] [multiplier]",
Short: "claim rewards for cdp owner and collateral-type", Short: "claim rewards for cdp owner and collateral-type",
Long: strings.TrimSpace( Long: strings.TrimSpace(
fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input collateral-type and multiplier, fmt.Sprintf(`Claim any outstanding rewards owned by owner for the input collateral-type and multiplier,
Example: Example:
$ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw bnb-a large $ %s tx %s claim kava15qdefkmwswysgg4qxgqpqr35k3m49pkx2jdfnw large
`, version.ClientName, types.ModuleName), `, version.ClientName, types.ModuleName),
), ),
Args: cobra.ExactArgs(3), Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
inBuf := bufio.NewReader(cmd.InOrStdin()) inBuf := bufio.NewReader(cmd.InOrStdin())
cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc) cliCtx := context.NewCLIContextWithInputAndFrom(inBuf, args[0]).WithCodec(cdc)
@ -54,7 +54,7 @@ func getCmdClaim(cdc *codec.Codec) *cobra.Command {
return err return err
} }
msg := types.NewMsgClaimReward(owner, args[1], args[2]) msg := types.NewMsgClaimUSDXMintingReward(owner, args[1])
err = msg.ValidateBasic() err = msg.ValidateBasic()
if err != nil { if err != nil {
return err return err

View File

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

View File

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

View File

@ -24,51 +24,37 @@ func InitGenesis(ctx sdk.Context, k keeper.Keeper, supplyKeeper types.SupplyKeep
k.SetParams(ctx, gs.Params) k.SetParams(ctx, gs.Params)
for _, r := range gs.Params.Rewards { for _, gat := range gs.PreviousAccumulationTimes {
k.SetNextClaimPeriodID(ctx, r.CollateralType, 1) 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 for _, claim := range gs.USDXMintingClaims {
if !gs.PreviousBlockTime.Equal(types.DefaultPreviousBlockTime) { k.SetClaim(ctx, claim)
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)
} }
} }
// ExportGenesis export genesis state for incentive module // ExportGenesis export genesis state for incentive module
func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState { func ExportGenesis(ctx sdk.Context, k keeper.Keeper) types.GenesisState {
// get all objects out of the store
params := k.GetParams(ctx) 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 claims := k.GetAllClaims(ctx)
// immediately after InitGenesis, there would be no previousBlockTime value.
if !found { var gats GenesisAccumulationTimes
previousBlockTime = types.DefaultPreviousBlockTime
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)
} }
// Get all objects from the store return types.NewGenesisState(params, gats, claims)
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)
} }

View File

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

View File

@ -13,6 +13,7 @@ import (
"github.com/kava-labs/kava/app" "github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/incentive" "github.com/kava-labs/kava/x/incentive"
"github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist" "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))) coins = append(coins, cs(c("bnb", 10000000000), c("ukava", 10000000000)))
} }
authGS := app.NewAuthGenState(addrs, coins) 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.addrs = addrs
suite.handler = incentive.NewHandler(keeper) suite.handler = incentive.NewHandler(keeper)
@ -51,15 +61,10 @@ func (suite *HandlerTestSuite) SetupTest() {
} }
func (suite *HandlerTestSuite) addClaim() { func (suite *HandlerTestSuite) addClaim() {
supplyKeeper := suite.app.GetSupplyKeeper() sk := suite.app.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(suite.ctx, kavadist.ModuleName) err := sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000)))
err := supplyKeeper.MintCoins(suite.ctx, macc.GetName(), cs(c("ukava", 1000000)))
suite.Require().NoError(err) 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"))}) c1 := incentive.NewUSDXMintingClaim(suite.addrs[0], c("ukava", 1000000), types.RewardIndexes{types.NewRewardIndex("bnb-s", sdk.ZeroDec())})
suite.NotPanics(func() {
suite.keeper.SetClaimPeriod(suite.ctx, cp)
})
c1 := incentive.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1)
suite.NotPanics(func() { suite.NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c1) suite.keeper.SetClaim(suite.ctx, c1)
}) })
@ -67,7 +72,7 @@ func (suite *HandlerTestSuite) addClaim() {
func (suite *HandlerTestSuite) TestMsgClaimReward() { func (suite *HandlerTestSuite) TestMsgClaimReward() {
suite.addClaim() suite.addClaim()
msg := incentive.NewMsgClaimReward(suite.addrs[0], "bnb", "small") msg := incentive.NewMsgClaimUSDXMintingReward(suite.addrs[0], "small")
res, err := suite.handler(suite.ctx, msg) res, err := suite.handler(suite.ctx, msg)
suite.NoError(err) suite.NoError(err)
suite.Require().NotNil(res) suite.Require().NotNil(res)
@ -75,3 +80,7 @@ func (suite *HandlerTestSuite) TestMsgClaimReward() {
func TestHandlerTestSuite(t *testing.T) { func TestHandlerTestSuite(t *testing.T) {
suite.Run(t, new(HandlerTestSuite)) 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 // 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) store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := store.Get(types.GetClaimPrefix(addr, collateralType, id)) bz := store.Get(addr)
if bz == nil { if bz == nil {
return types.Claim{}, false return types.USDXMintingClaim{}, false
} }
var c types.Claim var c types.USDXMintingClaim
k.cdc.MustUnmarshalBinaryBare(bz, &c) k.cdc.MustUnmarshalBinaryBare(bz, &c)
return c, true return c, true
} }
// SetClaim sets the claim in the store corresponding to the input address, collateral type, and id // 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) store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
bz := k.cdc.MustMarshalBinaryBare(c) 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 // 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 := 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 // 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) store := prefix.NewStore(ctx.KVStore(k.key), types.ClaimKeyPrefix)
iterator := sdk.KVStorePrefixIterator(store, []byte{}) iterator := sdk.KVStorePrefixIterator(store, []byte{})
defer iterator.Close() defer iterator.Close()
for ; iterator.Valid(); iterator.Next() { for ; iterator.Valid(); iterator.Next() {
var c types.Claim var c types.USDXMintingClaim
k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c) k.cdc.MustUnmarshalBinaryBare(iterator.Value(), &c)
if cb(c) { if cb(c) {
break 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 // GetAllClaims returns all Claim objects in the store
func (k Keeper) GetAllClaims(ctx sdk.Context) types.Claims { func (k Keeper) GetAllClaims(ctx sdk.Context) types.USDXMintingClaims {
cs := types.Claims{} cs := types.USDXMintingClaims{}
k.IterateClaims(ctx, func(c types.Claim) (stop bool) { k.IterateClaims(ctx, func(c types.USDXMintingClaim) (stop bool) {
cs = append(cs, c) cs = append(cs, c)
return false return false
}) })
return cs return cs
} }
// GetPreviousBlockTime get the blocktime for the previous block // GetPreviousAccrualTime returns the last time a collateral type accrued rewards
func (k Keeper) GetPreviousBlockTime(ctx sdk.Context) (blockTime time.Time, found bool) { func (k Keeper) GetPreviousAccrualTime(ctx sdk.Context, ctype string) (blockTime time.Time, found bool) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) store := prefix.NewStore(ctx.KVStore(k.key), types.BlockTimeKey)
b := store.Get([]byte{}) bz := store.Get([]byte(ctype))
if b == nil { if bz == nil {
return time.Time{}, false return time.Time{}, false
} }
k.cdc.MustUnmarshalBinaryBare(b, &blockTime) k.cdc.MustUnmarshalBinaryBare(bz, &blockTime)
return blockTime, true return blockTime, true
} }
// SetPreviousBlockTime set the time of the previous block // SetPreviousAccrualTime sets the last time a collateral type accrued rewards
func (k Keeper) SetPreviousBlockTime(ctx sdk.Context, blockTime time.Time) { func (k Keeper) SetPreviousAccrualTime(ctx sdk.Context, ctype string, blockTime time.Time) {
store := prefix.NewStore(ctx.KVStore(k.key), types.PreviousBlockTimeKey) store := prefix.NewStore(ctx.KVStore(k.key), types.BlockTimeKey)
store.Set([]byte{}, k.cdc.MustMarshalBinaryBare(blockTime)) 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 ( import (
"testing" "testing"
"time"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth"
authexported "github.com/cosmos/cosmos-sdk/x/auth/exported" 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" supplyexported "github.com/cosmos/cosmos-sdk/x/supply/exported"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
@ -33,7 +34,7 @@ func (suite *KeeperTestSuite) SetupTest() {
tApp := app.NewTestApp() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates() tApp.InitializeFromGenesisStates()
_, addrs := app.GeneratePrivKeyAddressPairs(1) _, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIncentiveKeeper() keeper := tApp.GetIncentiveKeeper()
suite.app = tApp suite.app = tApp
suite.ctx = ctx suite.ctx = ctx
@ -51,129 +52,63 @@ func (suite *KeeperTestSuite) getModuleAccount(name string) supplyexported.Modul
return sk.GetModuleAccount(suite.ctx, name) 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() { func (suite *KeeperTestSuite) TestGetSetDeleteClaim() {
c := types.NewClaim(suite.addrs[0], c("ukava", 1000000), "bnb", 1) 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], "bnb", 1) _, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.False(found) suite.Require().False(found)
suite.NotPanics(func() { suite.Require().NotPanics(func() {
suite.keeper.SetClaim(suite.ctx, c) suite.keeper.SetClaim(suite.ctx, c)
}) })
testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1) testC, found := suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.True(found) suite.Require().True(found)
suite.Equal(c, testC) suite.Require().Equal(c, testC)
suite.NotPanics(func() { suite.Require().NotPanics(func() {
suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0], "bnb", 1) suite.keeper.DeleteClaim(suite.ctx, suite.addrs[0])
}) })
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1) _, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0])
suite.False(found) suite.Require().False(found)
} }
func (suite *KeeperTestSuite) TestIterateMethods() { func (suite *KeeperTestSuite) TestIterateClaims() {
suite.addObjectsToStore() // adds 2 objects of each type to the store 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())})
var rewardPeriods types.RewardPeriods suite.Require().NotPanics(func() {
suite.keeper.IterateRewardPeriods(suite.ctx, func(rp types.RewardPeriod) (stop bool) { suite.keeper.SetClaim(suite.ctx, c)
rewardPeriods = append(rewardPeriods, rp) })
return false }
}) claims := types.USDXMintingClaims{}
suite.Equal(2, len(rewardPeriods)) suite.keeper.IterateClaims(suite.ctx, func(c types.USDXMintingClaim) bool {
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 = append(claims, c) claims = append(claims, c)
return false return false
}) })
suite.Equal(2, len(claims)) suite.Require().Equal(len(suite.addrs), 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)
claims = suite.keeper.GetAllClaims(suite.ctx)
suite.Require().Equal(len(suite.addrs), len(claims))
} }
func TestKeeperTestSuite(t *testing.T) { func TestKeeperTestSuite(t *testing.T) {
suite.Run(t, new(KeeperTestSuite)) 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 package keeper
import ( import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/incentive/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) { func (k Keeper) SetParams(ctx sdk.Context, params types.Params) {
k.paramSubspace.SetParamSet(ctx, &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 package keeper
import ( import (
"fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
@ -14,42 +12,48 @@ import (
validatorvesting "github.com/kava-labs/kava/x/validator-vesting" validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
) )
// PayoutClaim sends the timelocked claim coins to the input address // ClaimReward sends the reward amount to the input address and zero's out the claim in the store
func (k Keeper) PayoutClaim(ctx sdk.Context, addr sdk.AccAddress, collateralType string, id uint64, multiplierName types.MultiplierName) error { func (k Keeper) ClaimReward(ctx sdk.Context, addr sdk.AccAddress, multiplierName types.MultiplierName) error {
claim, found := k.GetClaim(ctx, addr, collateralType, id) claim, found := k.GetClaim(ctx, addr)
if !found { if !found {
return sdkerrors.Wrapf(types.ErrClaimNotFound, "id: %d, collateral type %s, address: %s", id, collateralType, addr) return sdkerrors.Wrapf(types.ErrClaimNotFound, "address: %s", addr)
}
claimPeriod, found := k.GetClaimPeriod(ctx, id, collateralType)
if !found {
return sdkerrors.Wrapf(types.ErrClaimPeriodNotFound, "id: %d, collateral type: %s", id, collateralType)
} }
multiplier, found := claimPeriod.GetMultiplier(multiplierName) multiplier, found := k.GetMultiplier(ctx, multiplierName)
if !found { if !found {
return sdkerrors.Wrapf(types.ErrInvalidMultiplier, string(multiplierName)) 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() { if rewardAmount.IsZero() {
return types.ErrZeroClaim return types.ErrZeroClaim
} }
rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount) rewardCoin := sdk.NewCoin(claim.Reward.Denom, rewardAmount)
length := ctx.BlockTime().AddDate(0, int(multiplier.MonthsLockup), 0).Unix() - ctx.BlockTime().Unix() 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 { if err != nil {
return err return err
} }
k.DeleteClaim(ctx, addr, collateralType, id) k.ZeroClaim(ctx, claim)
ctx.EventManager().EmitEvent( ctx.EventManager().EmitEvent(
sdk.NewEvent( sdk.NewEvent(
types.EventTypeClaim, types.EventTypeClaim,
sdk.NewAttribute(types.AttributeKeyClaimedBy, addr.String()), sdk.NewAttribute(types.AttributeKeyClaimedBy, addr.String()),
sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()), sdk.NewAttribute(types.AttributeKeyClaimAmount, claim.Reward.String()),
sdk.NewAttribute(types.AttributeKeyClaimPeriod, fmt.Sprintf("%d", claim.ClaimPeriodID)),
), ),
) )
return nil return nil
@ -112,75 +116,6 @@ func (k Keeper) SendTimeLockedCoinsToBaseAccount(ctx sdk.Context, senderModule s
return nil 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 // 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 // the input address must be a periodic vesting account
func (k Keeper) addCoinsToVestingSchedule(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins, length int64) { 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 ( import (
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
sdk "github.com/cosmos/cosmos-sdk/types" 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/auth/vesting"
abci "github.com/tendermint/tendermint/abci/types" 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/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/incentive/types"
"github.com/kava-labs/kava/x/kavadist" "github.com/kava-labs/kava/x/kavadist"
validatorvesting "github.com/kava-labs/kava/x/validator-vesting" validatorvesting "github.com/kava-labs/kava/x/validator-vesting"
"github.com/tendermint/tendermint/crypto"
) )
func (suite *KeeperTestSuite) setupChain() { func (suite *KeeperTestSuite) TestPayoutClaim() {
// creates a new app state with 4 funded addresses and 1 module account type args struct {
tApp := app.NewTestApp() ctype string
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)}) rewardsPerSecond sdk.Coin
_, addrs := app.GeneratePrivKeyAddressPairs(4) initialTime time.Time
authGS := app.NewAuthGenState( initialCollateral sdk.Coin
addrs, initialPrincipal sdk.Coin
[]sdk.Coins{ multipliers types.Multipliers
cs(c("ukava", 400)), multiplier types.MultiplierName
cs(c("ukava", 400)), timeElapsed int
cs(c("ukava", 400)), expectedBalance sdk.Coins
cs(c("ukava", 400)), 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),
)
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)
// setup kavadist state
err = sk.MintCoins(suite.ctx, kavadist.ModuleName, cs(c("ukava", 1000000000000)))
suite.Require().NoError(err)
// 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)
}
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,
)
supplyKeeper := tApp.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 600)))
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)
// 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
}
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)),
})
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() { func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
@ -391,7 +426,7 @@ func (suite *KeeperTestSuite) TestSendCoinsToPeriodicVestingAccount() {
} }
func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() { func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
suite.setupChain() suite.SetupWithAccountState()
// send coins to base account // send coins to base account
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[1], cs(c("ukava", 100)), 5) err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[1], cs(c("ukava", 100)), 5)
suite.Require().NoError(err) suite.Require().NoError(err)
@ -410,267 +445,59 @@ func (suite *KeeperTestSuite) TestSendCoinsToBaseAccount() {
} }
func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() { func (suite *KeeperTestSuite) TestSendCoinsToInvalidAccount() {
suite.setupChain() suite.SetupWithAccountState()
err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[2], cs(c("ukava", 100)), 5) err := suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, suite.addrs[2], cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType)) 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) err = suite.keeper.SendTimeLockedCoinsToAccount(suite.ctx, kavadist.ModuleName, macc.GetAddress(), cs(c("ukava", 100)), 5)
suite.Require().True(errors.Is(err, types.ErrInvalidAccountType)) suite.Require().True(errors.Is(err, types.ErrInvalidAccountType))
} }
func (suite *KeeperTestSuite) TestDeleteExpiredClaimPeriods() { func (suite *KeeperTestSuite) SetupWithAccountState() {
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 // creates a new app state with 4 funded addresses and 1 module account
tApp := app.NewTestApp()
// both claim periods are present ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: time.Unix(100, 0)})
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb") _, addrs := app.GeneratePrivKeyAddressPairs(4)
suite.True(found) authGS := app.NewAuthGenState(
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "xrp") addrs,
suite.True(found) []sdk.Coins{
// both claims are present cs(c("ukava", 400)),
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "bnb", 1) cs(c("ukava", 400)),
suite.True(found) cs(c("ukava", 400)),
_, found = suite.keeper.GetClaim(suite.ctx, suite.addrs[0], "xrp", 1) cs(c("ukava", 400)),
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
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tc.args.blockTime})
authGS := app.NewAuthGenState(
[]sdk.AccAddress{tc.args.claimOwner},
[]sdk.Coins{
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
})
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,
)
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 {
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))
}
}) })
tApp.InitializeFromGenesisStates(
authGS,
)
supplyKeeper := tApp.GetSupplyKeeper()
macc := supplyKeeper.GetModuleAccount(ctx, kavadist.ModuleName)
err := supplyKeeper.MintCoins(ctx, macc.GetName(), cs(c("ukava", 600)))
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)
// 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 package keeper
import ( import (
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
@ -18,10 +19,6 @@ func NewQuerier(k Keeper) sdk.Querier {
return queryGetParams(ctx, req, k) return queryGetParams(ctx, req, k)
case types.QueryGetClaims: case types.QueryGetClaims:
return queryGetClaims(ctx, req, k) return queryGetClaims(ctx, req, k)
case types.QueryGetRewardPeriods:
return queryGetRewardPeriods(ctx, req, k)
case types.QueryGetClaimPeriods:
return queryGetClaimPeriods(ctx, req, k)
default: default:
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName) return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unknown %s query endpoint", types.ModuleName)
} }
@ -41,41 +38,30 @@ func queryGetParams(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, e
return bz, nil 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) { func queryGetClaims(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, error) {
var requestParams types.QueryClaimsParams var requestParams types.QueryClaimsParams
err := k.cdc.UnmarshalJSON(req.Data, &requestParams) err := k.cdc.UnmarshalJSON(req.Data, &requestParams)
if err != nil { if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) 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 { if err != nil {
return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) 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 package keeper
import ( import (
"math"
"time" "time"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
cdptypes "github.com/kava-labs/kava/x/cdp/types" cdptypes "github.com/kava-labs/kava/x/cdp/types"
"github.com/kava-labs/kava/x/incentive/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 // AccumulateRewards updates the rewards accumulated for the input reward period
func (k Keeper) HandleRewardPeriodExpiry(ctx sdk.Context, rp types.RewardPeriod) { func (k Keeper) AccumulateRewards(ctx sdk.Context, rewardPeriod types.RewardPeriod) error {
k.CreateUniqueClaimPeriod(ctx, rp.CollateralType, rp.ClaimEnd, rp.ClaimMultipliers) if !rewardPeriod.Active {
store := prefix.NewStore(ctx.KVStore(k.key), types.RewardPeriodKeyPrefix) k.SetPreviousAccrualTime(ctx, rewardPeriod.CollateralType, ctx.BlockTime())
store.Delete([]byte(rp.CollateralType)) return nil
return
}
// 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)
}
} }
previousAccrualTime, found := k.GetPreviousAccrualTime(ctx, rewardPeriod.CollateralType)
if !found {
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
} }
// ApplyRewardsToCdps iterates over the reward periods and creates a claim for each // InitializeClaim creates or updates a claim such that no new rewards are accrued, but any existing rewards are not lost.
// cdp owner that created usdx with the collateral specified in the reward period. // this function should be called after a cdp is created. If a user previously had a cdp, then closed it, they shouldn't
func (k Keeper) ApplyRewardsToCdps(ctx sdk.Context) { // accrue rewards during the period the cdp was closed. By setting the reward factor to the current global reward factor,
previousBlockTime, found := k.GetPreviousBlockTime(ctx) // 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 { if !found {
previousBlockTime = ctx.BlockTime() // this collateral type is not incentivized, do nothing
k.SetPreviousBlockTime(ctx, previousBlockTime)
return return
} }
rewardFactor, found := k.GetRewardFactor(ctx, cdp.Type)
k.IterateRewardPeriods(ctx, func(rp types.RewardPeriod) bool { if !found {
expired := false rewardFactor = sdk.ZeroDec()
// the total amount of usdx created with the collateral type being incentivized }
totalPrincipal := k.cdpKeeper.GetTotalPrincipal(ctx, rp.CollateralType, types.PrincipalDenom) claim, found := k.GetClaim(ctx, cdp.Owner)
// the number of seconds since last payout if !found { // this is the owner's first usdx minting reward claim
timeElapsed := sdk.NewInt(ctx.BlockTime().Unix() - previousBlockTime.Unix()) claim = types.NewUSDXMintingClaim(cdp.Owner, sdk.NewCoin(types.USDXMintingRewardDenom, sdk.ZeroInt()), types.RewardIndexes{types.NewRewardIndex(cdp.Type, rewardFactor)})
if rp.End.Before(ctx.BlockTime()) { k.SetClaim(ctx, claim)
timeElapsed = sdk.NewInt(rp.End.Unix() - previousBlockTime.Unix()) return
expired = true }
} // the owner has an existing usdx minting reward claim
index, hasRewardIndex := claim.HasRewardIndex(cdp.Type)
// the amount of rewards to pay (rewardAmount * timeElapsed) if !hasRewardIndex { // this is the owner's first usdx minting reward for this collateral type
rewardsThisPeriod := rp.Reward.Amount.Mul(timeElapsed) claim.RewardIndexes = append(claim.RewardIndexes, types.NewRewardIndex(cdp.Type, rewardFactor))
id := k.GetNextClaimPeriodID(ctx, rp.CollateralType) } else { // the owner has a previous usdx minting reward for this collateral type
k.cdpKeeper.IterateCdpsByCollateralType(ctx, rp.CollateralType, func(cdp cdptypes.CDP) bool { claim.RewardIndexes[index] = types.NewRewardIndex(cdp.Type, rewardFactor)
rewardsShare := sdk.NewDecFromInt(cdp.GetTotalPrincipal().Amount).Quo(sdk.NewDecFromInt(totalPrincipal))
// sanity check - don't create zero claims
if rewardsShare.IsZero() {
return false
}
rewardsEarned := rewardsShare.Mul(sdk.NewDecFromInt(rewardsThisPeriod)).RoundInt()
k.AddToClaim(ctx, cdp.Owner, rp.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)
} }
k.SetClaim(ctx, claim) 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 package keeper_test
import ( import (
"fmt"
"testing"
"time" "time"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types" abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time" tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app" "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/incentive/types"
"github.com/kava-labs/kava/x/pricefeed"
) )
func (suite *KeeperTestSuite) TestExpireRewardPeriod() { func (suite *KeeperTestSuite) TestAccumulateRewards() {
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"))}) type args struct {
suite.keeper.SetRewardPeriod(suite.ctx, rp) ctype string
suite.keeper.SetNextClaimPeriodID(suite.ctx, "bnb", 1) rewardsPerSecond sdk.Coin
suite.NotPanics(func() { initialTime time.Time
suite.keeper.HandleRewardPeriodExpiry(suite.ctx, rp) initialTotalPrincipal sdk.Coin
}) timeElapsed int
_, found := suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb") expectedRewardFactor sdk.Dec
suite.True(found) }
} type test struct {
name string
func (suite *KeeperTestSuite) TestAddToClaim() { args args
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) testCases := []test{
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 {
name string
arg string
expectFound bool
}{
{ {
"active reward period", "7 seconds",
"bnb", args{
true, 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", "1 day",
"xrp", args{
false, 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", "0 seconds",
"btc", args{
false, 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 { for _, tc := range testCases {
suite.Run(tc.name, func() { suite.Run(tc.name, func() {
_, found := suite.keeper.GetRewardPeriod(suite.ctx, tc.arg) suite.SetupWithCDPGenState()
if tc.expectFound { suite.ctx = suite.ctx.WithBlockTime(tc.args.initialTime)
suite.True(found)
} else { // setup cdp state
suite.False(found) 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() { func (suite *KeeperTestSuite) TestSyncRewards() {
suite.setupCdpChain() // creates a test app with 3 BNB cdps and usdx incentives for bnb - each reward period is one week 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
}
// move the context forward by 100 periods testCases := []test{
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Second * 100)) {
// apply rewards to BNB cdps "10 blocks",
suite.NotPanics(func() { args{
suite.keeper.ApplyRewardsToCdps(suite.ctx) ctype: "bnb-a",
}) rewardsPerSecond: c("ukava", 122354),
// each cdp should have a claim initialTime: time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC),
claims := types.Claims{} initialCollateral: c("bnb", 1000000000000),
suite.keeper.IterateClaims(suite.ctx, func(c types.Claim) (stop bool) { initialPrincipal: c("usdx", 10000000000),
claims = append(claims, c) blockTimes: []int{10, 10, 10, 10, 10, 10, 10, 10, 10, 10},
return false expectedRewardFactor: d("0.001223540000000000"),
}) expectedRewards: c("ukava", 12235400),
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) "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)
// move ctx to the reward period expiry and check that the claim period has been created and the next claim period id has increased // setup incentive state
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(time.Hour * 24 * 7)) 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())
suite.NotPanics(func() { // setup account state
// apply rewards to cdps sk := suite.app.GetSupplyKeeper()
suite.keeper.ApplyRewardsToCdps(suite.ctx) sk.MintCoins(suite.ctx, cdptypes.ModuleName, sdk.NewCoins(tc.args.initialCollateral))
// delete the old reward period amd create a new one sk.SendCoinsFromModuleToAccount(suite.ctx, cdptypes.ModuleName, suite.addrs[0], sdk.NewCoins(tc.args.initialCollateral))
suite.keeper.CreateAndDeleteRewardPeriods(suite.ctx)
}) // setup cdp state
_, found = suite.keeper.GetClaimPeriod(suite.ctx, 1, "bnb-a") cdpKeeper := suite.app.GetCDPKeeper()
suite.True(found) err := cdpKeeper.AddCdp(suite.ctx, suite.addrs[0], tc.args.initialCollateral, tc.args.initialPrincipal, tc.args.ctype)
testID := suite.keeper.GetNextClaimPeriodID(suite.ctx, "bnb-a") suite.Require().NoError(err)
suite.Equal(uint64(2), testID)
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)
})
}
// 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) setupCdpChain() { func TestRewardCalculation(t *testing.T) {
// 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
// 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() tApp := app.NewTestApp()
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()}) ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
// 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( tApp.InitializeFromGenesisStates(
authGS, NewPricefeedGenStateMulti(),
pricefeedAppGs, NewCDPGenStateMulti(),
incentiveAppGs,
cdpAppGs,
) )
_, addrs := app.GeneratePrivKeyAddressPairs(5)
keeper := tApp.GetIncentiveKeeper()
suite.app = tApp suite.app = tApp
suite.keeper = tApp.GetIncentiveKeeper()
suite.ctx = ctx suite.ctx = ctx
// create 3 cdps suite.keeper = keeper
cdpKeeper := tApp.GetCDPKeeper() suite.addrs = addrs
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())
} }
// Avoid cluttering test cases with long function names // newRepeatingSliceInt creates a slice of the specified length containing a single repeating element.
func i(in int64) sdk.Int { return sdk.NewInt(in) } func newRepeatingSliceInt(element int, length int) []int {
func d(str string) sdk.Dec { return sdk.MustNewDecFromStr(str) } slice := make([]int, length)
func c(denom string, amount int64) sdk.Coin { return sdk.NewInt64Coin(denom, amount) } for i := 0; i < length; i++ {
func cs(coins ...sdk.Coin) sdk.Coins { return sdk.NewCoins(coins...) } slice[i] = element
}
return slice
}

View File

@ -2,11 +2,11 @@ package simulation
import ( import (
"bytes" "bytes"
"encoding/binary"
"fmt" "fmt"
"time" "time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/tendermint/tendermint/libs/kv" "github.com/tendermint/tendermint/libs/kv"
@ -16,35 +16,25 @@ import (
// DecodeStore unmarshals the KVPair's Value to the module's corresponding type // DecodeStore unmarshals the KVPair's Value to the module's corresponding type
func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string { func DecodeStore(cdc *codec.Codec, kvA, kvB kv.Pair) string {
switch { 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): case bytes.Equal(kvA.Key[:1], types.ClaimKeyPrefix):
var claimA, claimB types.Claim var claimA, claimB types.USDXMintingClaim
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &claimA) cdc.MustUnmarshalBinaryBare(kvA.Value, &claimA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &claimB) cdc.MustUnmarshalBinaryBare(kvB.Value, &claimB)
return fmt.Sprintf("%v\n%v", claimA, claimB) return fmt.Sprintf("%v\n%v", claimA, claimB)
case bytes.Equal(kvA.Key[:1], types.NextClaimPeriodIDPrefix): case bytes.Equal(kvA.Key[:1], types.BlockTimeKey):
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):
var timeA, timeB time.Time var timeA, timeB time.Time
cdc.MustUnmarshalBinaryLengthPrefixed(kvA.Value, &timeA) cdc.MustUnmarshalBinaryBare(kvA.Value, &timeA)
cdc.MustUnmarshalBinaryLengthPrefixed(kvB.Value, &timeB) cdc.MustUnmarshalBinaryBare(kvB.Value, &timeB)
return fmt.Sprintf("%s\n%s", timeA, 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: default:
panic(fmt.Sprintf("invalid %s key prefix %X", types.ModuleName, kvA.Key[:1])) 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) { func TestDecodeDistributionStore(t *testing.T) {
cdc := makeTestCodec() 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") 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() prevBlockTime := time.Now().Add(time.Hour * -1).UTC()
factor := sdk.ZeroDec()
kvPairs := kv.Pairs{ kvPairs := kv.Pairs{
kv.Pair{Key: types.RewardPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&rewardPeriod)}, kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryBare(claim)},
kv.Pair{Key: types.ClaimPeriodKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claimPeriod)}, kv.Pair{Key: []byte(types.BlockTimeKey), Value: cdc.MustMarshalBinaryBare(prevBlockTime)},
kv.Pair{Key: types.ClaimKeyPrefix, Value: cdc.MustMarshalBinaryLengthPrefixed(&claim)}, kv.Pair{Key: []byte(types.RewardFactorKey), Value: cdc.MustMarshalBinaryBare(factor)},
kv.Pair{Key: types.NextClaimPeriodIDPrefix, Value: sdk.Uint64ToBigEndian(10)},
kv.Pair{Key: []byte(types.PreviousBlockTimeKey), Value: cdc.MustMarshalBinaryLengthPrefixed(prevBlockTime)},
kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}}, kv.Pair{Key: []byte{0x99}, Value: []byte{0x99}},
} }
@ -46,11 +40,9 @@ func TestDecodeDistributionStore(t *testing.T) {
name string name string
expectedLog 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)}, {"Claim", fmt.Sprintf("%v\n%v", claim, claim)},
{"NextClaimPeriodID", "10\n10"},
{"PreviousBlockTime", fmt.Sprintf("%v\n%v", prevBlockTime, prevBlockTime)}, {"PreviousBlockTime", fmt.Sprintf("%v\n%v", prevBlockTime, prevBlockTime)},
{"RewardFactor", fmt.Sprintf("%v\n%v", factor, factor)},
{"other", ""}, {"other", ""},
} }
for i, tt := range tests { for i, tt := range tests {

View File

@ -2,15 +2,11 @@ package simulation
import ( import (
"fmt" "fmt"
"math/rand"
"time"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module" "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" "github.com/kava-labs/kava/x/incentive/types"
) )
@ -23,115 +19,9 @@ var (
// RandomizedGenState generates a random GenesisState for incentive module // RandomizedGenState generates a random GenesisState for incentive module
func RandomizedGenState(simState *module.SimulationState) { 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 // New genesis state holds valid, linked reward periods, claim periods, and claim period IDs
incentiveGenesis := types.NewGenesisState(params, types.DefaultPreviousBlockTime, incentiveGenesis := types.DefaultGenesisState()
rewardPeriods, claimPeriods, types.Claims{}, claimPeriodIDs)
if err := incentiveGenesis.Validate(); err != nil {
panic(err)
}
fmt.Printf("Selected randomly generated %s parameters:\n%s\n", types.ModuleName, codec.MustMarshalJSONIndent(simState.Cdc, incentiveGenesis)) 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) 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 package simulation
import ( import (
"fmt"
"math/rand" "math/rand"
"github.com/cosmos/cosmos-sdk/baseapp" "github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth" "github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/auth/vesting"
"github.com/cosmos/cosmos-sdk/x/simulation" "github.com/cosmos/cosmos-sdk/x/simulation"
appparams "github.com/kava-labs/kava/app/params" appparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/incentive/keeper" "github.com/kava-labs/kava/x/incentive/keeper"
"github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/incentive/types"
"github.com/kava-labs/kava/x/kavadist"
) )
// Simulation operation weights constants // 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, r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) { ) (simulation.OperationMsg, []simulation.FutureOperation, error) {
// Load only account types that can claim rewards return simulation.NewOperationMsgBasic(types.ModuleName,
validAccounts := make(map[string]bool) "no-operation (no accounts currently have fulfillable claims)", "", false, nil), nil, nil
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 package simulation
import ( import (
"fmt"
"math/rand" "math/rand"
"github.com/cosmos/cosmos-sdk/x/simulation" "github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/kava-labs/kava/x/incentive/types"
) )
const ( const (
@ -23,16 +20,5 @@ func genActive(r *rand.Rand) bool {
// ParamChanges defines the parameters that can be modified by param change proposals // ParamChanges defines the parameters that can be modified by param change proposals
func ParamChanges(r *rand.Rand) []simulation.ParamChange { func ParamChanges(r *rand.Rand) []simulation.ParamChange {
return []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))
},
),
}
} }

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 // RegisterCodec registers the necessary types for incentive module
func RegisterCodec(cdc *codec.Codec) { func RegisterCodec(cdc *codec.Codec) {
cdc.RegisterConcrete(MsgClaimReward{}, "incentive/MsgClaimReward", nil) cdc.RegisterConcrete(MsgClaimUSDXMintingReward{}, "incentive/MsgClaimUSDXMintingReward", nil)
cdc.RegisterConcrete(GenesisClaimPeriodID{}, "incentive/GenesisClaimPeriodID", nil)
cdc.RegisterConcrete(RewardPeriod{}, "incentive/RewardPeriod", nil)
cdc.RegisterConcrete(ClaimPeriod{}, "incentive/ClaimPeriod", nil)
cdc.RegisterConcrete(Claim{}, "incentive/Claim", nil)
} }

View File

@ -9,11 +9,12 @@ import (
// Incentive module errors // Incentive module errors
var ( var (
ErrClaimNotFound = sdkerrors.Register(ModuleName, 2, "no claim with input id found for owner and collateral type") 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") ErrInvalidAccountType = sdkerrors.Register(ModuleName, 4, "account type not supported")
ErrNoClaimsFound = sdkerrors.Register(ModuleName, 5, "no claims with collateral type found for address") 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") ErrInsufficientModAccountBalance = sdkerrors.Register(ModuleName, 6, "module account has insufficient balance to pay claim")
ErrAccountNotFound = sdkerrors.Register(ModuleName, 7, "account not found") ErrAccountNotFound = sdkerrors.Register(ModuleName, 7, "account not found")
ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 8, "invalid rewards multiplier") ErrInvalidMultiplier = sdkerrors.Register(ModuleName, 8, "invalid rewards multiplier")
ErrZeroClaim = sdkerrors.Register(ModuleName, 9, "cannot claim - claim amount rounds to zero") 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 // CdpKeeper defines the expected cdp keeper for interacting with cdps
type CdpKeeper interface { 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) 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 // 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 GetAccount(ctx sdk.Context, addr sdk.AccAddress) authexported.Account
SetAccount(ctx sdk.Context, acc 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,83 +2,34 @@ package types
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"strings"
"time" "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. // GenesisState is the state that must be provided at genesis.
type GenesisState struct { type GenesisState struct {
Params Params `json:"params" yaml:"params"` Params Params `json:"params" yaml:"params"`
PreviousBlockTime time.Time `json:"previous_block_time" yaml:"previous_block_time"` PreviousAccumulationTimes GenesisAccumulationTimes `json:"previous_accumulation_times" yaml:"previous_accumulation_times"`
RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"` USDXMintingClaims USDXMintingClaims `json:"usdx_minting_claims" yaml:"usdx_minting_claims"`
ClaimPeriods ClaimPeriods `json:"claim_periods" yaml:"claim_periods"`
Claims Claims `json:"claims" yaml:"claims"`
NextClaimPeriodIDs GenesisClaimPeriodIDs `json:"next_claim_period_ids" yaml:"next_claim_period_ids"`
} }
// NewGenesisState returns a new genesis state // 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{ return GenesisState{
Params: params, Params: params,
PreviousBlockTime: previousBlockTime, PreviousAccumulationTimes: prevAccumTimes,
RewardPeriods: rp, USDXMintingClaims: c,
ClaimPeriods: cp,
Claims: c,
NextClaimPeriodIDs: ids,
} }
} }
// DefaultGenesisState returns a default genesis state // DefaultGenesisState returns a default genesis state
func DefaultGenesisState() GenesisState { func DefaultGenesisState() GenesisState {
return GenesisState{ return GenesisState{
Params: DefaultParams(), Params: DefaultParams(),
PreviousBlockTime: DefaultPreviousBlockTime, PreviousAccumulationTimes: GenesisAccumulationTimes{},
RewardPeriods: RewardPeriods{}, USDXMintingClaims: DefaultClaims,
ClaimPeriods: ClaimPeriods{},
Claims: Claims{},
NextClaimPeriodIDs: GenesisClaimPeriodIDs{},
} }
} }
@ -88,19 +39,11 @@ func (gs GenesisState) Validate() error {
if err := gs.Params.Validate(); err != nil { if err := gs.Params.Validate(); err != nil {
return err return err
} }
if gs.PreviousBlockTime.IsZero() { if err := gs.PreviousAccumulationTimes.Validate(); err != nil {
return errors.New("previous block time cannot be 0")
}
if err := gs.RewardPeriods.Validate(); err != nil {
return err return err
} }
if err := gs.ClaimPeriods.Validate(); err != nil {
return err return gs.USDXMintingClaims.Validate()
}
if err := gs.Claims.Validate(); err != nil {
return err
}
return gs.NextClaimPeriodIDs.Validate()
} }
// Equal checks whether two gov GenesisState structs are equivalent // 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 { func (gs GenesisState) IsEmpty() bool {
return gs.Equal(GenesisState{}) 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 package types
import ( import (
"strings"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
tmtypes "github.com/tendermint/tendermint/types" "github.com/tendermint/tendermint/crypto"
sdk "github.com/cosmos/cosmos-sdk/types" 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) { func TestGenesisStateValidate(t *testing.T) {
now := time.Now() type args struct {
mockPrivKey := tmtypes.NewMockPV() params Params
pubkey, err := mockPrivKey.GetPubKey() genAccTimes GenesisAccumulationTimes
require.NoError(t, err) claims USDXMintingClaims
owner := sdk.AccAddress(pubkey.Address()) }
rewards := Rewards{ type errArgs struct {
NewReward( expectPass bool
true, "bnb", sdk.NewCoin("ukava", sdk.NewInt(10000000000)), contains string
time.Hour*24*7, Multipliers{NewMultiplier(Small, 1, sdk.MustNewDecFromStr("0.33"))}, time.Hour*24*14,
),
} }
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 { testCases := []struct {
msg string name string
genesisState GenesisState args args
expPass bool errArgs errArgs
}{ }{
{ {
msg: "default", name: "default",
genesisState: DefaultGenesisState(), args: args{
expPass: true, params: DefaultParams(),
genAccTimes: DefaultGenesisAccumulationTimes,
claims: DefaultClaims,
},
errArgs: errArgs{
expectPass: true,
contains: "",
},
}, },
{ {
msg: "valid genesis", name: "valid",
genesisState: NewGenesisState( args: args{
NewParams(true, rewards), 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)),
now, rewardPeriods, claimPeriods, claims, gcps, genAccTimes: GenesisAccumulationTimes{GenesisAccumulationTime{
), CollateralType: "bnb-a",
expPass: true, PreviousAccumulationTime: time.Date(2020, 10, 15, 14, 0, 0, 0, time.UTC),
}, RewardFactor: sdk.ZeroDec(),
{ }},
msg: "invalid Params", claims: USDXMintingClaims{
genesisState: GenesisState{ {
Params: Params{ Owner: sdk.AccAddress(crypto.AddressHash([]byte("KavaTestUser1"))),
Active: true, Reward: sdk.NewCoin("ukava", sdk.NewInt(100000000)),
Rewards: Rewards{
Reward{}, RewardIndexes: []RewardIndex{
{
CollateralType: "bnb-a",
RewardFactor: sdk.ZeroDec(),
},
},
}, },
}, },
}, },
expPass: false, errArgs: errArgs{
}, expectPass: true,
{ contains: "",
msg: "zero PreviousBlockTime",
genesisState: GenesisState{
PreviousBlockTime: time.Time{},
}, },
expPass: false,
}, },
{ {
msg: "invalid RewardsPeriod", name: "invalid genesis accumulation time",
genesisState: GenesisState{ args: args{
PreviousBlockTime: now, params: DefaultParams(),
RewardPeriods: RewardPeriods{ genAccTimes: GenesisAccumulationTimes{
{Start: time.Time{}}, {
CollateralType: "btcb-a",
RewardFactor: sdk.MustNewDecFromStr("-0.1"),
},
},
claims: DefaultClaims,
},
errArgs: errArgs{
expectPass: false,
contains: "reward factor should be ≥ 0.0",
},
},
{
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",
},
},
{
name: "invalid claim",
args: args{
params: DefaultParams(),
genAccTimes: DefaultGenesisAccumulationTimes,
claims: USDXMintingClaims{
{
Owner: sdk.AccAddress{},
Reward: sdk.NewCoin("ukava", sdk.NewInt(100000000)),
RewardIndexes: []RewardIndex{
{
CollateralType: "bnb-a",
RewardFactor: sdk.ZeroDec(),
},
},
},
}, },
}, },
expPass: false, errArgs: errArgs{
}, expectPass: false,
{ contains: "claim owner cannot be empty",
msg: "invalid ClaimPeriods",
genesisState: GenesisState{
PreviousBlockTime: now,
ClaimPeriods: ClaimPeriods{
{ID: 0},
},
}, },
expPass: false,
},
{
msg: "invalid Claims",
genesisState: GenesisState{
PreviousBlockTime: now,
Claims: Claims{
{ClaimPeriodID: 0},
},
},
expPass: false,
},
{
msg: "invalid NextClaimPeriodIds",
genesisState: GenesisState{
PreviousBlockTime: now,
NextClaimPeriodIDs: GenesisClaimPeriodIDs{
{ID: 0},
},
},
expPass: false,
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
err := tc.genesisState.Validate() t.Run(tc.name, func(t *testing.T) {
if tc.expPass { gs := NewGenesisState(tc.args.params, tc.args.genAccTimes, tc.args.claims)
require.NoError(t, err, tc.msg) err := gs.Validate()
} else { if tc.errArgs.expectPass {
require.Error(t, err, tc.msg) require.NoError(t, err, tc.name)
} } else {
require.Error(t, err, tc.name)
require.True(t, strings.Contains(err.Error(), tc.errArgs.contains))
}
})
} }
} }

View File

@ -1,11 +1,5 @@
package types package types
import (
"encoding/binary"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const ( const (
// ModuleName The name that will be used throughout the module // ModuleName The name that will be used throughout the module
ModuleName = "incentive" ModuleName = "incentive"
@ -25,37 +19,9 @@ const (
// Key Prefixes // Key Prefixes
var ( var (
RewardPeriodKeyPrefix = []byte{0x01} // prefix for keys that store reward periods ClaimKeyPrefix = []byte{0x01} // prefix for keys that store claims
ClaimPeriodKeyPrefix = []byte{0x02} // prefix for keys that store claim periods BlockTimeKey = []byte{0x02} // prefix for key that stores the blocktime
ClaimKeyPrefix = []byte{0x03} // prefix for keys that store claims RewardFactorKey = []byte{0x03} // prefix for key that stores reward factors
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 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 package types
import ( import (
"fmt"
"strings" "strings"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
@ -9,48 +8,43 @@ import (
) )
// ensure Msg interface compliance at compile time // ensure Msg interface compliance at compile time
var _ sdk.Msg = &MsgClaimReward{} var _ sdk.Msg = &MsgClaimUSDXMintingReward{}
// MsgClaimReward message type used to claim rewards // MsgClaimUSDXMintingReward message type used to claim rewards
type MsgClaimReward struct { type MsgClaimUSDXMintingReward struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"` Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
} }
// NewMsgClaimReward returns a new MsgClaimReward. // NewMsgClaimUSDXMintingReward returns a new MsgClaimUSDXMintingReward.
func NewMsgClaimReward(sender sdk.AccAddress, collateralType, multiplierName string) MsgClaimReward { func NewMsgClaimUSDXMintingReward(sender sdk.AccAddress, multiplierName string) MsgClaimUSDXMintingReward {
return MsgClaimReward{ return MsgClaimUSDXMintingReward{
Sender: sender, Sender: sender,
CollateralType: collateralType,
MultiplierName: multiplierName, MultiplierName: multiplierName,
} }
} }
// Route return the message type used for routing the message. // 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. // 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. // 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() { if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be 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() return MultiplierName(strings.ToLower(msg.MultiplierName)).IsValid()
} }
// GetSignBytes gets the canonical byte representation of the Msg. // GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgClaimReward) GetSignBytes() []byte { func (msg MsgClaimUSDXMintingReward) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(msg) bz := ModuleCdc.MustMarshalJSON(msg)
return sdk.MustSortJSON(bz) return sdk.MustSortJSON(bz)
} }
// GetSigners returns the addresses of signers that must sign. // 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} return []sdk.AccAddress{msg.Sender}
} }

View File

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

View File

@ -1,6 +1,7 @@
package types package types
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@ -23,40 +24,49 @@ const (
// Parameter keys and default values // Parameter keys and default values
var ( var (
KeyActive = []byte("Active") KeyActive = []byte("Active")
KeyRewards = []byte("Rewards") KeyRewards = []byte("RewardPeriods")
DefaultActive = false KeyClaimEnd = []byte("ClaimEnd")
DefaultRewards = Rewards{} KeyMultipliers = []byte("ClaimMultipliers")
DefaultPreviousBlockTime = tmtime.Canonical(time.Unix(0, 0)) DefaultActive = false
GovDenom = cdptypes.DefaultGovDenom DefaultRewardPeriods = RewardPeriods{}
PrincipalDenom = "usdx" DefaultMultipliers = Multipliers{}
IncentiveMacc = kavadistTypes.ModuleName DefaultClaims = USDXMintingClaims{}
DefaultGenesisAccumulationTimes = GenesisAccumulationTimes{}
DefaultClaimEnd = tmtime.Canonical(time.Unix(0, 0))
GovDenom = cdptypes.DefaultGovDenom
PrincipalDenom = "usdx"
IncentiveMacc = kavadistTypes.ModuleName
) )
// Params governance parameters for the incentive module // Params governance parameters for the incentive module
type Params struct { type Params struct {
Active bool `json:"active" yaml:"active"` // top level governance switch to disable all rewards RewardPeriods RewardPeriods `json:"reward_periods" yaml:"reward_periods"`
Rewards Rewards `json:"rewards" yaml:"rewards"` ClaimMultipliers Multipliers `json:"claim_multipliers" yaml:"claim_multipliers"`
ClaimEnd time.Time `json:"claim_end" yaml:"claim_end"`
} }
// NewParams returns a new params object // 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{ return Params{
Active: active, RewardPeriods: rewards,
Rewards: rewards, ClaimMultipliers: multipliers,
ClaimEnd: claimEnd,
} }
} }
// DefaultParams returns default params for incentive module // DefaultParams returns default params for incentive module
func DefaultParams() Params { func DefaultParams() Params {
return NewParams(DefaultActive, DefaultRewards) return NewParams(DefaultRewardPeriods, DefaultMultipliers, DefaultClaimEnd)
} }
// String implements fmt.Stringer // String implements fmt.Stringer
func (p Params) String() string { func (p Params) String() string {
return fmt.Sprintf(`Params: return fmt.Sprintf(`Params:
Active: %t Rewards: %s
Rewards: %s`, p.Active, p.Rewards) Claim Multipliers :%s
Claim End Time: %s
`, p.RewardPeriods, p.ClaimMultipliers, p.ClaimEnd)
} }
// ParamKeyTable Key declaration for parameters // 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 // ParamSetPairs implements the ParamSet interface and returns all the key/value pairs
func (p *Params) ParamSetPairs() params.ParamSetPairs { func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{ return params.ParamSetPairs{
params.NewParamSetPair(KeyActive, &p.Active, validateActiveParam), params.NewParamSetPair(KeyRewards, &p.RewardPeriods, validateRewardsParam),
params.NewParamSetPair(KeyRewards, &p.Rewards, validateRewardsParam), params.NewParamSetPair(KeyClaimEnd, &p.ClaimEnd, validateClaimEndParam),
params.NewParamSetPair(KeyMultipliers, &p.ClaimMultipliers, validateMultipliersParam),
} }
} }
// Validate checks that the parameters have valid values. // Validate checks that the parameters have valid values.
func (p Params) Validate() error { func (p Params) Validate() error {
if err := validateActiveParam(p.Active); err != nil { if err := validateMultipliersParam(p.ClaimMultipliers); err != nil {
return err return err
} }
return validateRewardsParam(p.Rewards) return validateRewardsParam(p.RewardPeriods)
} }
func validateActiveParam(i interface{}) error { func validateActiveParam(i interface{}) error {
@ -90,7 +101,7 @@ func validateActiveParam(i interface{}) error {
} }
func validateRewardsParam(i interface{}) error { func validateRewardsParam(i interface{}) error {
rewards, ok := i.(Rewards) rewards, ok := i.(RewardPeriods)
if !ok { if !ok {
return fmt.Errorf("invalid parameter type: %T", i) return fmt.Errorf("invalid parameter type: %T", i)
} }
@ -98,93 +109,97 @@ func validateRewardsParam(i interface{}) error {
return rewards.Validate() return rewards.Validate()
} }
// Reward stores the specified state for a single reward period. func validateMultipliersParam(i interface{}) error {
type Reward struct { multipliers, ok := i.(Multipliers)
Active bool `json:"active" yaml:"active"` // governance switch to disable a period if !ok {
CollateralType string `json:"collateral_type" yaml:"collateral_type"` // the collateral type rewards apply to, must be found in the cdp collaterals return fmt.Errorf("invalid parameter type: %T", i)
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 return multipliers.Validate()
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
} }
// NewReward returns a new Reward func validateClaimEndParam(i interface{}) error {
func NewReward(active bool, collateralType string, reward sdk.Coin, duration time.Duration, multiplier Multipliers, claimDuration time.Duration) Reward { endTime, ok := i.(time.Time)
return Reward{ 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, Active: active,
CollateralType: collateralType, CollateralType: collateralType,
AvailableRewards: reward, Start: start,
Duration: duration, End: end,
ClaimMultipliers: multiplier, RewardsPerSecond: reward,
ClaimDuration: claimDuration,
} }
} }
// String implements fmt.Stringer // Validate performs a basic check of a RewardPeriod fields.
func (r Reward) String() string { func (rp RewardPeriod) Validate() error {
return fmt.Sprintf(`Reward: if rp.Start.IsZero() {
Active: %t, return errors.New("reward period start time cannot be 0")
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 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 !r.AvailableRewards.IsPositive() { if rp.End.IsZero() {
return fmt.Errorf("reward amount must be positive, is %s for %s", r.AvailableRewards, r.CollateralType) return errors.New("reward period end time cannot be 0")
} }
if r.Duration <= 0 { if rp.Start.After(rp.End) {
return fmt.Errorf("reward duration must be positive, is %s for %s", r.Duration, r.CollateralType) return fmt.Errorf("end period time %s cannot be before start time %s", rp.End, rp.Start)
} }
if err := r.ClaimMultipliers.Validate(); err != nil { if !rp.RewardsPerSecond.IsValid() {
return err return fmt.Errorf("invalid reward amount: %s", rp.RewardsPerSecond)
} }
if r.ClaimDuration <= 0 { if strings.TrimSpace(rp.CollateralType) == "" {
return fmt.Errorf("claim duration must be positive, is %s for %s", r.ClaimDuration, r.CollateralType) return fmt.Errorf("reward period collateral type cannot be blank: %s", rp)
}
if strings.TrimSpace(r.CollateralType) == "" {
return fmt.Errorf("collateral type cannot be blank: %s", r)
} }
return nil return nil
} }
// Rewards array of Reward // RewardPeriods array of RewardPeriod
type Rewards []Reward 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. // entries.
func (rs Rewards) Validate() error { func (rps RewardPeriods) Validate() error {
rewardCollateralTypes := make(map[string]bool) seenPeriods := make(map[string]bool)
for _, r := range rs { for _, rp := range rps {
if rewardCollateralTypes[r.CollateralType] { if seenPeriods[rp.CollateralType] {
return fmt.Errorf("cannot have duplicate reward collateral types: %s", r.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 return err
} }
seenPeriods[rp.CollateralType] = true
rewardCollateralTypes[r.CollateralType] = true
} }
return nil 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 // Multiplier amount the claim rewards get increased by, along with how long the claim rewards are locked
type Multiplier struct { type Multiplier struct {
Name MultiplierName `json:"name" yaml:"name"` Name MultiplierName `json:"name" yaml:"name"`
@ -209,6 +224,9 @@ func (m Multiplier) Validate() error {
if m.MonthsLockup < 0 { if m.MonthsLockup < 0 {
return fmt.Errorf("expected non-negative lockup, got %d", m.MonthsLockup) 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() { if m.Factor.IsNegative() {
return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String()) return fmt.Errorf("expected non-negative factor, got %s", m.Factor.String())
} }

View File

@ -5,212 +5,104 @@ import (
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/suite"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/incentive/types" "github.com/kava-labs/kava/x/incentive/types"
) )
type paramTest struct {
name string
params types.Params
errResult errResult
}
type errResult struct {
expectPass bool
contains string
}
type ParamTestSuite struct { type ParamTestSuite struct {
suite.Suite suite.Suite
tests []paramTest
} }
func (suite *ParamTestSuite) SetupTest() { func (suite *ParamTestSuite) SetupTest() {}
suite.tests = []paramTest{
func (suite *ParamTestSuite) TestParamValidation() {
type args struct {
rewardPeriods types.RewardPeriods
multipliers types.Multipliers
end time.Time
}
type errArgs struct {
expectPass bool
contains string
}
type test struct {
name string
args args
errArgs errArgs
}
testCases := []test{
{ {
name: "valid - active", "default",
params: types.Params{ args{
Active: true, rewardPeriods: types.DefaultRewardPeriods,
Rewards: types.Rewards{ multipliers: types.DefaultMultipliers,
types.Reward{ end: types.DefaultClaimEnd,
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{ errArgs{
expectPass: true, expectPass: true,
contains: "", contains: "",
}, },
}, },
{ {
name: "valid - inactive", "valid",
params: types.Params{ args{
Active: false, rewardPeriods: types.RewardPeriods{types.NewRewardPeriod(
Rewards: types.Rewards{ 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),
types.Reward{ sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)))},
Active: true, multipliers: types.Multipliers{
CollateralType: "bnb-a", types.NewMultiplier(
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), types.Small, 1, sdk.MustNewDecFromStr("0.25"),
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"))}, types.NewMultiplier(
ClaimDuration: time.Hour * 24 * 14, types.Large, 1, sdk.MustNewDecFromStr("1.0"),
}, ),
}, },
end: time.Date(2025, 10, 15, 14, 0, 0, 0, time.UTC),
}, },
errResult: errResult{ errArgs{
expectPass: true, expectPass: true,
contains: "", contains: "",
}, },
}, },
{ {
name: "duplicate reward", "invalid: empty reward factor",
params: types.Params{ args{
Active: true, rewardPeriods: types.RewardPeriods{types.NewRewardPeriod(
Rewards: types.Rewards{ 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),
types.Reward{ sdk.NewCoin(types.USDXMintingRewardDenom, sdk.NewInt(122354)))},
Active: true, multipliers: types.Multipliers{
CollateralType: "bnb-a", types.NewMultiplier(
AvailableRewards: sdk.NewCoin("ukava", sdk.NewInt(10000000000)), types.Small, 1, sdk.MustNewDecFromStr("0.25"),
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"))}, types.NewMultiplier(
ClaimDuration: time.Hour * 24 * 14, 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, 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 _, tc := range testCases {
for _, t := range suite.tests { suite.Run(tc.name, func() {
suite.Run(t.name, func() { params := types.NewParams(
err := t.params.Validate() tc.args.rewardPeriods, tc.args.multipliers, tc.args.end,
if t.errResult.expectPass { )
err := params.Validate()
if tc.errArgs.expectPass {
suite.Require().NoError(err) suite.Require().NoError(err)
} else { } else {
suite.Require().Error(err) 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 // QueryClaimsParams params for query /incentive/claims
type QueryClaimsParams struct { type QueryClaimsParams struct {
Owner sdk.AccAddress Page int `json:"page" yaml:"page"`
CollateralType string Limit int `json:"limit" yaml:"limit"`
Owner sdk.AccAddress
} }
// NewQueryClaimsParams returns QueryClaimsParams // NewQueryClaimsParams returns QueryClaimsParams
func NewQueryClaimsParams(owner sdk.AccAddress, collateralType string) QueryClaimsParams { func NewQueryClaimsParams(page, limit int, owner sdk.AccAddress) QueryClaimsParams {
return QueryClaimsParams{ return QueryClaimsParams{
Owner: owner, Page: page,
CollateralType: collateralType, Limit: limit,
Owner: owner,
} }
} }
@ -33,6 +35,5 @@ func NewQueryClaimsParams(owner sdk.AccAddress, collateralType string) QueryClai
type PostClaimReq struct { type PostClaimReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"` BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Sender sdk.AccAddress `json:"sender" yaml:"sender"` Sender sdk.AccAddress `json:"sender" yaml:"sender"`
CollateralType string `json:"collateral_type" yaml:"collateral_type"`
MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"` MultiplierName string `json:"multiplier_name" yaml:"multiplier_name"`
} }

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