diff --git a/x/leverage/types/position.go b/x/leverage/types/position.go index aec2902ed7..a5a8f08848 100644 --- a/x/leverage/types/position.go +++ b/x/leverage/types/position.go @@ -256,12 +256,40 @@ func (ap *AccountPosition) Limit() sdk.Dec { // compute limit due to collateral weights limit := ap.totalBorrowLimit() + // if no borrows, borrow factor limits will not apply + borrowedValue := ap.BorrowedValue() + if borrowedValue.IsZero() { + return limit + } + // compute limit due to borrow factors usage := ap.totalCollateralUsage() - avgWeight := ap.normalBorrowLimit().Quo(collateralValue) unusedCollateralValue := ap.CollateralValue().Sub(usage) // can be negative + + var avgWeight sdk.Dec + if unusedCollateralValue.IsPositive() { + // if user is below limit, unused collateral can be borrowed against at its average collateral weight at most + avgWeight = ap.averageWeight(ap.collateralValue) + } else { + // if user if above limit, overused collateral is being borrowed against at borrow factor + avgWeight = borrowedValue.Quo(usage) + } borrowFactorLimit := ap.BorrowedValue().Add(unusedCollateralValue.Mul(avgWeight)) + if len(ap.collateralValue) == 1 && ap.collateralValue[0].Denom == "FFFF" { + if len(ap.borrowedValue) == 1 && ap.borrowedValue[0].Denom == "HHHH" { + fmt.Printf("%s -> %s (%t)\n %s, %s\n >>> %s\n", + ap.collateralValue, + ap.borrowedValue, + ap.isForLiquidation, + limit, + borrowFactorLimit, + usage, + ) + } + } + + // return the minimum of the two limits return sdk.MinDec(limit, borrowFactorLimit) } @@ -300,6 +328,20 @@ func (ap *AccountPosition) tokenWeight(denom string) sdk.Dec { return sdk.ZeroDec() } +// averageWeight gets the weighted average collateral weight (or liquidation threshold) of a set of tokens +func (ap *AccountPosition) averageWeight(coins sdk.DecCoins) sdk.Dec { + if coins.IsZero() { + return sdk.OneDec() + } + valueSum := sdk.ZeroDec() + weightSum := sdk.ZeroDec() + for _, c := range coins { + weightSum = weightSum.Add(c.Amount.Mul(ap.tokenWeight(c.Denom))) + valueSum = valueSum.Add(c.Amount) + } + return weightSum.Quo(valueSum) +} + // borrowFactor gets a token's collateral weight or liquidation threshold (or minimumBorrowFactor if greater) // if the token is registered, else zero. func (ap *AccountPosition) borrowFactor(denom string) sdk.Dec { diff --git a/x/leverage/types/position_test.go b/x/leverage/types/position_test.go index fa3dc954da..6125aa82f2 100644 --- a/x/leverage/types/position_test.go +++ b/x/leverage/types/position_test.go @@ -164,17 +164,17 @@ func TestBorrowLimit(t *testing.T) { { // multiple assets, one with zero weight, at borrow limit sdk.NewDecCoins( - coin.Dec("AAAA", "100"), - coin.Dec("GGGG", "100"), - coin.Dec("IIII", "100"), + coin.Dec("AAAA", "100"), // $10, $15 + coin.Dec("GGGG", "100"), // $70, $75 + coin.Dec("IIII", "100"), // $0, $95 ), sdk.NewDecCoins( - coin.Dec("GGGG", "80"), + coin.Dec("GGGG", "80"), // uses $114.2 or $106.6 of collateral ), - // effectiveness of I collateral is reduced to due to G liquidation threshold, thus leading - // to a lower liquidation threshold than "simple AGI" test case above + // effectiveness of I collateral would be reduced to due to G liquidation threshold, + // but ordinary liquidation threshold is already more restrictive than borrow factor here "80.00", - "165.00", + "185.00", "AGI -> G at borrow limit", }, { @@ -185,12 +185,12 @@ func TestBorrowLimit(t *testing.T) { coin.Dec("IIII", "100"), ), sdk.NewDecCoins( - coin.Dec("GGGG", "165"), + coin.Dec("GGGG", "185"), ), // significantly over borrow limit, so calculation subtracts value of unpaired borrows - // from total borrowed value to determine borrow limit + // from total borrowed value to determine borrow limit to arrive at the same result "80.00", - "165.00", + "185.00", "AGI -> G at liquidation threshold", }, { @@ -206,7 +206,7 @@ func TestBorrowLimit(t *testing.T) { // significantly over borrow limit and liquidation threshold, but calculation still reaches // the same values for them "80.00", - "165.00", + "185.00", "AGI -> G above liquidation threshold", }, { @@ -250,6 +250,36 @@ func TestBorrowLimit(t *testing.T) { "53.00", "F -> A", }, + { + // single asset with unused special pair (borrowFactor reducing weight, minimumBorrowFactor, at limit) + sdk.NewDecCoins( + coin.Dec("FFFF", "100"), + ), + sdk.NewDecCoins( + coin.Dec("AAAA", "50"), + ), + // 50 A consumes 100 F collateral (weight 0.5 due to MinimumBorrowFactor) + // the F <-> H special pair has no effect + "50.00", + "50.00", + "F -> A", + }, + { + // single asset with unused special pair (borrowFactor, minimumBorrowFactor, over limits) + sdk.NewDecCoins( + coin.Dec("FFFF", "100"), + ), + sdk.NewDecCoins( + coin.Dec("AAAA", "80"), + ), + // 80 A would consume 160 F collateral (weight 0.5 due to MinimumBorrowFactor), + // meanwhile 100F on its own would have 60, 65 borrow limit and liquidation threshold. + // The calculation works backwards from the 160/80 collateral usage to find the limit at 100 + // the F <-> H special pair has no effect + "50.00", + "50.00", + "F -> A over limits", + }, { // single asset with special pair in effect sdk.NewDecCoins( @@ -291,7 +321,7 @@ func TestBorrowLimit(t *testing.T) { ), // 60 H consumes all 100 F collateral (weight 0.6 due to Special Pair). // A remaining 20H is unpaired borrowed value. Borrow limit equals value minus unpaired. - // Meanwhile, 80A consumes 100 F collateral (liquidation threshold 0.8 due to special pair). + // Meanwhile, 80 H consumes 100 F collateral (liquidation threshold 0.8 due to special pair). // Liquidation threshold is exactly borrowed value. "60.00", "80.00",