mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-27 00:35:18 +00:00
205 lines
8.4 KiB
Go
205 lines
8.4 KiB
Go
package keeper
|
|
|
|
import (
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/x/params/subspace"
|
|
|
|
"github.com/kava-labs/kava/x/auction"
|
|
"github.com/kava-labs/kava/x/liquidator/types"
|
|
)
|
|
|
|
type Keeper struct {
|
|
cdc *codec.Codec
|
|
paramSubspace subspace.Subspace
|
|
key sdk.StoreKey
|
|
cdpKeeper types.CdpKeeper
|
|
auctionKeeper types.AuctionKeeper
|
|
bankKeeper types.BankKeeper
|
|
}
|
|
|
|
func NewKeeper(cdc *codec.Codec, storeKey sdk.StoreKey, paramstore subspace.Subspace, cdpKeeper types.CdpKeeper, auctionKeeper types.AuctionKeeper, bankKeeper types.BankKeeper) Keeper {
|
|
subspace := paramstore.WithKeyTable(types.ParamKeyTable())
|
|
return Keeper{
|
|
cdc: cdc,
|
|
paramSubspace: subspace,
|
|
key: storeKey,
|
|
cdpKeeper: cdpKeeper,
|
|
auctionKeeper: auctionKeeper,
|
|
bankKeeper: bankKeeper,
|
|
}
|
|
}
|
|
|
|
// SeizeAndStartCollateralAuction pulls collateral out of a CDP and sells it in an auction for stable coin. Excess collateral goes to the original CDP owner.
|
|
// Known as Cat.bite in maker
|
|
// result: stable coin is transferred to module account, collateral is transferred from module account to buyer, (and any excess collateral is transferred to original CDP owner)
|
|
func (k Keeper) SeizeAndStartCollateralAuction(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string) (auction.ID, sdk.Error) {
|
|
// Get CDP
|
|
cdp, found := k.cdpKeeper.GetCDP(ctx, owner, collateralDenom)
|
|
if !found {
|
|
return 0, sdk.ErrInternal("CDP not found")
|
|
}
|
|
|
|
// Calculate amount of collateral to sell in this auction
|
|
paramsMap := make(map[string]types.CollateralParams)
|
|
params := k.GetParams(ctx).CollateralParams
|
|
for _, cp := range params {
|
|
paramsMap[cp.Denom] = cp
|
|
}
|
|
collateralParams, found := paramsMap[collateralDenom]
|
|
if !found {
|
|
return 0, sdk.ErrInternal("collateral denom not found")
|
|
}
|
|
collateralToSell := sdk.MinInt(cdp.CollateralAmount, collateralParams.AuctionSize)
|
|
// Calculate the corresponding maximum amount of stable coin to raise TODO test maths
|
|
stableToRaise := sdk.NewDecFromInt(collateralToSell).Quo(sdk.NewDecFromInt(cdp.CollateralAmount)).Mul(sdk.NewDecFromInt(cdp.Debt)).RoundInt()
|
|
|
|
// Seize the collateral and debt from the CDP
|
|
err := k.partialSeizeCDP(ctx, owner, collateralDenom, collateralToSell, stableToRaise)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Start "forward reverse" auction type
|
|
lot := sdk.NewCoin(cdp.CollateralDenom, collateralToSell)
|
|
maxBid := sdk.NewCoin(k.cdpKeeper.GetStableDenom(), stableToRaise)
|
|
auctionID, err := k.auctionKeeper.StartForwardReverseAuction(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), lot, maxBid, owner)
|
|
if err != nil {
|
|
panic(err) // TODO how can errors here be handled to be safe with the state update in PartialSeizeCDP?
|
|
}
|
|
return auctionID, nil
|
|
}
|
|
|
|
// StartDebtAuction sells off minted gov coin to raise set amounts of stable coin.
|
|
// Known as Vow.flop in maker
|
|
// result: minted gov coin moved to highest bidder, stable coin moved to moduleAccount
|
|
func (k Keeper) StartDebtAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
|
|
|
|
// Ensure amount of seized stable coin is 0 (ie Joy = 0)
|
|
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
|
|
if !stableCoins.IsZero() {
|
|
return 0, sdk.ErrInternal("debt auction cannot be started as there is outstanding stable coins")
|
|
}
|
|
|
|
// check the seized debt is above a threshold
|
|
params := k.GetParams(ctx)
|
|
seizedDebt := k.GetSeizedDebt(ctx)
|
|
if seizedDebt.Available().LT(params.DebtAuctionSize) {
|
|
return 0, sdk.ErrInternal("not enough seized debt to start an auction")
|
|
}
|
|
// start reverse auction, selling minted gov coin for stable coin
|
|
auctionID, err := k.auctionKeeper.StartReverseAuction(
|
|
ctx,
|
|
k.cdpKeeper.GetLiquidatorAccountAddress(),
|
|
sdk.NewCoin(k.cdpKeeper.GetStableDenom(), params.DebtAuctionSize),
|
|
sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 2^255-1), // TODO is there a way to avoid potentially minting infinite gov coin?
|
|
)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
// Record amount of debt sent for auction. Debt can only be reduced in lock step with reducing stable coin
|
|
seizedDebt.SentToAuction = seizedDebt.SentToAuction.Add(params.DebtAuctionSize)
|
|
k.setSeizedDebt(ctx, seizedDebt)
|
|
return auctionID, nil
|
|
}
|
|
|
|
// With no stability and liquidation fees, surplus auctions can never be run.
|
|
// StartSurplusAuction sells off excess stable coin in exchange for gov coin, which is burned
|
|
// Known as Vow.flap in maker
|
|
// result: stable coin removed from module account (eventually to buyer), gov coin transferred to module account
|
|
// func (k Keeper) StartSurplusAuction(ctx sdk.Context) (auction.ID, sdk.Error) {
|
|
|
|
// // TODO ensure seized debt is 0
|
|
|
|
// // check there is enough surplus to be sold
|
|
// surplus := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
|
|
// if surplus.LT(SurplusAuctionSize) {
|
|
// return 0, sdk.ErrInternal("not enough surplus stable coin to start an auction")
|
|
// }
|
|
// // start normal auction, selling stable coin
|
|
// auctionID, err := k.auctionKeeper.StartForwardAuction(
|
|
// ctx,
|
|
// k.cdpKeeper.GetLiquidatorAccountAddress(),
|
|
// sdk.NewCoin(k.cdpKeeper.GetStableDenom(), SurplusAuctionSize),
|
|
// sdk.NewInt64Coin(k.cdpKeeper.GetGovDenom(), 0),
|
|
// )
|
|
// if err != nil {
|
|
// return 0, err
|
|
// }
|
|
// // Starting the auction will remove coins from the account, so they don't need modified here.
|
|
// return auctionID, nil
|
|
// }
|
|
|
|
// PartialSeizeCDP seizes some collateral and debt from an under-collateralized CDP.
|
|
func (k Keeper) partialSeizeCDP(ctx sdk.Context, owner sdk.AccAddress, collateralDenom string, collateralToSeize sdk.Int, debtToSeize sdk.Int) sdk.Error { // aka Cat.bite
|
|
// Seize debt and collateral in the cdp module. This also validates the inputs.
|
|
err := k.cdpKeeper.PartialSeizeCDP(ctx, owner, collateralDenom, collateralToSeize, debtToSeize)
|
|
if err != nil {
|
|
return err // cdp could be not found, or not under collateralized, or inputs invalid
|
|
}
|
|
|
|
// increment the total seized debt (Awe) by cdp.debt
|
|
seizedDebt := k.GetSeizedDebt(ctx)
|
|
seizedDebt.Total = seizedDebt.Total.Add(debtToSeize)
|
|
k.setSeizedDebt(ctx, seizedDebt)
|
|
|
|
// add cdp.collateral amount of coins to the moduleAccount (so they can be transferred to the auction later)
|
|
coins := sdk.NewCoins(sdk.NewCoin(collateralDenom, collateralToSeize))
|
|
_, err = k.bankKeeper.AddCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), coins)
|
|
if err != nil {
|
|
panic(err) // TODO this shouldn't happen?
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SettleDebt removes equal amounts of debt and stable coin from the liquidator's reserves (and also updates the global debt in the cdp module).
|
|
// This is called in the handler when a debt or surplus auction is started
|
|
// TODO Should this be called with an amount, rather than annihilating the maximum?
|
|
func (k Keeper) SettleDebt(ctx sdk.Context) sdk.Error {
|
|
// Calculate max amount of debt and stable coins that can be settled (ie annihilated)
|
|
debt := k.GetSeizedDebt(ctx)
|
|
stableCoins := k.bankKeeper.GetCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress()).AmountOf(k.cdpKeeper.GetStableDenom())
|
|
settleAmount := sdk.MinInt(debt.Total, stableCoins)
|
|
|
|
// Call cdp module to reduce GlobalDebt. This can fail if genesis not set
|
|
err := k.cdpKeeper.ReduceGlobalDebt(ctx, settleAmount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Decrement total seized debt (also decrement from SentToAuction debt)
|
|
updatedDebt, err := debt.Settle(settleAmount)
|
|
if err != nil {
|
|
return err // this should not error in this context
|
|
}
|
|
k.setSeizedDebt(ctx, updatedDebt)
|
|
|
|
// Subtract stable coin from moduleAccout
|
|
k.bankKeeper.SubtractCoins(ctx, k.cdpKeeper.GetLiquidatorAccountAddress(), sdk.Coins{sdk.NewCoin(k.cdpKeeper.GetStableDenom(), settleAmount)})
|
|
return nil
|
|
}
|
|
|
|
// ---------- Store Wrappers ----------
|
|
|
|
func (k Keeper) getSeizedDebtKey() []byte {
|
|
return []byte("seizedDebt")
|
|
}
|
|
func (k Keeper) GetSeizedDebt(ctx sdk.Context) types.SeizedDebt {
|
|
store := ctx.KVStore(k.key)
|
|
bz := store.Get(k.getSeizedDebtKey())
|
|
if bz == nil {
|
|
// TODO make initial seized debt and CDPs configurable at genesis, then panic here if not found
|
|
bz = k.cdc.MustMarshalBinaryLengthPrefixed(types.SeizedDebt{
|
|
Total: sdk.ZeroInt(),
|
|
SentToAuction: sdk.ZeroInt()})
|
|
}
|
|
var seizedDebt types.SeizedDebt
|
|
k.cdc.MustUnmarshalBinaryLengthPrefixed(bz, &seizedDebt)
|
|
return seizedDebt
|
|
}
|
|
func (k Keeper) setSeizedDebt(ctx sdk.Context, debt types.SeizedDebt) {
|
|
store := ctx.KVStore(k.key)
|
|
bz := k.cdc.MustMarshalBinaryLengthPrefixed(debt)
|
|
store.Set(k.getSeizedDebtKey(), bz)
|
|
}
|