Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: liquidation directly rewards base assets #1118

Merged
merged 71 commits into from
Aug 4, 2022
Merged

Conversation

toteki
Copy link
Member

@toteki toteki commented Jul 7, 2022

Description

Significant Change:

  • Instead of receiving uToken rewards on MsgLiquidate, liquidator receives equivalent base assets.

Reasoning:

We're about to add MaxCollateralUtilization which restricts MsgWithdraw at critical times, so AvailableSupply can be used for liquidation.

But if liquidators receive their rewards as uTokens, then the MsgWithdraw restriction will apply to them just like any other user, and they will be unable to receive their base tokens.

Switching to base assets received eliminates makes MaxCollateralUtilization work as intended, and also simplifies the MsgLiquidate -> MsgWithdraw -> IBC (to sell reward) liquidation workflow to just MsgLiquidate -> IBC


API Breaking:

  • Reverts MsgLiquidate's reward sdk.Coin field to reward_denom string

Reasoning:

That field was for rare cases where liquidators do not trust the umee x/oracle and want to put their own price floors. Such cases should be handled off-chain (just query prices) rather than adding a computation and required field for all users. See #828


closes: #898
closes: #828

relevant to #831 - because rounding up repayment and reward eliminates most "dust" liquidation remainders


Author Checklist

All items are required. Please add a note to the item if the item is not applicable and
please add links to any relevant follow up issues.

I have...

  • included the correct type prefix in the PR title
  • added ! to the type prefix if API or client breaking change
  • added appropriate labels to the PR
  • targeted the correct branch (see PR Targeting)
  • provided a link to the relevant issue or specification
  • added a changelog entry to CHANGELOG.md
  • included comments for documenting Go code
  • updated the relevant documentation or specification
  • reviewed "Files changed" and left comments if necessary
  • confirmed all CI checks have passed

Reviewers Checklist

All items are required. Please add a note if the item is not applicable and please add
your handle next to the items reviewed if you only reviewed selected items.

I have...

  • confirmed the correct type prefix in the PR title
  • confirmed all author checklist items have been addressed
  • reviewed state machine logic
  • reviewed API design and naming
  • reviewed documentation is accurate
  • reviewed tests and test coverage
  • manually tested (if applicable)

@toteki toteki added this to the v3 - Calypso milestone Jul 7, 2022
@codecov-commenter
Copy link

codecov-commenter commented Jul 7, 2022

Codecov Report

Merging #1118 (3169527) into main (e1a6f1f) will decrease coverage by 0.36%.
The diff coverage is 37.65%.

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1118      +/-   ##
==========================================
- Coverage   51.54%   51.17%   -0.37%     
==========================================
  Files          65       67       +2     
  Lines        6585     6654      +69     
==========================================
+ Hits         3394     3405      +11     
- Misses       2928     3009      +81     
+ Partials      263      240      -23     
Impacted Files Coverage Δ
x/leverage/client/cli/tx.go 0.00% <0.00%> (ø)
x/leverage/keeper/keeper.go 40.53% <0.00%> (-3.40%) ⬇️
x/leverage/keeper/msg_server.go 0.84% <0.00%> (ø)
x/leverage/keeper/token.go 78.26% <ø> (+7.35%) ⬆️
x/leverage/types/token.go 69.86% <0.00%> (-5.14%) ⬇️
x/leverage/types/tx.go 0.00% <0.00%> (ø)
x/leverage/keeper/liquidate.go 38.77% <38.77%> (ø)
x/leverage/keeper/validate.go 57.14% <47.05%> (+5.52%) ⬆️
x/leverage/keeper/borrows.go 77.88% <57.14%> (+0.88%) ⬆️
x/leverage/keeper/oracle.go 67.56% <88.88%> (+0.49%) ⬆️
... and 5 more

@toteki toteki marked this pull request as ready for review July 7, 2022 21:30
@toteki toteki requested review from a team as code owners July 7, 2022 21:30
x/leverage/keeper/keeper.go Outdated Show resolved Hide resolved
x/leverage/keeper/keeper.go Show resolved Hide resolved
x/leverage/keeper/keeper.go Outdated Show resolved Hide resolved
Copy link
Member

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think we should not reduce available_supply by directly giving a tokenA (instead of uToken) to the liquidator. We want available supply to stay high (to optimize collateral_utilization(tokenA)).
So, I think we should:

  • keep the liquidation logic as is (liquidator receives uTokens)
  • round down when distributing rewards.

string liquidator = 1;
string borrower = 2;
cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false];
string reward_denom = 4;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to think more about this change and this PR. In particular, I think this should be repeated Coin, see: #1129

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering the level of mathematical complexity that single-denom liquidations involve (see below), I prefer avoiding sdk.Coins in either repayment or reward.

It does increase the number of MsgLiquidate required for complex positions unfortunately, but keeps the edge cases much more understandable.

Copy link
Member

@robert-zaremba robert-zaremba Jul 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case the complexity is moved to liquidator... he will need to calculate which denom he can liquidate and how much.

We are already adjusting the repayment. Instead we can go one by one of the collateral list and sum up to the value related to the repayment. We could use denom list to make it simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the liquidator can do that - I had a script doing as much during the testnet, and what it does is proceed in order of preference for repay and reward denoms by offering the maximum amount it is willing to repay.

For most borrowers, even with multiple borrowed and collateral types, the first MsgLiquidate brings them back to health, so the liquidator's initial preference for reward and repay denoms is enough.

For borrowers where multiple transactions are needed, the liquidator only needs to follow their ranked order of preferences, because the first transaction will have either exhausted a repay denom or a reward denom by succeeding.

Note: Exhausted a denom can mean:

  • Repaid a borrow completely in one token denom
  • Reduced liquidator's token balance to zero (or consumed maximum offered amount) for a token denom
  • Reduced borrower's collateral position in a uToken denom to zero
  • Exhausted available supply of a reward token

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test verifying if the liquidation of a single asset, even if it doesn't bring the account to a healthy state, is possible?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Liquidation logic is independent of final state (except for the collateral == 0 check for bad debt flagging) so that isn't a problem.

In particular, the test case labeled repayLimited covers this scenario in liquidate_test.go

proto/umee/leverage/v1/tx.proto Show resolved Hide resolved
x/leverage/keeper/keeper.go Outdated Show resolved Hide resolved
x/leverage/keeper/liquidate.go Outdated Show resolved Hide resolved
x/leverage/keeper/math.go Outdated Show resolved Hide resolved
x/leverage/keeper/math.go Outdated Show resolved Hide resolved
x/leverage/keeper/keeper.go Outdated Show resolved Hide resolved
x/leverage/keeper/math.go Outdated Show resolved Hide resolved
x/leverage/keeper/math.go Outdated Show resolved Hide resolved
x/leverage/keeper/math.go Outdated Show resolved Hide resolved
@toteki
Copy link
Member Author

toteki commented Jul 13, 2022

Keep the liquidation logic as is (liquidator receives uTokens)

Suppose we have a situation where collateral utilization has reached its limit. In this case, MsgWithdraw from suppliers is rejected in order to keep the remaining collateral available for liquidation. I believe our discussion was that liquidators need to be able to function in this crisis, which is why collateral utilization needed to be limited in the first place.

However, there is no way to distinguish between a supplier and a liquidator if they both have to use MsgWithdraw. If the liquidator receives uTokens, then they can't sell liquidated collateral (Liquidate -> Withdraw -> IBC to Osmosis) and therefore will prefer not to liquidate at all.

@toteki
Copy link
Member Author

toteki commented Jul 13, 2022

round down when distributing rewards

This change to rounding up is related to a dust problem I witnesses on chain during umeemania. Consider the following scenario

Borrowed 7 uosmo
Collateral 1 u/uatom worth 1.1 uatom
Assume due to exchange rates that 1 ATOM = 8.1 OSMO

Note the microatom and microosmo, which cannot be subdivided further.

The transaction Liquidate(3uosmo -> uatom) will have the following behavior

Rounding Repaid Collateral Reward Problem
Down for all 3 0 0 Collateral dust remains no matter what, liquidation is not incentivized, next liquidate will repeat and repay 3 more uosmo for 0 reward, and bad debt repayment is not reached because collateral never reaches zero
Up for all 3 1 1 Base token reward is slightly (1 micro) higher than if collateral had been input into MsgWithdraw
Up for collateral, Down for reward 3 1 0 Liquidation is not incentivized when targeting collateral dust

The priority for rounding during liquidation in my opinion should be to not leave dust. This is because

  • Bad debt repayment requires collateral to reach exactly zero
  • Collateral dust in a denomination makes that collateral denomination appear to be an eligible RewardDenom during the next liquidation if multiple liquidations happen over time. This could lead to loops where bots cannot eliminate the last 1 collateral no matter how many times they liquidate.

That said, I'll look into the Collateral -> Reward (uToken -> Token) rounding to make sure it can't be expoited (e.g. repeated MsgLiquidate on a controlled borrower, each time adding 1 collateral, to drain funds from the module by rounding up uToken exchange rate.

Since that looks like it might be the case, I'll change the code to round up for collateral consumed, and down for base tokens rewarded. I'll await further discussion on the rest.

edit: Use of ExchangeUToken rounds base token reward down - and then the final ReduceProportionally in the case of available base assets reaching zero rounds everything except base reward up (base reward stays flat). Looks like the current code works.

The above was a scenario where both the borrowed and the collateral value were tiny "dust" quantities, and the collateral denom was worth more in USD than the borrowed denom. Here are my opinions on other permutations:

Borrowed Size Collateral Size Price Relation Opinion
Dust Dust Collateral > Borrow Tradeoff discussed above
Dust Dust Borrow > Collateral Less collateral dust danger - nonzero repay amount will always allow nonzero collateral due to prices. No borrow dust danger as long as Repaid -> Collateral rounds up (as zero collateral consumed would cause position to be exploitable)
Large Dust any Priority is eliminating collateral dust, which is possible as long as Repaid -> Collateral rounds up.
Large Large any Collateral dust is only an issue if fully liquidating the larger collateral amount. Avoided if Repaid -> Collateral rounds up. Borrow dust will not occur as repaid amount is a liquidator controlled input, absent other limiting factors.

toteki and others added 3 commits July 13, 2022 07:51
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
Copy link
Member

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Part 1. Will finish tomorrow.

  • need to validate (in ValidateBasic) that MsgLiquidate.RewardDenom is not a uToken.... On the other hand, we should support uToken in case liquidator will like to keep uTokens - but that's probably a case for another task. Please confirm.

string liquidator = 1;
string borrower = 2;
cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false];
string reward_denom = 4;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test verifying if the liquidation of a single asset, even if it doesn't bring the account to a healthy state, is possible?

proto/umee/leverage/v1/tx.proto Outdated Show resolved Hide resolved
x/leverage/client/cli/tx.go Show resolved Hide resolved
x/leverage/client/cli/tx.go Show resolved Hide resolved
x/leverage/keeper/borrows.go Show resolved Hide resolved
x/leverage/keeper/filter.go Outdated Show resolved Hide resolved
Co-authored-by: Robert Zaremba <robert@zaremba.ch>
@toteki
Copy link
Member Author

toteki commented Aug 3, 2022

need to validate (in ValidateBasic) that MsgLiquidate.RewardDenom is not a uToken

In the current code, this is handled in keeper.go

if err := k.validateAcceptedDenom(ctx, rewardDenom); err != nil {
		return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err
	}

After #1202 both options will be valid (but lead to different outcomes)

Copy link
Member

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pre-approving. Left few comments.
Importantly, let's verify if our interest rate logic is applied correctly. In the setBorrow and getBorrow I don't see how time ingredient is applied. Interest rates are defined over year, so need to do few checks:

  • to not apply the interest rate in the same block twice
  • add time component to set/get Borrow.

x/leverage/keeper/keeper.go Outdated Show resolved Hide resolved
x/leverage/keeper/keeper.go Show resolved Hide resolved
if remainingCollateral.IsZero() {
for _, coin := range k.GetBorrowerBorrows(ctx, borrowerAddr) {
// set a bad debt flag for each borrowed denom
if err := k.setBadDebtAddress(ctx, borrowerAddr, coin.Denom, true); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why don't we store amount of tokens in bad debt?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bad debt is currently just something to iterate over, so storing 0x01 is sufficient.

We could store the amount - but then it would be redundant with adjusted borrow which also stores amount. Adjusted borrow would also continue to accrue interest while this does not. Additionally, any operation (e.g. partial repay) that updated one of the two would have to keep the other in sync, while the bad debt existed.

Overall, it's simpler to avoid storing amount here.

x/leverage/keeper/math_test.go Outdated Show resolved Hide resolved
x/leverage/keeper/liquidate.go Show resolved Hide resolved
x/leverage/keeper/liquidate.go Show resolved Hide resolved
x/leverage/keeper/liquidate.go Outdated Show resolved Hide resolved
@toteki
Copy link
Member Author

toteki commented Aug 4, 2022

verify if our interest rate logic is applied correctly.

Overall, try to keep discussions within the bounds of the PR.

The interest rate logic you're looking for is in EndBlock. This only happens once per block, and increases InterestScalar for each denom. The math and functionality has been audited during the Runtime Verification audit.

@robert-zaremba
Copy link
Member

Overall, try to keep discussions within the bounds of the PR.

This PR uses get/set Borrow. So it make sense to look there and eventually could be fixed in other PR if needed.

@mergify mergify bot merged commit af827ba into main Aug 4, 2022
@mergify mergify bot deleted the adam/liquidate branch August 4, 2022 15:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants