Skip to content

Commit

Permalink
feat: split collateralWeight into collateralWeight and liquidationThr…
Browse files Browse the repository at this point in the history
…eshold (#627)

* collateral damage

* comment++

* sp

* comment++

* param name

* sp

* lint

* test fix

* Apply suggestions from code review

Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>

* lint++

* rename

* rename++

Co-authored-by: Aleksandr Bezobchuk <alexanderbez@users.noreply.github.com>
  • Loading branch information
toteki and alexanderbez authored Mar 9, 2022
1 parent 8a794c8 commit d96b680
Show file tree
Hide file tree
Showing 22 changed files with 284 additions and 100 deletions.
1 change: 1 addition & 0 deletions app/beta/test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ func IntegrationTestNetworkConfig() network.Config {
Exponent: 6,
ReserveFactor: sdk.MustNewDecFromStr("0.100000000000000000"),
CollateralWeight: sdk.MustNewDecFromStr("0.050000000000000000"),
LiquidationThreshold: sdk.MustNewDecFromStr("0.050000000000000000"),
BaseBorrowRate: sdk.MustNewDecFromStr("0.020000000000000000"),
KinkBorrowRate: sdk.MustNewDecFromStr("0.200000000000000000"),
MaxBorrowRate: sdk.MustNewDecFromStr("1.50000000000000000"),
Expand Down
23 changes: 16 additions & 7 deletions proto/umee/leverage/v1beta1/leverage.proto
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,19 @@ message Token {
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"collateral_weight\""
];

// The liquidation_threshold defines what amount of the total value of the asset
// can contribute to a user's liquidation threshold (above which they become
// eligible for liquidation).
string liquidation_threshold = 4 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"liquidation_threshold\""
];

// The base_borrow_rate defines the base interest rate for borrowing this
// asset.
string base_borrow_rate = 4 [
string base_borrow_rate = 5 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"base_borrow_rate\""
Expand All @@ -67,38 +76,38 @@ message Token {
// The kink_borrow_rate defines the interest rate for borrowing this
// asset when utilization is at the 'kink' utilization value as defined
// on the utilization:interest graph.
string kink_borrow_rate = 5 [
string kink_borrow_rate = 6 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"kink_borrow_rate\""
];

// The max_borrow_rate defines the interest rate for borrowing this
// asset (seen when utilization is 100%).
string max_borrow_rate = 6 [
string max_borrow_rate = 7 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"max_borrow_rate\""
];

// The kink_utilization_rate defines the borrow utilization rate for this
// asset where the 'kink' on the utilization:interest graph occurs.
string kink_utilization_rate = 7 [
string kink_utilization_rate = 8 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"kink_utilization_rate\""
];

// The liquidation_incentive determines the portion of bonus collateral of
// a token type liquidators receive as a liquidation reward.
string liquidation_incentive = 8 [
string liquidation_incentive = 9 [
(gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec",
(gogoproto.nullable) = false,
(gogoproto.moretags) = "yaml:\"liquidation_incentive\""
];

// The symbol_denom and exponent are solely used to update the oracle's accept
// list of allowed tokens.
string symbol_denom = 9 [(gogoproto.moretags) = "yaml:\"symbol_denom\""];
uint32 exponent = 10 [(gogoproto.moretags) = "yaml:\"exponent\""];
string symbol_denom = 10 [(gogoproto.moretags) = "yaml:\"symbol_denom\""];
uint32 exponent = 11 [(gogoproto.moretags) = "yaml:\"exponent\""];
}
4 changes: 3 additions & 1 deletion starport.ci.beta.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ genesis:
exponent: 6
reserve_factor: "0.100000000000000000"
collateral_weight: "0.050000000000000000"
liquidation_threshold: "0.050000000000000000"
base_borrow_rate: "0.020000000000000000"
kink_borrow_rate: "0.200000000000000000"
max_borrow_rate: "1.50000000000000000"
Expand All @@ -23,7 +24,8 @@ genesis:
symbol_denom: "ATOM"
exponent: 6
reserve_factor: "0.100000000000000000"
collateral_weight: "0.050000000000000000"
collateral_weight: "0.550000000000000000"
liquidation_threshold: "0.600000000000000000"
base_borrow_rate: "0.020000000000000000"
kink_borrow_rate: "0.200000000000000000"
max_borrow_rate: "1.50000000000000000"
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/e2e_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ func (s *IntegrationTestSuite) initGenesis() {
Exponent: 6,
ReserveFactor: sdk.MustNewDecFromStr("0.100000000000000000"),
CollateralWeight: sdk.MustNewDecFromStr("0.050000000000000000"),
LiquidationThreshold: sdk.MustNewDecFromStr("0.050000000000000000"),
BaseBorrowRate: sdk.MustNewDecFromStr("0.020000000000000000"),
KinkBorrowRate: sdk.MustNewDecFromStr("0.200000000000000000"),
MaxBorrowRate: sdk.MustNewDecFromStr("1.50000000000000000"),
Expand Down
1 change: 1 addition & 0 deletions x/leverage/client/cli/proposal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ func TestParseUpdateRegistryProposal(t *testing.T) {
"base_denom": "uumee",
"reserve_factor": "0.1",
"collateral_weight": "0.05",
"liquidation_threshold": "0.05",
"base_borrow_rate": "0.02",
"kink_borrow_rate": "0.2",
"max_borrow_rate": "1.5",
Expand Down
1 change: 1 addition & 0 deletions x/leverage/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ Where proposal.json contains:
"base_denom": "uumee",
"reserve_factor": "0.1",
"collateral_weight": "0.05",
"liquidation_threshold": "0.05",
"base_borrow_rate": "0.02",
"kink_borrow_rate": "0.2",
"max_borrow_rate": "1.5",
Expand Down
1 change: 1 addition & 0 deletions x/leverage/client/tests/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ func (s *IntegrationTestSuite) TestQueryAllRegisteredTokens() {
Exponent: 6,
ReserveFactor: sdk.MustNewDecFromStr("0.1"),
CollateralWeight: sdk.MustNewDecFromStr("0.05"),
LiquidationThreshold: sdk.MustNewDecFromStr("0.05"),
BaseBorrowRate: sdk.MustNewDecFromStr("0.02"),
KinkBorrowRate: sdk.MustNewDecFromStr("0.2"),
MaxBorrowRate: sdk.MustNewDecFromStr("1.5"),
Expand Down
8 changes: 5 additions & 3 deletions x/leverage/client/tests/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ func (s *IntegrationTestSuite) UpdateRegistry(
)
}

// updateCollateralWeight modifies the collateral weight of a registered token identified by baseDenom.
// updateCollateralWeight modifies the collateral weight and liquidation threshold of a registered
// token identified by baseDenom.
func updateCollateralWeight(s *IntegrationTestSuite, baseDenom string, collateralWeight sdk.Dec) {
val := s.network.Validators[0]
clientCtx := s.network.Validators[0].ClientCtx
Expand All @@ -156,11 +157,12 @@ func updateCollateralWeight(s *IntegrationTestSuite, baseDenom string, collatera
resp := &types.QueryRegisteredTokensResponse{}
s.Require().NoError(clientCtx.Codec.UnmarshalJSON(out.Bytes(), resp), out.String())

// Replace the collateral weight of the selected token with the new value
// Replace the liquidation threshold and collateral weight of the selected token with the new value
newTokens := resp.GetRegistry()
for i := range newTokens {
if newTokens[i].BaseDenom == baseDenom {
newTokens[i].CollateralWeight = collateralWeight
newTokens[i].LiquidationThreshold = collateralWeight
}
}

Expand All @@ -172,7 +174,7 @@ func updateCollateralWeight(s *IntegrationTestSuite, baseDenom string, collatera
clientCtx,
types.NewUpdateRegistryProposal(
fmt.Sprintf("collateral weight update - %d", proposalCounter),
"update collateral weight to "+collateralWeight.String(),
fmt.Sprintf("update collateral weight and liquidation threshold to %s", collateralWeight.String()),
newTokens,
),
sdk.NewCoins(sdk.NewCoin(app.BondDenom, govtypes.DefaultMinDepositTokens)),
Expand Down
32 changes: 32 additions & 0 deletions x/leverage/keeper/borrows.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,38 @@ func (k Keeper) CalculateBorrowLimit(ctx sdk.Context, collateral sdk.Coins) (sdk
return limit, nil
}

// CalculateLiquidationLimit uses the price oracle to determine the liquidation limit
// (in USD) provided by collateral sdk.Coins, using each token's uToken exchange rate and
// liquidation threshold. An error is returned if any input coins are not uTokens or if value
// calculation fails.
func (k Keeper) CalculateLiquidationLimit(ctx sdk.Context, collateral sdk.Coins) (sdk.Dec, error) {
threshold := sdk.ZeroDec()

for _, coin := range collateral {
// convert uToken collateral to base assets
baseAsset, err := k.ExchangeUToken(ctx, coin)
if err != nil {
return sdk.ZeroDec(), err
}

// get USD value of base assets
value, err := k.TokenValue(ctx, baseAsset)
if err != nil {
return sdk.ZeroDec(), err
}

weight, err := k.GetLiquidationThreshold(ctx, baseAsset.Denom)
if err != nil {
return sdk.ZeroDec(), err
}

// add each liquidation threshold value to total
threshold = threshold.Add(value.Mul(weight))
}

return threshold, nil
}

// setBadDebtAddress sets or deletes an address in a denom's list of addresses with unpaid bad debt.
func (k Keeper) setBadDebtAddress(ctx sdk.Context, addr sdk.AccAddress, denom string, hasDebt bool) error {
if err := sdk.ValidateDenom(denom); err != nil {
Expand Down
1 change: 1 addition & 0 deletions x/leverage/keeper/borrows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func (s *IntegrationTestSuite) TestDeriveBorrowUtilization() {
BaseDenom: umeeDenom,
ReserveFactor: sdk.MustNewDecFromStr("0"),
CollateralWeight: sdk.MustNewDecFromStr("1"),
LiquidationThreshold: sdk.MustNewDecFromStr("1"),
BaseBorrowRate: sdk.MustNewDecFromStr("0"),
KinkBorrowRate: sdk.MustNewDecFromStr("0"),
MaxBorrowRate: sdk.MustNewDecFromStr("0"),
Expand Down
10 changes: 5 additions & 5 deletions x/leverage/keeper/iter.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,15 +196,15 @@ func (k Keeper) GetEligibleLiquidationTargets(ctx sdk.Context) ([]sdk.AccAddress
return err
}

// use collateral weights to compute borrow limit from enabled collateral
borrowLimit, err := k.CalculateBorrowLimit(ctx, collateral)
// compute liquidation limit from enabled collateral
liquidationLimit, err := k.CalculateLiquidationLimit(ctx, collateral)
if err != nil {
return err
}

// if the borrowLimit is smaller then the borrowValue
// the address is eligible to liquidation
if borrowLimit.LT(borrowValue) {
// If liquidation limit is smaller than borrowed value then the
// address is eligible for liquidation.
if liquidationLimit.LT(borrowValue) {
liquidationTargets = append(liquidationTargets, addr)
}

Expand Down
21 changes: 13 additions & 8 deletions x/leverage/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ func (k Keeper) GetCollateralSetting(ctx sdk.Context, borrowerAddr sdk.AccAddres
// LiquidateBorrow attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange
// for a selected denomination of uToken collateral, specified by its associated token denom. The liquidator
// may also specify a minimum reward amount, again in base token denom that will be adjusted by uToken exchange
// rate, they would accept for the specified repayment. If the borrower is not over their borrow limit, or
// rate, they would accept for the specified repayment. If the borrower is not over their liquidation limit, or
// the repayment or reward denominations are invalid, an error is returned. If the attempted repayment
// is greater than the amount owed or the maximum that can be repaid due to parameters (close factor)
// then a partial liquidation, equal to the maximum valid amount, is performed. The same occurs if the
Expand Down Expand Up @@ -399,20 +399,20 @@ func (k Keeper) LiquidateBorrow(
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

// use collateral weights to compute borrow limit from enabled collateral
borrowLimit, err := k.CalculateBorrowLimit(ctx, collateral)
// compute liquidation limit from enabled collateral
liquidationLimit, err := k.CalculateLiquidationLimit(ctx, collateral)
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}

// confirm borrower's eligibility for liquidation
if borrowLimit.GTE(borrowValue) {
if liquidationLimit.GTE(borrowValue) {
return sdk.ZeroInt(), sdk.ZeroInt(), sdkerrors.Wrap(types.ErrLiquidationIneligible, borrowerAddr.String())
}

// get reward-specific incentive and dynamic close factor
baseRewardDenom := desiredReward.Denom
liquidationIncentive, closeFactor, err := k.LiquidationParams(ctx, baseRewardDenom, borrowValue, borrowLimit)
liquidationIncentive, closeFactor, err := k.LiquidationParams(ctx, baseRewardDenom, borrowValue, liquidationLimit)
if err != nil {
return sdk.ZeroInt(), sdk.ZeroInt(), err
}
Expand Down Expand Up @@ -539,10 +539,15 @@ func (k Keeper) LiquidateBorrow(
}

// LiquidationParams computes dynamic liquidation parameters based on collateral denomination,
// borrowed value, and borrow limit. Returns liquidationIncentive (the ratio of bonus collateral
// borrowed value, and liquidation limit. Returns liquidationIncentive (the ratio of bonus collateral
// awarded during Liquidate transactions, and closeFactor (the fraction of a borrower's total
// borrowed value that can be repaid by a liquidator in a single liquidation event.)
func (k Keeper) LiquidationParams(ctx sdk.Context, reward string, borrowed, limit sdk.Dec) (sdk.Dec, sdk.Dec, error) {
func (k Keeper) LiquidationParams(
ctx sdk.Context,
reward string,
borrowed sdk.Dec,
limit sdk.Dec,
) (sdk.Dec, sdk.Dec, error) {
if borrowed.IsNegative() {
return sdk.ZeroDec(), sdk.ZeroDec(), sdkerrors.Wrap(types.ErrBadValue, borrowed.String())
}
Expand All @@ -556,7 +561,7 @@ func (k Keeper) LiquidationParams(ctx sdk.Context, reward string, borrowed, limi
return sdk.ZeroDec(), sdk.ZeroDec(), err
}

// special case: If borrow limit is zero, close factor is always 1
// special case: If liquidation limit is zero, close factor is always 1
if limit.IsZero() {
return liquidationIncentive, sdk.OneDec(), nil
}
Expand Down
Loading

0 comments on commit d96b680

Please sign in to comment.