0g-chain/x/cdp/simulation/operations.go

298 lines
11 KiB
Go
Raw Normal View History

package simulation
import (
"fmt"
"math/rand"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/simapp/helpers"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/simulation"
appparams "github.com/kava-labs/kava/app/params"
"github.com/kava-labs/kava/x/cdp/keeper"
"github.com/kava-labs/kava/x/cdp/types"
)
// Simulation operation weights constants
const (
OpWeightMsgCdp = "op_weight_msg_cdp"
)
// WeightedOperations returns all the operations from the module with their respective weights
func WeightedOperations(
appParams simulation.AppParams, cdc *codec.Codec, ak types.AccountKeeper,
k keeper.Keeper, pfk types.PricefeedKeeper,
) simulation.WeightedOperations {
var weightMsgCdp int
appParams.GetOrGenerate(cdc, OpWeightMsgCdp, &weightMsgCdp, nil,
func(_ *rand.Rand) {
weightMsgCdp = appparams.DefaultWeightMsgCdp
},
)
return simulation.WeightedOperations{
simulation.NewWeightedOperation(
weightMsgCdp,
SimulateMsgCdp(ak, k, pfk),
),
}
}
// SimulateMsgCdp generates a MsgCreateCdp or MsgDepositCdp with random values.
func SimulateMsgCdp(ak types.AccountKeeper, k keeper.Keeper, pfk types.PricefeedKeeper) simulation.Operation {
return func(
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, chainID string,
) (simulation.OperationMsg, []simulation.FutureOperation, error) {
simAccount, _ := simulation.RandomAcc(r, accs)
acc := ak.GetAccount(ctx, simAccount.Address)
if acc == nil {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
coins := acc.GetCoins()
collateralParams := k.GetParams(ctx).CollateralParams
if len(collateralParams) == 0 {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
randCollateralParam := collateralParams[r.Intn(len(collateralParams))]
debtParam, _ := k.GetDebtParam(ctx, randCollateralParam.DebtLimit.Denom)
if coins.AmountOf(randCollateralParam.Denom).IsZero() {
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
price, err := pfk.GetCurrentPrice(ctx, randCollateralParam.SpotMarketID)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, nil // pricefeed going down is an expected event
}
// convert the price to the same units as the debt param
priceShifted := ShiftDec(price.Price, debtParam.ConversionFactor)
spendableCoins := acc.SpendableCoins(ctx.BlockTime())
fees, err := simulation.RandomFees(r, ctx, spendableCoins)
if err != nil {
return simulation.NoOpMsg(types.ModuleName), nil, err
}
spendableCoins = spendableCoins.Sub(fees)
existingCDP, found := k.GetCdpByOwnerAndCollateralType(ctx, acc.GetAddress(), randCollateralParam.Type)
if !found {
// calculate the minimum amount of collateral that is needed to create a cdp with the debt floor amount of debt and the minimum liquidation ratio
// (debtFloor * liquidationRatio)/priceShifted
minCollateralDeposit := (sdk.NewDecFromInt(debtParam.DebtFloor).Mul(randCollateralParam.LiquidationRatio)).Quo(priceShifted)
// convert to proper collateral units
minCollateralDeposit = ShiftDec(minCollateralDeposit, randCollateralParam.ConversionFactor)
// convert to integer and always round up
minCollateralDepositRounded := minCollateralDeposit.TruncateInt().Add(sdk.OneInt())
if spendableCoins.AmountOf(randCollateralParam.Denom).LT(minCollateralDepositRounded) {
// account doesn't have enough funds to open a cdp for the min debt amount
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "insufficient funds to open cdp", false, nil), nil, nil
}
// set the max collateral deposit to the amount of coins in the account
maxCollateralDeposit := spendableCoins.AmountOf(randCollateralParam.Denom)
// randomly select a collateral deposit amount
collateralDeposit := sdk.NewInt(int64(simulation.RandIntBetween(r, int(minCollateralDepositRounded.Int64()), int(maxCollateralDeposit.Int64()))))
// calculate how much the randomly selected deposit is worth
collateralDepositValue := ShiftDec(sdk.NewDecFromInt(collateralDeposit), randCollateralParam.ConversionFactor.Neg()).Mul(priceShifted)
// calculate the max amount of debt that could be drawn for the chosen deposit
maxDebtDraw := collateralDepositValue.Quo(randCollateralParam.LiquidationRatio).TruncateInt()
// check that the debt limit hasn't been reached
availableAssetDebt := randCollateralParam.DebtLimit.Amount.Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Type, debtParam.Denom))
if availableAssetDebt.LTE(debtParam.DebtFloor) {
// debt limit has been reached
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot open cdp", false, nil), nil, nil
}
// ensure that the debt draw does not exceed the debt limit
maxDebtDraw = sdk.MinInt(maxDebtDraw, availableAssetDebt)
// randomly select a debt draw amount
debtDraw := sdk.NewInt(int64(simulation.RandIntBetween(r, int(debtParam.DebtFloor.Int64()), int(maxDebtDraw.Int64()))))
msg := types.NewMsgCreateCDP(acc.GetAddress(), sdk.NewCoin(randCollateralParam.Denom, collateralDeposit), sdk.NewCoin(debtParam.Denom, debtDraw), randCollateralParam.Type)
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{acc.GetAccountNumber()},
[]uint64{acc.GetSequence()},
simAccount.PrivKey,
)
_, _, 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
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// a cdp already exists, deposit to it, draw debt from it, or repay debt to it
// close 25% of the time
if canClose(spendableCoins, existingCDP, debtParam.Denom) && shouldClose(r) {
repaymentAmount := spendableCoins.AmountOf(debtParam.Denom)
// check that repayment isn't below the debt floor
if existingCDP.Principal.Amount.Add(existingCDP.AccumulatedFees.Amount).Sub(repaymentAmount).LT(debtParam.DebtFloor) {
repaymentAmount = existingCDP.Principal.Amount.Add(existingCDP.AccumulatedFees.Amount).Sub(debtParam.DebtFloor)
}
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Type, sdk.NewCoin(debtParam.Denom, repaymentAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{acc.GetAccountNumber()},
[]uint64{acc.GetSequence()},
simAccount.PrivKey,
)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// deposit 25% of the time
if hasCoins(spendableCoins, randCollateralParam.Denom) && shouldDeposit(r) {
randDepositAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(spendableCoins.AmountOf(randCollateralParam.Denom).Int64()))))
msg := types.NewMsgDeposit(acc.GetAddress(), acc.GetAddress(), sdk.NewCoin(randCollateralParam.Denom, randDepositAmount), randCollateralParam.Type)
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{acc.GetAccountNumber()},
[]uint64{acc.GetSequence()},
simAccount.PrivKey,
)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// draw debt 25% of the time
if shouldDraw(r) {
collateralShifted := ShiftDec(sdk.NewDecFromInt(existingCDP.Collateral.Amount), randCollateralParam.ConversionFactor.Neg())
collateralValue := collateralShifted.Mul(priceShifted)
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>
2021-01-18 19:12:37 +00:00
newFeesAccumulated := k.CalculateNewInterest(ctx, existingCDP)
totalFees := existingCDP.AccumulatedFees.Add(newFeesAccumulated)
// given the current collateral value, calculate how much debt we could add while maintaining a valid liquidation ratio
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>
2021-01-18 19:12:37 +00:00
debt := existingCDP.Principal.Add(totalFees)
maxTotalDebt := collateralValue.Quo(randCollateralParam.LiquidationRatio)
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>
2021-01-18 19:12:37 +00:00
maxDebt := (maxTotalDebt.Sub(sdk.NewDecFromInt(debt.Amount))).Mul(sdk.MustNewDecFromStr("0.95")).TruncateInt()
if maxDebt.LTE(sdk.OneInt()) {
// debt in cdp is maxed out
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "cdp debt maxed out, cannot draw more debt", false, nil), nil, nil
}
// check if the debt limit has been reached
availableAssetDebt := randCollateralParam.DebtLimit.Amount.Sub(k.GetTotalPrincipal(ctx, randCollateralParam.Type, debtParam.Denom))
if availableAssetDebt.LTE(sdk.OneInt()) {
// debt limit has been reached
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "debt limit reached, cannot draw more debt", false, nil), nil, nil
}
maxDraw := sdk.MinInt(maxDebt, availableAssetDebt)
randDrawAmount := sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxDraw.Int64()))))
msg := types.NewMsgDrawDebt(acc.GetAddress(), randCollateralParam.Type, sdk.NewCoin(debtParam.Denom, randDrawAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{acc.GetAccountNumber()},
[]uint64{acc.GetSequence()},
simAccount.PrivKey,
)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
// repay debt 25% of the time
if hasCoins(spendableCoins, debtParam.Denom) {
debt := existingCDP.Principal.Amount.Add(existingCDP.AccumulatedFees.Amount)
payableDebt := debt.Sub(debtParam.DebtFloor)
if payableDebt.IsZero() {
return simulation.NewOperationMsgBasic(types.ModuleName, "no-operation", "cannot make partial repayment, cdp at debt floor", false, nil), nil, nil
}
maxRepay := sdk.MinInt(
spendableCoins.AmountOf(debtParam.Denom),
payableDebt,
)
var randRepayAmount sdk.Int
if maxRepay.Equal(sdk.OneInt()) {
randRepayAmount = sdk.OneInt()
} else {
randRepayAmount = sdk.NewInt(int64(simulation.RandIntBetween(r, 1, int(maxRepay.Int64()))))
}
msg := types.NewMsgRepayDebt(acc.GetAddress(), randCollateralParam.Type, sdk.NewCoin(debtParam.Denom, randRepayAmount))
tx := helpers.GenTx(
[]sdk.Msg{msg},
fees,
helpers.DefaultGenTxGas,
chainID,
[]uint64{acc.GetAccountNumber()},
[]uint64{acc.GetSequence()},
simAccount.PrivKey,
)
_, _, err := app.Deliver(tx)
if err != nil {
return simulation.NewOperationMsg(msg, false, fmt.Sprintf("%+v", err)), nil, err
}
return simulation.NewOperationMsg(msg, true, ""), nil, nil
}
return simulation.NoOpMsg(types.ModuleName), nil, nil
}
}
func shouldDraw(r *rand.Rand) bool {
threshold := 50
value := simulation.RandIntBetween(r, 1, 100)
return value > threshold
}
func shouldDeposit(r *rand.Rand) bool {
threshold := 66
value := simulation.RandIntBetween(r, 1, 100)
return value > threshold
}
func hasCoins(spendableCoins sdk.Coins, denom string) bool {
return spendableCoins.AmountOf(denom).IsPositive()
}
func shouldClose(r *rand.Rand) bool {
threshold := 75
value := simulation.RandIntBetween(r, 1, 100)
return value > threshold
}
func canClose(spendableCoins sdk.Coins, c types.CDP, denom string) bool {
repaymentAmount := c.Principal.Add(c.AccumulatedFees).Amount
return spendableCoins.AmountOf(denom).GTE(repaymentAmount)
}