From 8277b19a6113cdfa27d31c64bf55cd722f347ae6 Mon Sep 17 00:00:00 2001 From: toteki <63419657+toteki@users.noreply.github.com> Date: Wed, 10 Aug 2022 14:34:51 -0700 Subject: [PATCH] feat: Add DirectLiquidationFee and uToken liquidation option (#1222) ## Description - Adds proto for `DirectLiquidationFee` as well as actually implementing the behavior. Misc additions: - Moved token <-> uToken denom functions to `types` package. Previously there were similar functions in both `types` and `keeper`. - Improved operations tests in simulation package and fixed a hidden bug (fees making messages fail if the random generator hit certain values, due to some transactions not using `CoinsSpentInMsg`) closes: #1158 --- ### 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... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] added `!` to the type prefix if API or client breaking change - [x] added appropriate labels to the PR - [x] targeted the correct branch (see [PR Targeting](https://github.com/umee-network/umee/blob/main/CONTRIBUTING.md#pr-targeting)) - [x] provided a link to the relevant issue or specification - [x] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [x] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [x] 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](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) 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) --- CHANGELOG.md | 2 + proto/umee/leverage/v1/leverage.proto | 8 + proto/umee/leverage/v1/tx.proto | 7 +- swagger/swagger.yaml | 638 +++++++-------------- x/leverage/client/tests/tests.go | 2 +- x/leverage/keeper/borrows.go | 11 + x/leverage/keeper/borrows_test.go | 6 +- x/leverage/keeper/collateral.go | 30 +- x/leverage/keeper/collateral_test.go | 6 +- x/leverage/keeper/exchange_rate.go | 6 +- x/leverage/keeper/filter.go | 10 + x/leverage/keeper/grpc_query.go | 2 +- x/leverage/keeper/grpc_query_test.go | 2 +- x/leverage/keeper/iter.go | 2 +- x/leverage/keeper/keeper.go | 155 +++-- x/leverage/keeper/keeper_test.go | 16 +- x/leverage/keeper/liquidate.go | 19 +- x/leverage/keeper/reserves.go | 19 + x/leverage/keeper/store.go | 7 +- x/leverage/keeper/{loaned.go => supply.go} | 15 +- x/leverage/keeper/token.go | 33 -- x/leverage/keeper/validate.go | 28 +- x/leverage/simulation/genesis.go | 13 + x/leverage/simulation/operations.go | 235 +++++--- x/leverage/simulation/operations_test.go | 24 +- x/leverage/simulation/params.go | 5 + x/leverage/spec/07_params.md | 7 +- x/leverage/types/errors.go | 2 + x/leverage/types/leverage.pb.go | 163 ++++-- x/leverage/types/params.go | 27 + x/leverage/types/token.go | 36 +- x/leverage/types/token_test.go | 33 +- x/leverage/types/tx.pb.go | 7 +- 33 files changed, 814 insertions(+), 762 deletions(-) rename x/leverage/keeper/{loaned.go => supply.go} (80%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ce6a3577..96715c056d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1140](https://github.com/umee-network/umee/pull/1140) Rename MarketSize query to TotalSuppliedValue, and TokenMarketSize to TotalSupplied. - [1188](https://github.com/umee-network/umee/pull/1188) Remove all individual queries which duplicate market_summary fields. - [1199](https://github.com/umee-network/umee/pull/1199) Move all queries which require address input (e.g. `supplied`, `collateral_value`, `borrow_limit`) into aggregate queries `acccount_summary` or `account_balances`. +- [1222](https://github.com/umee-network/umee/pull/1222) Add leverage parameter DirectLiquidationFee. ### Features @@ -79,6 +80,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1203](https://github.com/umee-network/umee/pull/1203) Add Swagger docs. - [1212](https://github.com/umee-network/umee/pull/1212) Add `util/checkers` utility package providing common check / validation functions. - [1220](https://github.com/umee-network/umee/pull/1220) Submit oracle prevotes / vote txs via the CLI. +- [1222](https://github.com/umee-network/umee/pull/1222) Liquidation reward_denom can now be either token or uToken. ### Improvements diff --git a/proto/umee/leverage/v1/leverage.proto b/proto/umee/leverage/v1/leverage.proto index a1460309f8..092cbcc0c9 100644 --- a/proto/umee/leverage/v1/leverage.proto +++ b/proto/umee/leverage/v1/leverage.proto @@ -41,6 +41,14 @@ message Params { (gogoproto.nullable) = false, (gogoproto.moretags) = "yaml:\"small_liquidation_size\"" ]; + // Direct Liquidation Fee is the reduction in liquidation incentive experienced + // by liquidators who choose to receive base assets instead of uTokens as + // liquidation rewards. + string direct_liquidation_fee = 6 [ + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false, + (gogoproto.moretags) = "yaml:\"direct_liquidation_fee\"" + ]; } // Token defines a token, along with its metadata and parameters, in the Umee diff --git a/proto/umee/leverage/v1/tx.proto b/proto/umee/leverage/v1/tx.proto index 95554057f0..59ac0840dd 100644 --- a/proto/umee/leverage/v1/tx.proto +++ b/proto/umee/leverage/v1/tx.proto @@ -96,9 +96,10 @@ message MsgLiquidate { // Repayment is the maximum amount of base tokens that the liquidator is willing // to repay. cosmos.base.v1beta1.Coin repayment = 3 [(gogoproto.nullable) = false]; - // RewardDenom is the base token denom that the liquidator is willing to accept - // as a liquidation reward. The uToken equivalent of any base token rewards - // will be taken from the borrower's collateral. + // RewardDenom is the denom that the liquidator will receive as a liquidation reward. + // If it is a uToken, the liquidator will receive uTokens from the borrower's + // collateral. If it is a base token, the uTokens will be redeemed directly at + // a reduced Liquidation Incentive, and the liquidator will receive base tokens. string reward_denom = 4; } diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 37688e598d..79a418d465 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -4,70 +4,19 @@ info: description: A REST interface for state queries version: 1.0.0 paths: - /umee/leverage/v1/borrow_limit: - get: - summary: BorrowLimit queries the borrow limit in USD of a given borrower. - operationId: BorrowLimit - responses: - '200': - description: A successful response. - schema: - type: object - properties: - borrow_limit: - type: string - description: >- - QueryBorrowLimitResponse defines the response structure for the - BorrowLimit - - gRPC service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - tags: - - Query - /umee/leverage/v1/borrowed: + /umee/leverage/v1/account_balances: get: summary: >- - Borrowed queries for the borrowed amount of a user by token - denomination. - - If the denomination is not specified, the total for each borrowed token - is - - returned. - operationId: Borrowed + AccountBalances queries an account's current supply, collateral, and + borrow positions. + operationId: AccountBalances responses: '200': description: A successful response. schema: type: object properties: - borrowed: + supplied: type: array items: type: object @@ -84,116 +33,34 @@ paths: method signatures required by gogoproto. - description: >- - QueryBorrowedResponse defines the response structure for the - Borrowed gRPC - - service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: + description: >- + Supplied contains all tokens the account has supplied, + including interest earned. It is denominated in base tokens, + so exponent from each coin's registered_tokens entry must be + applied to convert to symbol denom. + collateral: type: array items: type: object properties: - type_url: + denom: type: string - value: + amount: type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - - name: denom - in: query - required: false - type: string - tags: - - Query - /umee/leverage/v1/borrowed_value: - get: - summary: >- - BorrowedValue queries for the usd value of the borrowed amount of a user - - by token denomination. If the denomination is not specified, the sum - across + description: >- + Coin defines a token with a denomination and an amount. - all borrowed tokens is returned. - operationId: BorrowedValue - responses: - '200': - description: A successful response. - schema: - type: object - properties: - borrowed_value: - type: string - description: |- - QueryBorrowedValueResponse defines the response structure for the - BorrowedValue gRPC service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - - name: denom - in: query - required: false - type: string - tags: - - Query - /umee/leverage/v1/collateral: - get: - summary: >- - Collateral queries the collateral amount of a user by token - denomination. - If the denomination is not specified, all of the user's collateral - tokens + NOTE: The amount field is an Int which implements the custom + method - are returned. - operationId: Collateral - responses: - '200': - description: A successful response. - schema: - type: object - properties: - collateral: + signatures required by gogoproto. + description: >- + Collateral contains all uTokens the account has + collateralized. It is denominated in uTokens, so both exponent + and uToken exchange rate from each coin's market_summary must + be applied to convert to base token symbol denom. + borrowed: type: array items: type: object @@ -210,11 +77,14 @@ paths: method signatures required by gogoproto. + description: >- + Borrowed contains all tokens the account has borrowed, + including interest owed. It is denominated in base tokens, so + exponent from each coin's registered_tokens entry must be + applied to convert to symbol denom. description: >- - QueryCollateralResponse defines the response structure for the - Collateral - - gRPC service handler. + QueryAccountBalancesResponse defines the response structure for + the AccountBalances gRPC service handler. default: description: An unexpected error response. schema: @@ -242,34 +112,49 @@ paths: in: query required: false type: string - - name: denom - in: query - required: false - type: string tags: - Query - /umee/leverage/v1/collateral_value: + /umee/leverage/v1/account_summary: get: summary: >- - CollateralValue queries for the total USD value of a user's collateral, - or - - the USD value held as a given base asset's associated uToken - denomination. - operationId: CollateralValue + AccountSummary queries USD values representing an account's total + positions and borrowing limits. It requires oracle prices to return + successfully. + operationId: AccountSummary responses: '200': description: A successful response. schema: type: object properties: + supplied_value: + type: string + description: >- + Supplied Value is the sum of the USD value of all tokens the + account has supplied, includng interest earned. collateral_value: type: string + description: >- + Collateral Value is the sum of the USD value of all uTokens + the account has collateralized. + borrowed_value: + type: string + description: >- + Borrowed Value is the sum of the USD value of all tokens the + account has borrowed, including interest owed. + borrow_limit: + type: string + description: >- + Borrow Limit is the maximum Borrowed Value the account is + allowed to reach through direct borrowing. + liquidation_threshold: + type: string + description: >- + Liquidation Threshold is the Borrowed Value at which the + account becomes eligible for liquidation. description: >- - QueryCollateralValueResponse defines the response structure for - the - - CollateralValue gRPC service handler. + QueryAccountSummaryResponse defines the response structure for the + AccountSummary gRPC service handler. default: description: An unexpected error response. schema: @@ -297,17 +182,13 @@ paths: in: query required: false type: string - - name: denom - in: query - required: false - type: string tags: - Query /umee/leverage/v1/liquidation_targets: get: - summary: |- - LiquidationTargets queries a list of all borrower addresses eligible for - liquidation. + summary: >- + LiquidationTargets queries a list of all borrower account addresses + eligible for liquidation. operationId: LiquidationTargets responses: '200': @@ -319,6 +200,9 @@ paths: type: array items: type: string + description: >- + Targets are the addresses of borrowers eligible for + liquidation. description: >- QueryLiquidationTargetsResponse defines the response structure for the @@ -348,54 +232,6 @@ paths: format: byte tags: - Query - /umee/leverage/v1/liquidation_threshold: - get: - summary: |- - LiquidationThreshold returns a maximum borrow value in USD above which a - given borrower is eligible for liquidation. - operationId: LiquidationThreshold - responses: - '200': - description: A successful response. - schema: - type: object - properties: - liquidation_threshold: - type: string - description: >- - QueryLiquidationThresholdResponse defines the response structure - for the - - LiquidationThreshold gRPC service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - tags: - - Query /umee/leverage/v1/market_summary: get: summary: >- @@ -636,6 +472,16 @@ paths: transaction, bypassing dynamic close factor. + direct_liquidation_fee: + type: string + description: >- + Direct Liquidation Fee is the reduction in liquidation + incentive experienced + + by liquidators who choose to receive base assets instead + of uTokens as + + liquidation rewards. description: Params defines the parameters for the leverage module. description: >- QueryParamsResponse defines the response structure for the Params @@ -773,8 +619,11 @@ paths: Enable Msg Supply allows supplying for lending or collateral using this - token. Note that withdrawing is always enabled. - Disabling supplying would + token. `false` means that a token can no longer be + supplied. + + Note that withdrawing is always enabled. Disabling + supply would be one step in phasing out an asset type. enable_msg_borrow: @@ -848,6 +697,20 @@ paths: value due to interest or liquidations. + max_supply: + type: string + description: >- + Max Supply is the maximum amount of tokens the protocol + can hold. + + Adding more supply of the given token to the protocol + will return an error. + + Must be a non negative value. 0 means that there is no + limit. + + To mark a token as not valid for supply, `msg_supply` + must be set to false. description: >- Token defines a token, along with its metadata and parameters, in the Umee @@ -882,130 +745,6 @@ paths: format: byte tags: - Query - /umee/leverage/v1/supplied: - get: - summary: >- - Supplied queries for the amount of tokens by a user by denomination. - - If the denomination is not specified, the total for each supplied token - is - - returned. - operationId: Supplied - responses: - '200': - description: A successful response. - schema: - type: object - properties: - supplied: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: >- - Coin defines a token with a denomination and an amount. - - - NOTE: The amount field is an Int which implements the custom - method - - signatures required by gogoproto. - description: >- - QuerySuppliedResponse defines the response structure for the - Supplied gRPC - - service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - - name: denom - in: query - required: false - type: string - tags: - - Query - /umee/leverage/v1/supplied_value: - get: - summary: |- - SuppliedValue queries for the USD value supplied by a user by token - denomination. If the denomination is not specified, the sum across all - supplied tokens is returned. - operationId: SuppliedValue - responses: - '200': - description: A successful response. - schema: - type: object - properties: - supplied_value: - type: string - description: >- - QuerySuppliedValueResponse defines the response structure for the - SuppliedValue - - gRPC service handler. - default: - description: An unexpected error response. - schema: - type: object - properties: - error: - type: string - code: - type: integer - format: int32 - message: - type: string - details: - type: array - items: - type: object - properties: - type_url: - type: string - value: - type: string - format: byte - parameters: - - name: address - in: query - required: false - type: string - - name: denom - in: query - required: false - type: string - tags: - - Query /umee/oracle/v1/denoms/active_exchange_rates: get: summary: ActiveExchangeRates returns all active denoms @@ -1661,21 +1400,20 @@ definitions: bypassing dynamic close factor. - description: Params defines the parameters for the leverage module. - umee.leverage.v1.QueryBorrowLimitResponse: - type: object - properties: - borrow_limit: + direct_liquidation_fee: type: string - description: >- - QueryBorrowLimitResponse defines the response structure for the - BorrowLimit + description: >- + Direct Liquidation Fee is the reduction in liquidation incentive + experienced - gRPC service handler. - umee.leverage.v1.QueryBorrowedResponse: + by liquidators who choose to receive base assets instead of uTokens as + + liquidation rewards. + description: Params defines the parameters for the leverage module. + umee.leverage.v1.QueryAccountBalancesResponse: type: object properties: - borrowed: + supplied: type: array items: type: object @@ -1689,20 +1427,11 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. - description: |- - QueryBorrowedResponse defines the response structure for the Borrowed gRPC - service handler. - umee.leverage.v1.QueryBorrowedValueResponse: - type: object - properties: - borrowed_value: - type: string - description: |- - QueryBorrowedValueResponse defines the response structure for the - BorrowedValue gRPC service handler. - umee.leverage.v1.QueryCollateralResponse: - type: object - properties: + description: >- + Supplied contains all tokens the account has supplied, including + interest earned. It is denominated in base tokens, so exponent from + each coin's registered_tokens entry must be applied to convert to + symbol denom. collateral: type: array items: @@ -1717,17 +1446,64 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. - description: |- - QueryCollateralResponse defines the response structure for the Collateral - gRPC service handler. - umee.leverage.v1.QueryCollateralValueResponse: + description: >- + Collateral contains all uTokens the account has collateralized. It is + denominated in uTokens, so both exponent and uToken exchange rate from + each coin's market_summary must be applied to convert to base token + symbol denom. + borrowed: + type: array + items: + type: object + properties: + denom: + type: string + amount: + type: string + description: |- + Coin defines a token with a denomination and an amount. + + NOTE: The amount field is an Int which implements the custom method + signatures required by gogoproto. + description: >- + Borrowed contains all tokens the account has borrowed, including + interest owed. It is denominated in base tokens, so exponent from each + coin's registered_tokens entry must be applied to convert to symbol + denom. + description: >- + QueryAccountBalancesResponse defines the response structure for the + AccountBalances gRPC service handler. + umee.leverage.v1.QueryAccountSummaryResponse: type: object properties: + supplied_value: + type: string + description: >- + Supplied Value is the sum of the USD value of all tokens the account + has supplied, includng interest earned. collateral_value: type: string - description: |- - QueryCollateralValueResponse defines the response structure for the - CollateralValue gRPC service handler. + description: >- + Collateral Value is the sum of the USD value of all uTokens the + account has collateralized. + borrowed_value: + type: string + description: >- + Borrowed Value is the sum of the USD value of all tokens the account + has borrowed, including interest owed. + borrow_limit: + type: string + description: >- + Borrow Limit is the maximum Borrowed Value the account is allowed to + reach through direct borrowing. + liquidation_threshold: + type: string + description: >- + Liquidation Threshold is the Borrowed Value at which the account + becomes eligible for liquidation. + description: >- + QueryAccountSummaryResponse defines the response structure for the + AccountSummary gRPC service handler. umee.leverage.v1.QueryLiquidationTargetsResponse: type: object properties: @@ -1735,17 +1511,10 @@ definitions: type: array items: type: string + description: Targets are the addresses of borrowers eligible for liquidation. description: |- QueryLiquidationTargetsResponse defines the response structure for the LiquidationTargets gRPC service handler. - umee.leverage.v1.QueryLiquidationThresholdResponse: - type: object - properties: - liquidation_threshold: - type: string - description: |- - QueryLiquidationThresholdResponse defines the response structure for the - LiquidationThreshold gRPC service handler. umee.leverage.v1.QueryMarketSummaryResponse: type: object properties: @@ -1931,6 +1700,16 @@ definitions: bypassing dynamic close factor. + direct_liquidation_fee: + type: string + description: >- + Direct Liquidation Fee is the reduction in liquidation incentive + experienced + + by liquidators who choose to receive base assets instead of + uTokens as + + liquidation rewards. description: Params defines the parameters for the leverage module. description: |- QueryParamsResponse defines the response structure for the Params gRPC @@ -2023,8 +1802,9 @@ definitions: Enable Msg Supply allows supplying for lending or collateral using this - token. Note that withdrawing is always enabled. Disabling - supplying would + token. `false` means that a token can no longer be supplied. + + Note that withdrawing is always enabled. Disabling supply would be one step in phasing out an asset type. enable_msg_borrow: @@ -2097,6 +1877,19 @@ definitions: to interest or liquidations. + max_supply: + type: string + description: >- + Max Supply is the maximum amount of tokens the protocol can + hold. + + Adding more supply of the given token to the protocol will + return an error. + + Must be a non negative value. 0 means that there is no limit. + + To mark a token as not valid for supply, `msg_supply` must be + set to false. description: >- Token defines a token, along with its metadata and parameters, in the Umee @@ -2105,36 +1898,6 @@ definitions: description: |- QueryRegisteredTokensResponse defines the response structure for the RegisteredTokens gRPC service handler. - umee.leverage.v1.QuerySuppliedResponse: - type: object - properties: - supplied: - type: array - items: - type: object - properties: - denom: - type: string - amount: - type: string - description: |- - Coin defines a token with a denomination and an amount. - - NOTE: The amount field is an Int which implements the custom method - signatures required by gogoproto. - description: |- - QuerySuppliedResponse defines the response structure for the Supplied gRPC - service handler. - umee.leverage.v1.QuerySuppliedValueResponse: - type: object - properties: - supplied_value: - type: string - description: >- - QuerySuppliedValueResponse defines the response structure for the - SuppliedValue - - gRPC service handler. umee.leverage.v1.Token: type: object properties: @@ -2210,8 +1973,9 @@ definitions: Enable Msg Supply allows supplying for lending or collateral using this - token. Note that withdrawing is always enabled. Disabling supplying - would + token. `false` means that a token can no longer be supplied. + + Note that withdrawing is always enabled. Disabling supply would be one step in phasing out an asset type. enable_msg_borrow: @@ -2284,6 +2048,18 @@ definitions: interest or liquidations. + max_supply: + type: string + description: >- + Max Supply is the maximum amount of tokens the protocol can hold. + + Adding more supply of the given token to the protocol will return an + error. + + Must be a non negative value. 0 means that there is no limit. + + To mark a token as not valid for supply, `msg_supply` must be set to + false. description: |- Token defines a token, along with its metadata and parameters, in the Umee capital facility that can be supplied and borrowed. diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index 9d6ec99334..f6985149c3 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -216,7 +216,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { sdk.NewInt64Coin(umeeapp.BondDenom, 1000), ), Collateral: sdk.NewCoins( - sdk.NewInt64Coin(types.UTokenFromTokenDenom(umeeapp.BondDenom), 1000), + sdk.NewInt64Coin(types.ToUTokenDenom(umeeapp.BondDenom), 1000), ), Borrowed: sdk.NewCoins( sdk.NewInt64Coin(umeeapp.BondDenom, 51), diff --git a/x/leverage/keeper/borrows.go b/x/leverage/keeper/borrows.go index 975c46462e..f6c29d0599 100644 --- a/x/leverage/keeper/borrows.go +++ b/x/leverage/keeper/borrows.go @@ -26,6 +26,17 @@ func (k Keeper) GetBorrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, denom st return owed } +// repayBorrow repays tokens borrowed by borrowAddr by sending coins in fromAddr to the module. This +// occurs during normal repayment (in which case fromAddr and borrowAddr are the same) and during +// liquidations, where fromAddr is the liquidator instead. +func (k Keeper) repayBorrow(ctx sdk.Context, fromAddr, borrowAddr sdk.AccAddress, repay sdk.Coin) error { + err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, fromAddr, types.ModuleName, sdk.NewCoins(repay)) + if err != nil { + return err + } + return k.setBorrow(ctx, borrowAddr, k.GetBorrow(ctx, borrowAddr, repay.Denom).Sub(repay)) +} + // setBorrow sets the amount borrowed by an address in a given denom. // If the amount is zero, any stored value is cleared. func (k Keeper) setBorrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk.Coin) error { diff --git a/x/leverage/keeper/borrows_test.go b/x/leverage/keeper/borrows_test.go index ac99f78e98..58f77fa18f 100644 --- a/x/leverage/keeper/borrows_test.go +++ b/x/leverage/keeper/borrows_test.go @@ -2,6 +2,8 @@ package keeper_test import ( sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v2/x/leverage/types" ) func (s *IntegrationTestSuite) TestGetBorrow() { @@ -195,7 +197,7 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { s.Require().EqualError(err, "abcd: invalid asset") // Create collateral uTokens (1k u/umee) - umeeCollatDenom := s.app.LeverageKeeper.FromTokenToUTokenDenom(s.ctx, umeeDenom) + umeeCollatDenom := types.ToUTokenDenom(umeeDenom) umeeCollateral := sdk.NewCoins(sdk.NewInt64Coin(umeeCollatDenom, 1000000000)) // Manually compute borrow limit using collateral weight of 0.25 @@ -210,7 +212,7 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { s.Require().Equal(expectedUmeeLimit, borrowLimit) // Create collateral atom uTokens (1k u/uatom) - atomCollatDenom := s.app.LeverageKeeper.FromTokenToUTokenDenom(s.ctx, atomIBCDenom) + atomCollatDenom := types.ToUTokenDenom(atomIBCDenom) atomCollateral := sdk.NewCoins(sdk.NewInt64Coin(atomCollatDenom, 1000000000)) // Manually compute borrow limit using collateral weight of 0.25 diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index 6912c17362..80f236ce94 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -7,6 +7,30 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) +// burnCollateral removes some uTokens from an account's collateral and burns them. This occurs +// during liquidations. +func (k Keeper) burnCollateral(ctx sdk.Context, addr sdk.AccAddress, coin sdk.Coin) error { + err := k.setCollateralAmount(ctx, addr, k.GetCollateralAmount(ctx, addr, coin.Denom).Sub(coin)) + if err != nil { + return err + } + if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { + return err + } + return k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, coin.Denom).Sub(coin)) +} + +// removeCollateral removes some uTokens in fromAddr's collateral and sends them to toAddr. This +// occurs when decollateralizing uTokens (in which case fromAddr and toAddr are the same) as well as +// during liquidations, where toAddr is the liquidator. +func (k Keeper) removeCollateral(ctx sdk.Context, fromAddr, toAddr sdk.AccAddress, coin sdk.Coin) error { + err := k.setCollateralAmount(ctx, fromAddr, k.GetCollateralAmount(ctx, fromAddr, coin.Denom).Sub(coin)) + if err != nil { + return err + } + return k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, toAddr, sdk.NewCoins(coin)) +} + // GetCollateralAmount returns an sdk.Coin representing how much of a given denom the // x/leverage module account currently holds as collateral for a given borrower. func (k Keeper) GetCollateralAmount(ctx sdk.Context, borrowerAddr sdk.AccAddress, denom string) sdk.Coin { @@ -54,10 +78,10 @@ func (k Keeper) setCollateralAmount(ctx sdk.Context, borrowerAddr sdk.AccAddress } // GetTotalCollateral returns an sdk.Coin representing how much of a given uToken -// the x/leverage module account currently holds as collateral. Non-uTokens and invalid -// assets return zero. +// the x/leverage module account currently holds as collateral. Non-uTokens return +// zero. func (k Keeper) GetTotalCollateral(ctx sdk.Context, denom string) sdk.Int { - if !k.IsAcceptedUToken(ctx, denom) { + if !types.HasUTokenPrefix(denom) { // non-uTokens cannot be collateral return sdk.ZeroInt() } diff --git a/x/leverage/keeper/collateral_test.go b/x/leverage/keeper/collateral_test.go index e643db62af..fd48279084 100644 --- a/x/leverage/keeper/collateral_test.go +++ b/x/leverage/keeper/collateral_test.go @@ -2,10 +2,12 @@ package keeper_test import ( sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/umee-network/umee/v2/x/leverage/types" ) func (s *IntegrationTestSuite) TestGetCollateralAmount() { - uDenom := s.tk.FromTokenToUTokenDenom(s.ctx, umeeDenom) + uDenom := types.ToUTokenDenom(umeeDenom) // get u/umee collateral amount of empty account address (zero) collateral := s.tk.GetCollateralAmount(s.ctx, sdk.AccAddress{}, uDenom) @@ -48,7 +50,7 @@ func (s *IntegrationTestSuite) TestGetCollateralAmount() { } func (s *IntegrationTestSuite) TestSetCollateralAmount() { - uDenom := s.tk.FromTokenToUTokenDenom(s.ctx, umeeDenom) + uDenom := types.ToUTokenDenom(umeeDenom) // set u/umee collateral amount of empty account address (error) err := s.tk.SetCollateralAmount(s.ctx, sdk.AccAddress{}, sdk.NewInt64Coin(uDenom, 0)) diff --git a/x/leverage/keeper/exchange_rate.go b/x/leverage/keeper/exchange_rate.go index cc775d1f45..fb82a6bc2a 100644 --- a/x/leverage/keeper/exchange_rate.go +++ b/x/leverage/keeper/exchange_rate.go @@ -14,7 +14,7 @@ func (k Keeper) ExchangeToken(ctx sdk.Context, token sdk.Coin) (sdk.Coin, error) return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, token.String()) } - uTokenDenom := k.FromTokenToUTokenDenom(ctx, token.Denom) + uTokenDenom := types.ToUTokenDenom(token.Denom) if uTokenDenom == "" { return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, token.Denom) } @@ -32,7 +32,7 @@ func (k Keeper) ExchangeUToken(ctx sdk.Context, uToken sdk.Coin) (sdk.Coin, erro return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, uToken.String()) } - tokenDenom := k.FromUTokenToTokenDenom(ctx, uToken.Denom) + tokenDenom := types.ToTokenDenom(uToken.Denom) if tokenDenom == "" { return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, uToken.Denom) } @@ -72,7 +72,7 @@ func (k Keeper) DeriveExchangeRate(ctx sdk.Context, denom string) sdk.Dec { moduleBalance := k.ModuleBalance(ctx, denom).ToDec() reserveAmount := k.GetReserveAmount(ctx, denom).ToDec() totalBorrowed := k.getAdjustedTotalBorrowed(ctx, denom).Mul(k.getInterestScalar(ctx, denom)) - uTokenSupply := k.GetUTokenSupply(ctx, k.FromTokenToUTokenDenom(ctx, denom)).Amount + uTokenSupply := k.GetUTokenSupply(ctx, types.ToUTokenDenom(denom)).Amount // Derive effective token supply tokenSupply := moduleBalance.Add(totalBorrowed).Sub(reserveAmount) diff --git a/x/leverage/keeper/filter.go b/x/leverage/keeper/filter.go index 864c958273..0b1a276c41 100644 --- a/x/leverage/keeper/filter.go +++ b/x/leverage/keeper/filter.go @@ -24,3 +24,13 @@ func (k Keeper) filterAcceptedCoins(ctx sdk.Context, coins sdk.Coins) sdk.Coins }, ) } + +// filterAcceptedUTokens returns the subset of an sdk.Coins that are accepted, non-blacklisted uTokens +func (k Keeper) filterAcceptedUTokens(ctx sdk.Context, coins sdk.Coins) sdk.Coins { + return k.filterCoins( + coins, + func(c sdk.Coin) bool { + return k.validateAcceptedUToken(ctx, c) == nil + }, + ) +} diff --git a/x/leverage/keeper/grpc_query.go b/x/leverage/keeper/grpc_query.go index e15deea254..c41e0bb3a1 100644 --- a/x/leverage/keeper/grpc_query.go +++ b/x/leverage/keeper/grpc_query.go @@ -82,7 +82,7 @@ func (q Querier) MarketSummary( reserved := q.Keeper.GetReserveAmount(ctx, req.Denom) borrowed := q.Keeper.GetTotalBorrowed(ctx, req.Denom) - uDenom := q.Keeper.FromTokenToUTokenDenom(ctx, req.Denom) + uDenom := types.ToUTokenDenom(req.Denom) uSupply := q.Keeper.GetUTokenSupply(ctx, uDenom) uCollateral := q.Keeper.GetTotalCollateral(ctx, uDenom) diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index 8b926819e7..ff3fcffd4e 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -65,7 +65,7 @@ func (s *IntegrationTestSuite) TestQuerier_AccountBalances() { sdk.NewCoin(umeeDenom, sdk.NewInt(1000000000)), ), Collateral: sdk.NewCoins( - sdk.NewCoin(types.UTokenFromTokenDenom(umeeDenom), sdk.NewInt(1000000000)), + sdk.NewCoin(types.ToUTokenDenom(umeeDenom), sdk.NewInt(1000000000)), ), Borrowed: nil, } diff --git a/x/leverage/keeper/iter.go b/x/leverage/keeper/iter.go index 7be4bc5c1e..a0a1923229 100644 --- a/x/leverage/keeper/iter.go +++ b/x/leverage/keeper/iter.go @@ -146,7 +146,6 @@ func (k Keeper) GetBorrowerCollateral(ctx sdk.Context, borrowerAddr sdk.AccAddre return err } - // add to totalBorrowed totalCollateral = totalCollateral.Add(sdk.NewCoin(denom, amount)) return nil } @@ -234,6 +233,7 @@ func (k Keeper) SweepBadDebts(ctx sdk.Context) error { // first check if the borrower has gained collateral since the bad debt was identified done := k.HasCollateral(ctx, addr) + // TODO #1223: Decollateralize any blacklisted collateral and proceed // if collateral is still zero, attempt to repay a single address's debt in this denom if !done { diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 00291bd8cf..3ca72dac81 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -110,16 +110,20 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, loan sdk.Co return nil } -// Withdraw attempts to deposit uTokens into the leverage module in exchange -// for the original tokens supplied. Accepts a uToken amount to exchange for base tokens. -// If the uToken denom is invalid or account or module balance insufficient, returns error. -func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Coin) error { - if !k.IsAcceptedUToken(ctx, coin.Denom) { - return sdkerrors.Wrap(types.ErrInvalidAsset, coin.String()) +// Withdraw attempts to deposit uTokens into the leverage module in exchange for base tokens. +// If there are not enough uTokens in balance, Withdraw will attempt to withdraw uToken collateral +// to make up the difference (as long as borrow limit allows). If the uToken denom is invalid or +// balances are insufficient to withdraw the full amount requested, returns an error. +func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) error { + if err := uToken.Validate(); err != nil { + return err + } + if !types.HasUTokenPrefix(uToken.Denom) { + return types.ErrNotUToken.Wrap(uToken.Denom) } // calculate base asset amount to withdraw - token, err := k.ExchangeUToken(ctx, coin) + token, err := k.ExchangeUToken(ctx, uToken) if err != nil { return err } @@ -132,9 +136,9 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk. } // Withdraw will first attempt to use any uTokens in the supplier's wallet - amountFromWallet := sdk.MinInt(k.bankKeeper.SpendableCoins(ctx, supplierAddr).AmountOf(coin.Denom), coin.Amount) + amountFromWallet := sdk.MinInt(k.bankKeeper.SpendableCoins(ctx, supplierAddr).AmountOf(uToken.Denom), uToken.Amount) // Any additional uTokens must come from the supplier's collateral - amountFromCollateral := coin.Amount.Sub(amountFromWallet) + amountFromCollateral := uToken.Amount.Sub(amountFromWallet) if amountFromCollateral.IsPositive() { // Calculate current borrowed value @@ -146,12 +150,15 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk. // Check for sufficient collateral collateral := k.GetBorrowerCollateral(ctx, supplierAddr) - if collateral.AmountOf(coin.Denom).LT(amountFromCollateral) { - return sdkerrors.Wrap(types.ErrInsufficientBalance, coin.String()) + if collateral.AmountOf(uToken.Denom).LT(amountFromCollateral) { + return types.ErrInsufficientBalance.Wrapf("%s uToken balance + %s from collateral is less than %s to withdraw", + amountFromWallet.String(), + collateral.AmountOf(uToken.Denom).String(), + uToken.String()) } // Calculate what borrow limit will be AFTER this withdrawal - collateralToWithdraw := sdk.NewCoins(sdk.NewCoin(coin.Denom, amountFromCollateral)) + collateralToWithdraw := sdk.NewCoins(sdk.NewCoin(uToken.Denom, amountFromCollateral)) newBorrowLimit, err := k.CalculateBorrowLimit(ctx, collateral.Sub(collateralToWithdraw)) if err != nil { return err @@ -164,14 +171,14 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk. } // reduce the supplier's collateral by amountFromCollateral - newCollateral := sdk.NewCoin(coin.Denom, collateral.AmountOf(coin.Denom).Sub(amountFromCollateral)) + newCollateral := sdk.NewCoin(uToken.Denom, collateral.AmountOf(uToken.Denom).Sub(amountFromCollateral)) if err = k.setCollateralAmount(ctx, supplierAddr, newCollateral); err != nil { return err } } // transfer amountFromWallet uTokens to the module account - uTokens := sdk.NewCoins(sdk.NewCoin(coin.Denom, amountFromWallet)) + uTokens := sdk.NewCoins(sdk.NewCoin(uToken.Denom, amountFromWallet)) if err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, supplierAddr, types.ModuleName, uTokens); err != nil { return err } @@ -183,10 +190,10 @@ func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk. } // burn the uTokens and set the new total uToken supply - if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(coin)); err != nil { + if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(uToken)); err != nil { return err } - if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, coin.Denom).Sub(coin)); err != nil { + if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, uToken.Denom).Sub(uToken)); err != nil { return err } @@ -249,35 +256,21 @@ func (k Keeper) Borrow(ctx sdk.Context, borrowerAddr sdk.AccAddress, borrow sdk. // necessary amount is transferred. Because amount repaid may be less than the repayment attempted, // Repay returns the actual amount repaid. func (k Keeper) Repay(ctx sdk.Context, borrowerAddr sdk.AccAddress, payment sdk.Coin) (sdk.Int, error) { - if !payment.IsValid() { - return sdk.ZeroInt(), types.ErrInvalidAsset.Wrap(payment.String()) + if err := payment.Validate(); err != nil { + return sdk.ZeroInt(), err } - // Determine amount of selected denom currently owed + // determine amount of selected denom currently owed owed := k.GetBorrow(ctx, borrowerAddr, payment.Denom) if owed.IsZero() { - // Borrower has no open borrows in the denom presented as payment - return sdk.ZeroInt(), types.ErrInvalidRepayment.Wrap( - "Borrower doesn't have active position in " + payment.Denom) + return sdk.ZeroInt(), types.ErrInvalidRepayment.Wrapf("No %s borrowed ", payment.Denom) } - // Prevent overpaying + // prevent overpaying payment.Amount = sdk.MinInt(owed.Amount, payment.Amount) - if err := payment.Validate(); err != nil { - return sdk.ZeroInt(), types.ErrInvalidRepayment.Wrap(err.Error()) - } // send payment to leverage module account - if err := k.bankKeeper.SendCoinsFromAccountToModule( - ctx, borrowerAddr, - types.ModuleName, - sdk.NewCoins(payment), - ); err != nil { - return sdk.ZeroInt(), err - } - - owed.Amount = owed.Amount.Sub(payment.Amount) - if err := k.setBorrow(ctx, borrowerAddr, owed); err != nil { + if err := k.repayBorrow(ctx, borrowerAddr, borrowerAddr, payment); err != nil { return sdk.ZeroInt(), err } return payment.Amount, nil @@ -346,31 +339,40 @@ func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, co return nil } -// Liquidate attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange -// for a the base token equivalent of selected denomination of the borrower's uToken collateral. 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 or available balances, then a partial liquidation, equal to the maximum -// valid amount, is performed. Because partial liquidation is possible and exchange rates vary, Liquidate -// returns the actual amount of tokens repaid, uTokens consumed, and base tokens rewarded. +// Liquidate attempts to repay one of an eligible borrower's borrows (in part or in full) in exchange for +// some of the borrower's uToken collateral or associated base tokens. 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 +// or available balances, then a partial liquidation, equal to the maximum valid amount, is performed. +// Because partial liquidation is possible and exchange rates vary, Liquidate returns the actual amount of +// tokens repaid, collateral liquidated, and base tokens or uTokens rewarded. func (k Keeper) Liquidate( ctx sdk.Context, liquidatorAddr, borrowerAddr sdk.AccAddress, maxRepay sdk.Coin, rewardDenom string, -) (baseRepay sdk.Coin, collateralReward sdk.Coin, baseReward sdk.Coin, err error) { +) (repaid sdk.Coin, liquidated sdk.Coin, reward sdk.Coin, err error) { if err := k.validateAcceptedAsset(ctx, maxRepay); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } + + // detect if the user selected a base token reward instead of a uToken + directLiquidation := !types.HasUTokenPrefix(rewardDenom) + if !directLiquidation { + // convert rewardDenom to base token + rewardDenom = types.ToTokenDenom(rewardDenom) + } + // ensure that base reward is a registered token if err := k.validateAcceptedDenom(ctx, rewardDenom); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // calculate borrowed Token repay, uToken collateral, and Token reward amounts allowed by + // calculate borrowed Token repay, uToken liquidate, and Token reward amounts allowed by // liquidation rules and available balances - baseRepay, collateralReward, baseReward, err = k.getLiquidationAmounts( + baseRepay, collateralLiquidate, baseReward, err := k.getLiquidationAmounts( ctx, liquidatorAddr, borrowerAddr, maxRepay, rewardDenom, + directLiquidation, ) if err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err @@ -381,49 +383,38 @@ func (k Keeper) Liquidate( return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationInvalid } - // send repayment from liquidator to leverage module account - err = k.bankKeeper.SendCoinsFromAccountToModule(ctx, liquidatorAddr, types.ModuleName, sdk.NewCoins(baseRepay)) - if err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - // update borrower's remaining borrowed amount - newBorrow := k.GetBorrow(ctx, borrowerAddr, baseRepay.Denom).Amount.Sub(baseRepay.Amount) - if err = k.setBorrow(ctx, borrowerAddr, sdk.NewCoin(baseRepay.Denom, newBorrow)); err != nil { + // repay some of the borrower's debt using the liquidator's balance + if err = k.repayBorrow(ctx, liquidatorAddr, borrowerAddr, baseRepay); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // reduce borrower's collateral by collateral reward amount - oldCollateral := k.GetCollateralAmount(ctx, borrowerAddr, collateralReward.Denom) - newCollateral := sdk.NewCoin(collateralReward.Denom, oldCollateral.Amount.Sub(collateralReward.Amount)) - if err = k.setCollateralAmount(ctx, borrowerAddr, newCollateral); err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - // burn the collateral reward uTokens and set the new total uToken supply - if err = k.bankKeeper.BurnCoins(ctx, types.ModuleName, sdk.NewCoins(collateralReward)); err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - if err = k.setUTokenSupply(ctx, k.GetUTokenSupply(ctx, collateralReward.Denom).Sub(collateralReward)); err != nil { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + if directLiquidation { + // burn the uToken reward from borrower's collateral + if err = k.burnCollateral(ctx, borrowerAddr, collateralLiquidate); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + + // send base rewards from module to liquidator's account + if err = k.bankKeeper.SendCoinsFromModuleToAccount( + ctx, types.ModuleName, liquidatorAddr, sdk.NewCoins(baseReward), + ); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } + } else { + // send uToken rewards from borrower collateral to liquidator's account + if err = k.removeCollateral(ctx, borrowerAddr, liquidatorAddr, collateralLiquidate); err != nil { + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err + } } - // send base rewards from module to liquidator's account - err = k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, liquidatorAddr, sdk.NewCoins(baseReward)) - if err != nil { + // if borrower's collateral has reached zero, mark any remaining borrows as bad debt + if err := k.checkBadDebt(ctx, borrowerAddr); err != nil { return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err } - // get remaining collateral, ignoring blacklisted - remainingCollateral := k.filterAcceptedCoins(ctx, k.GetBorrowerCollateral(ctx, borrowerAddr)) - - // detect bad debt if collateral is completely exhausted - 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 { - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, err - } - } + // the last return value is the liquidator's selected reward + if directLiquidation { + return baseRepay, collateralLiquidate, baseReward, nil } - - return baseRepay, collateralReward, baseReward, nil + return baseRepay, collateralLiquidate, collateralLiquidate, nil } diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index d828c960c4..9e0b3c50c3 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -165,7 +165,7 @@ func (s *IntegrationTestSuite) TestSupply_Valid() { s.Require().NoError(err) // verify the total supply of the minted uToken - uTokenDenom := types.UTokenFromTokenDenom(umeeapp.BondDenom) + uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) supply := s.app.LeverageKeeper.GetUTokenSupply(ctx, uTokenDenom) expected := sdk.NewInt64Coin(uTokenDenom, 1000000000) // 1k u/umee s.Require().Equal(expected, supply) @@ -194,7 +194,7 @@ func (s *IntegrationTestSuite) TestWithdraw_Valid() { s.Require().NoError(err) // verify the total supply of the minted uToken - uTokenDenom := types.UTokenFromTokenDenom(umeeapp.BondDenom) + uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) supply := s.app.LeverageKeeper.GetUTokenSupply(ctx, uTokenDenom) expected := sdk.NewInt64Coin(uTokenDenom, 1000000000) // 1k u/umee s.Require().Equal(expected, supply) @@ -402,7 +402,7 @@ func (s *IntegrationTestSuite) TestBorrow_BorrowLimit() { // determine an amount of umee to borrow, such that the user will be at about 90% of their borrow limit token, _ := s.app.LeverageKeeper.GetTokenSettings(s.ctx, umeeapp.BondDenom) - uDenom := s.app.LeverageKeeper.FromTokenToUTokenDenom(s.ctx, umeeapp.BondDenom) + uDenom := types.ToUTokenDenom(umeeapp.BondDenom) collateral := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, uDenom) amountToBorrow := token.CollateralWeight.Mul(sdk.MustNewDecFromStr("0.9")).MulInt(collateral.Amount).TruncateInt() @@ -855,7 +855,7 @@ func (s *IntegrationTestSuite) TestCollateralAmountInvariant() { _, broken := keeper.CollateralAmountInvariant(s.app.LeverageKeeper)(s.ctx) s.Require().False(broken) - uTokenDenom := types.UTokenFromTokenDenom(umeeapp.BondDenom) + uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) // withdraw the supplyed umee in the initBorrowScenario err := s.app.LeverageKeeper.Withdraw(s.ctx, addr, sdk.NewInt64Coin(uTokenDenom, 1000000000)) @@ -898,7 +898,7 @@ func (s *IntegrationTestSuite) TestWithdraw_InsufficientCollateral() { _ = s.setupAccount(umeeapp.BondDenom, 1000000, 1000000, 0, true) // verify collateral amount and total supply of minted uTokens - uTokenDenom := types.UTokenFromTokenDenom(umeeapp.BondDenom) + uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) collateral := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, supplierAddr, uTokenDenom) s.Require().Equal(sdk.NewInt64Coin(uTokenDenom, 1000000), collateral) // 1 u/umee supply := s.app.LeverageKeeper.GetUTokenSupply(s.ctx, uTokenDenom) @@ -907,12 +907,14 @@ func (s *IntegrationTestSuite) TestWithdraw_InsufficientCollateral() { // withdraw more collateral than available uToken := collateral.Add(sdk.NewInt64Coin(uTokenDenom, 1)) err := s.app.LeverageKeeper.Withdraw(s.ctx, supplierAddr, uToken) - s.Require().EqualError(err, "1000001u/uumee: insufficient balance") + s.Require().EqualError(err, + "0 uToken balance + 1000000 from collateral is less than 1000001u/uumee to withdraw: insufficient balance", + ) } func (s *IntegrationTestSuite) TestTotalCollateral() { // Test zero collateral - uDenom := types.UTokenFromTokenDenom(umeeDenom) + uDenom := types.ToUTokenDenom(umeeDenom) collateral := s.app.LeverageKeeper.GetTotalCollateral(s.ctx, uDenom) s.Require().Equal(sdk.ZeroInt(), collateral) diff --git a/x/leverage/keeper/liquidate.go b/x/leverage/keeper/liquidate.go index ed6669ff61..e80b8a6897 100644 --- a/x/leverage/keeper/liquidate.go +++ b/x/leverage/keeper/liquidate.go @@ -8,16 +8,17 @@ import ( // getLiquidationAmounts takes a repayment and reward denom proposed by a liquidator and calculates // the actual repayment amount a target address is eligible for, and the corresponding collateral -// to burn and rewards to return to the liquidator. +// to liquidate and equivalent base rewards to send to the liquidator. func (k Keeper) getLiquidationAmounts( ctx sdk.Context, - liquidatorAddr sdk.AccAddress, + liquidatorAddr, targetAddr sdk.AccAddress, maxRepay sdk.Coin, rewardDenom string, -) (tokenRepay sdk.Coin, collateralBurn sdk.Coin, tokenReward sdk.Coin, err error) { + directLiquidation bool, +) (tokenRepay sdk.Coin, collateralLiquidate sdk.Coin, tokenReward sdk.Coin, err error) { repayDenom := maxRepay.Denom - collateralDenom := k.FromTokenToUTokenDenom(ctx, rewardDenom) + collateralDenom := types.ToUTokenDenom(rewardDenom) // get relevant liquidator, borrower, and module balances borrowerCollateral := k.GetBorrowerCollateral(ctx, targetAddr) @@ -67,6 +68,14 @@ func (k Keeper) getLiquidationAmounts( // get collateral uToken exchange rate exchangeRate := k.DeriveExchangeRate(ctx, rewardDenom) + // Reduce liquidation incentive if the liquidator has specified they would like to directly receive base assets. + // Since this fee also reduces the amount of collateral that must be burned, it is applied before any other + // computations, as if the token itself had a smaller liquidation incentive. + liqudationIncentive := ts.LiquidationIncentive + if directLiquidation { + liqudationIncentive = liqudationIncentive.Mul(sdk.OneDec().Sub(params.DirectLiquidationFee)) + } + // compute final liquidation amounts repay, burn, reward := ComputeLiquidation( sdk.MinInt(sdk.MinInt(availableRepay, maxRepay.Amount), totalBorrowed.AmountOf(repayDenom)), @@ -75,7 +84,7 @@ func (k Keeper) getLiquidationAmounts( repayTokenPrice, rewardTokenPrice, exchangeRate, - ts.LiquidationIncentive, + liqudationIncentive, closeFactor, borrowedValue, ) diff --git a/x/leverage/keeper/reserves.go b/x/leverage/keeper/reserves.go index 1d2443e8b3..cf01fb2af6 100644 --- a/x/leverage/keeper/reserves.go +++ b/x/leverage/keeper/reserves.go @@ -46,6 +46,25 @@ func (k Keeper) setReserveAmount(ctx sdk.Context, coin sdk.Coin) error { return nil } +// checkBadDebt detects if a borrower has zero non-blacklisted collateral, +// and marks any remaining borrowed tokens as bad debt. +func (k Keeper) checkBadDebt(ctx sdk.Context, borrowerAddr sdk.AccAddress) error { + // get remaining collateral uTokens, ignoring blacklisted + remainingCollateral := k.filterAcceptedUTokens(ctx, k.GetBorrowerCollateral(ctx, borrowerAddr)) + + // detect bad debt if collateral is completely exhausted + 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 { + return err + } + } + } + + return nil +} + // RepayBadDebt uses reserves to repay borrower's debts of a given denom. // It returns a boolean representing whether full repayment was achieved. func (k Keeper) RepayBadDebt(ctx sdk.Context, borrowerAddr sdk.AccAddress, denom string) (bool, error) { diff --git a/x/leverage/keeper/store.go b/x/leverage/keeper/store.go index 95a3f5dcfc..e609d18968 100644 --- a/x/leverage/keeper/store.go +++ b/x/leverage/keeper/store.go @@ -134,8 +134,11 @@ func (k Keeper) GetUTokenSupply(ctx sdk.Context, denom string) sdk.Coin { // setUTokenSupply sets the total supply of a uToken. func (k Keeper) setUTokenSupply(ctx sdk.Context, coin sdk.Coin) error { - if !coin.IsValid() || !k.IsAcceptedUToken(ctx, coin.Denom) { - return sdkerrors.Wrap(types.ErrInvalidAsset, coin.String()) + if err := coin.Validate(); err != nil { + return err + } + if !types.HasUTokenPrefix(coin.Denom) { + return types.ErrNotUToken.Wrap(coin.Denom) } key := types.CreateUTokenSupplyKey(coin.Denom) diff --git a/x/leverage/keeper/loaned.go b/x/leverage/keeper/supply.go similarity index 80% rename from x/leverage/keeper/loaned.go rename to x/leverage/keeper/supply.go index 56cf5ed938..f51737e342 100644 --- a/x/leverage/keeper/loaned.go +++ b/x/leverage/keeper/supply.go @@ -2,7 +2,6 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/umee-network/umee/v2/x/leverage/types" ) @@ -10,12 +9,12 @@ import ( // GetSupplied returns an sdk.Coin representing how much of a given denom a // user has supplied, including interest accrued. func (k Keeper) GetSupplied(ctx sdk.Context, supplierAddr sdk.AccAddress, denom string) (sdk.Coin, error) { - if !k.IsAcceptedToken(ctx, denom) { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, denom) + if types.HasUTokenPrefix(denom) { + return sdk.Coin{}, types.ErrUToken.Wrap(denom) } // sum wallet-held and collateral-enabled uTokens in the associated uToken denom - uDenom := k.FromTokenToUTokenDenom(ctx, denom) + uDenom := types.ToUTokenDenom(denom) balance := k.bankKeeper.GetBalance(ctx, supplierAddr, uDenom) collateral := k.GetCollateralAmount(ctx, supplierAddr, uDenom) @@ -33,7 +32,7 @@ func (k Keeper) GetAllSupplied(ctx sdk.Context, supplierAddr sdk.AccAddress) (sd uTokens := sdk.Coins{} balance := k.bankKeeper.GetAllBalances(ctx, supplierAddr) for _, coin := range balance { - if k.IsAcceptedUToken(ctx, coin.Denom) { + if types.HasUTokenPrefix(coin.Denom) { uTokens = uTokens.Add(coin) } } @@ -45,11 +44,11 @@ func (k Keeper) GetAllSupplied(ctx sdk.Context, supplierAddr sdk.AccAddress) (sd // GetTotalSupply returns the total supplied by all suppliers in a given denom, // including any interest accrued. func (k Keeper) GetTotalSupply(ctx sdk.Context, denom string) (sdk.Coin, error) { - if !k.IsAcceptedToken(ctx, denom) { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, denom) + if types.HasUTokenPrefix(denom) { + return sdk.Coin{}, types.ErrUToken.Wrap(denom) } // convert associated uToken's total supply to base tokens - uTokenDenom := k.FromTokenToUTokenDenom(ctx, denom) + uTokenDenom := types.ToUTokenDenom(denom) return k.ExchangeUToken(ctx, k.GetUTokenSupply(ctx, uTokenDenom)) } diff --git a/x/leverage/keeper/token.go b/x/leverage/keeper/token.go index 688516fd73..411c6a6837 100644 --- a/x/leverage/keeper/token.go +++ b/x/leverage/keeper/token.go @@ -2,7 +2,6 @@ package keeper import ( "fmt" - "strings" sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -10,38 +9,6 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -// FromUTokenToTokenDenom strips the uToken prefix ("u/") from an input denom. -// An empty string is returned if the prefix is not present. -func (k Keeper) FromUTokenToTokenDenom(ctx sdk.Context, uTokenDenom string) string { - if strings.HasPrefix(uTokenDenom, types.UTokenPrefix) { - return strings.TrimPrefix(uTokenDenom, types.UTokenPrefix) - } - return "" -} - -// FromTokenToUTokenDenom adds the uToken prefix ("u/") to an input denom. -// An empty string is returned if the input token denom already has the prefix. -func (k Keeper) FromTokenToUTokenDenom(ctx sdk.Context, tokenDenom string) string { - if strings.HasPrefix(tokenDenom, types.UTokenPrefix) { - return "" - } - return types.UTokenPrefix + tokenDenom -} - -// IsAcceptedToken returns true if a given (non-UToken) token denom is an -// existing, non-blacklisted asset type. -func (k Keeper) IsAcceptedToken(ctx sdk.Context, tokenDenom string) bool { - t, err := k.GetTokenSettings(ctx, tokenDenom) - return err == nil && !t.Blacklist -} - -// IsAcceptedUToken returns true if a given uToken denom is associated with -// an accepted base asset type. -func (k Keeper) IsAcceptedUToken(ctx sdk.Context, uTokenDenom string) bool { - tokenDenom := k.FromUTokenToTokenDenom(ctx, uTokenDenom) - return k.IsAcceptedToken(ctx, tokenDenom) -} - // SetTokenSettings stores a Token into the x/leverage module's KVStore. func (k Keeper) SetTokenSettings(ctx sdk.Context, token types.Token) error { if err := token.Validate(); err != nil { diff --git a/x/leverage/keeper/validate.go b/x/leverage/keeper/validate.go index ff5c4b3d13..0693e06e71 100644 --- a/x/leverage/keeper/validate.go +++ b/x/leverage/keeper/validate.go @@ -16,6 +16,15 @@ func (k Keeper) validateAcceptedDenom(ctx sdk.Context, denom string) error { return token.AssertNotBlacklisted() } +// validateAcceptedUTokenDenom validates an sdk.Coin and ensures it is a uToken +// associated with a registered Token with Blacklisted == false +func (k Keeper) validateAcceptedUTokenDenom(ctx sdk.Context, udenom string) error { + if !types.HasUTokenPrefix(udenom) { + return types.ErrNotUToken.Wrap(udenom) + } + return k.validateAcceptedDenom(ctx, types.ToTokenDenom(udenom)) +} + // validateAcceptedAsset validates an sdk.Coin and ensures it is a registered Token // with Blacklisted == false func (k Keeper) validateAcceptedAsset(ctx sdk.Context, coin sdk.Coin) error { @@ -25,6 +34,15 @@ func (k Keeper) validateAcceptedAsset(ctx sdk.Context, coin sdk.Coin) error { return k.validateAcceptedDenom(ctx, coin.Denom) } +// validateAcceptedUToken validates an sdk.Coin and ensures it is a uToken +// associated with a registered Token with Blacklisted == false +func (k Keeper) validateAcceptedUToken(ctx sdk.Context, coin sdk.Coin) error { + if err := coin.Validate(); err != nil { + return err + } + return k.validateAcceptedUTokenDenom(ctx, coin.Denom) +} + // validateSupply validates an sdk.Coin and ensures its Denom is a Token with EnableMsgSupply func (k Keeper) validateSupply(ctx sdk.Context, loan sdk.Coin) error { if err := loan.Validate(); err != nil { @@ -49,14 +67,16 @@ func (k Keeper) validateBorrow(ctx sdk.Context, borrow sdk.Coin) error { return token.AssertBorrowEnabled() } -// validateCollateralAsset validates an sdk.Coin and ensures its Denom is a Token with EnableMsgSupply -// and CollateralWeight > 0 +// validateCollateralAsset validates an sdk.Coin and ensures it is a uToken of an accepted +// Token with EnableMsgSupply and CollateralWeight > 0 func (k Keeper) validateCollateralAsset(ctx sdk.Context, collateral sdk.Coin) error { if err := collateral.Validate(); err != nil { return err } - tokenDenom := k.FromUTokenToTokenDenom(ctx, collateral.Denom) - token, err := k.GetTokenSettings(ctx, tokenDenom) + if !types.HasUTokenPrefix(collateral.Denom) { + return types.ErrNotUToken.Wrap(collateral.Denom) + } + token, err := k.GetTokenSettings(ctx, types.ToTokenDenom(collateral.Denom)) if err != nil { return err } diff --git a/x/leverage/simulation/genesis.go b/x/leverage/simulation/genesis.go index 8a00b54143..5f6c5ad5c5 100644 --- a/x/leverage/simulation/genesis.go +++ b/x/leverage/simulation/genesis.go @@ -16,6 +16,7 @@ const ( minimumCloseFactorKey = "minimum_close_factor" oracleRewardFactorKey = "oracle_reward_factor" smallLiquidationSizeKey = "small_liquidation_size" + directLiquidationFeeKey = "direct_liquidation_fee" ) // GenCompleteLiquidationThreshold produces a randomized CompleteLiquidationThreshold in the range of [0.050, 0.100] @@ -38,6 +39,11 @@ func GenSmallLiquidationSize(r *rand.Rand) sdk.Dec { return sdk.NewDec(int64(r.Intn(1000))) } +// GenDirectLiquidationFee produces a randomized DirectLiquidationFee in the range of [0, 1000] +func GenDirectLiquidationFee(r *rand.Rand) sdk.Dec { + return sdk.NewDec(int64(r.Intn(1000))) +} + // RandomizedGenState generates a random GenesisState for oracle func RandomizedGenState(simState *module.SimulationState) { var completeLiquidationThreshold sdk.Dec @@ -64,12 +70,19 @@ func RandomizedGenState(simState *module.SimulationState) { func(r *rand.Rand) { smallLiquidationSize = GenSmallLiquidationSize(r) }, ) + var directLiquidationFee sdk.Dec + simState.AppParams.GetOrGenerate( + simState.Cdc, directLiquidationFeeKey, &directLiquidationFee, simState.Rand, + func(r *rand.Rand) { smallLiquidationSize = GenDirectLiquidationFee(r) }, + ) + leverageGenesis := types.NewGenesisState( types.Params{ CompleteLiquidationThreshold: completeLiquidationThreshold, MinimumCloseFactor: minimumCloseFactor, OracleRewardFactor: oracleRewardFactor, SmallLiquidationSize: smallLiquidationSize, + DirectLiquidationFee: directLiquidationFee, }, []types.Token{}, []types.AdjustedBorrow{}, diff --git a/x/leverage/simulation/operations.go b/x/leverage/simulation/operations.go index e71bc180bc..25f04a4683 100644 --- a/x/leverage/simulation/operations.go +++ b/x/leverage/simulation/operations.go @@ -20,7 +20,7 @@ const ( DefaultWeightMsgWithdraw int = 85 DefaultWeightMsgBorrow int = 80 DefaultWeightMsgCollateralize int = 60 - DefaultWeightMsgDecollateralize int = 0 + DefaultWeightMsgDecollateralize int = 60 DefaultWeightMsgRepay int = 70 DefaultWeightMsgLiquidate int = 75 OperationWeightMsgSupply = "op_weight_msg_supply" @@ -121,7 +121,7 @@ func SimulateMsgSupply(ak simulation.AccountKeeper, bk types.BankKeeper) simtype r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - from, coin, skip := randomSpendableFields(r, ctx, accs, bk) + from, coin, skip := randomSupplyFields(r, ctx, accs, bk) if skip { return simtypes.NoOpMsg(types.ModuleName, types.EventTypeSupply, "skip all transfers"), nil, nil } @@ -162,17 +162,18 @@ func SimulateMsgWithdraw(ak simulation.AccountKeeper, bk types.BankKeeper, lk ke msg := types.NewMsgWithdraw(from.Address, withdrawUToken) txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: simappparams.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: msg, - MsgType: types.EventTypeWithdraw, - Context: ctx, - SimAccount: from, - AccountKeeper: ak, - Bankkeeper: bk, - ModuleName: types.ModuleName, + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: types.EventTypeWithdraw, + Context: ctx, + SimAccount: from, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(withdrawUToken), } return simulation.GenAndDeliverTxWithRandFees(txCtx) @@ -186,7 +187,7 @@ func SimulateMsgBorrow(ak simulation.AccountKeeper, bk types.BankKeeper, lk keep r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - from, token, skip := randomTokenFields(r, ctx, accs, lk) + from, token, skip := randomBorrowFields(r, ctx, accs, lk) if skip { return simtypes.NoOpMsg(types.ModuleName, types.EventTypeBorrow, "skip all transfers"), nil, nil } @@ -222,27 +223,26 @@ func SimulateMsgCollateralize( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - from, token, skip := randomTokenFields(r, ctx, accs, lk) + from, collateral, skip := randomCollateralizeFields(r, ctx, accs, bk) if skip { return simtypes.NoOpMsg(types.ModuleName, types.EventTypeCollateralize, "skip all transfers"), nil, nil } - uDenom := lk.FromTokenToUTokenDenom(ctx, token.Denom) - coin := sdk.NewCoin(uDenom, token.Amount) - msg := types.NewMsgCollateralize(from.Address, coin) + msg := types.NewMsgCollateralize(from.Address, collateral) txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: simappparams.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: msg, - MsgType: types.EventTypeCollateralize, - Context: ctx, - SimAccount: from, - AccountKeeper: ak, - Bankkeeper: bk, - ModuleName: types.ModuleName, + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: types.EventTypeCollateralize, + Context: ctx, + SimAccount: from, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(collateral), } return simulation.GenAndDeliverTxWithRandFees(txCtx) @@ -260,14 +260,12 @@ func SimulateMsgDecollateralize( r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - from, token, skip := randomTokenFields(r, ctx, accs, lk) + from, collateral, skip := randomDecollateralizeFields(r, ctx, accs, lk) if skip { return simtypes.NoOpMsg(types.ModuleName, types.EventTypeDecollateralize, "skip all transfers"), nil, nil } - uDenom := lk.FromTokenToUTokenDenom(ctx, token.Denom) - coin := sdk.NewCoin(uDenom, token.Amount) - msg := types.NewMsgDecollateralize(from.Address, coin) + msg := types.NewMsgDecollateralize(from.Address, collateral) txCtx := simulation.OperationInput{ R: r, @@ -294,25 +292,26 @@ func SimulateMsgRepay(ak simulation.AccountKeeper, bk types.BankKeeper, lk keepe r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - from, borrowToken, skip := randomBorrowedFields(r, ctx, accs, lk) + from, repayToken, skip := randomRepayFields(r, ctx, accs, lk) if skip { return simtypes.NoOpMsg(types.ModuleName, types.EventTypeRepayBorrowedAsset, "skip all transfers"), nil, nil } - msg := types.NewMsgRepay(from.Address, borrowToken) + msg := types.NewMsgRepay(from.Address, repayToken) txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: simappparams.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: msg, - MsgType: types.EventTypeRepayBorrowedAsset, - Context: ctx, - SimAccount: from, - AccountKeeper: ak, - Bankkeeper: bk, - ModuleName: types.ModuleName, + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: types.EventTypeRepayBorrowedAsset, + Context: ctx, + SimAccount: from, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(repayToken), } return simulation.GenAndDeliverTxWithRandFees(txCtx) @@ -334,17 +333,18 @@ func SimulateMsgLiquidate(ak simulation.AccountKeeper, bk types.BankKeeper, lk k msg := types.NewMsgLiquidate(liquidator.Address, borrower.Address, repaymentToken, rewardDenom) txCtx := simulation.OperationInput{ - R: r, - App: app, - TxGen: simappparams.MakeTestEncodingConfig().TxConfig, - Cdc: nil, - Msg: msg, - MsgType: types.EventTypeLiquidate, - Context: ctx, - SimAccount: liquidator, - AccountKeeper: ak, - Bankkeeper: bk, - ModuleName: types.ModuleName, + R: r, + App: app, + TxGen: simappparams.MakeTestEncodingConfig().TxConfig, + Cdc: nil, + Msg: msg, + MsgType: types.EventTypeLiquidate, + Context: ctx, + SimAccount: liquidator, + AccountKeeper: ak, + Bankkeeper: bk, + ModuleName: types.ModuleName, + CoinsSpentInMsg: sdk.NewCoins(repaymentToken), } return simulation.GenAndDeliverTxWithRandFees(txCtx) @@ -359,40 +359,45 @@ func randomCoin(r *rand.Rand, coins sdk.Coins) sdk.Coin { return coins[r.Int31n(int32(coins.Len()))] } -// randomSpendableFields returns a random account and an sdk.Coin from its spendable -// coins. It returns skip=true if the account has zero spendable coins. -func randomSpendableFields( - r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, bk types.BankKeeper, -) (acc simtypes.Account, spendableToken sdk.Coin, skip bool) { - acc, _ = simtypes.RandomAcc(r, accs) +// getSpendableTokens returns all non-uTokens from an account's spendable coins. +func getSpendableTokens(ctx sdk.Context, addr sdk.AccAddress, bk types.BankKeeper) sdk.Coins { + tokens := sdk.NewCoins() + for _, coin := range bk.SpendableCoins(ctx, addr) { + if !types.HasUTokenPrefix(coin.Denom) { + tokens = tokens.Add(coin) + } + } - spendableBalances := bk.SpendableCoins(ctx, acc.Address) + return tokens +} - spendableTokens := simtypes.RandSubsetCoins(r, spendableBalances) - if spendableTokens.Empty() { - return acc, sdk.Coin{}, true +// getSpendableUTokens returns all uTokens from an account's spendable coins. +func getSpendableUTokens(ctx sdk.Context, addr sdk.AccAddress, bk types.BankKeeper) sdk.Coins { + uTokens := sdk.NewCoins() + for _, coin := range bk.SpendableCoins(ctx, addr) { + if types.HasUTokenPrefix(coin.Denom) { + uTokens = uTokens.Add(coin) + } } - return acc, randomCoin(r, spendableTokens), false + return uTokens } -// randomTokenFields returns a random account and an sdk.Coin from all -// the registered tokens with an random amount [0, 150]. -// It returns skip=true if no registered token was found. -func randomTokenFields( - r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, lk keeper.Keeper, -) (acc simtypes.Account, token sdk.Coin, skip bool) { +// randomSupplyFields returns a random account and a non-uToken from its spendable +// coins. It returns skip=true if the account has zero spendable base tokens. +func randomSupplyFields( + r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, bk types.BankKeeper, +) (acc simtypes.Account, spendableToken sdk.Coin, skip bool) { acc, _ = simtypes.RandomAcc(r, accs) - allTokens := lk.GetAllRegisteredTokens(ctx) - if len(allTokens) == 0 { + tokens := getSpendableTokens(ctx, acc.Address, bk) + + spendableTokens := simtypes.RandSubsetCoins(r, tokens) + if spendableTokens.Empty() { return acc, sdk.Coin{}, true } - registeredToken := allTokens[r.Int31n(int32(len(allTokens)))] - token = sdk.NewCoin(registeredToken.BaseDenom, simtypes.RandomAmount(r, sdk.NewInt(150))) - - return acc, token, false + return acc, randomCoin(r, spendableTokens), false } // randomWithdrawFields returns a random account and an sdk.Coin from its uTokens @@ -404,7 +409,7 @@ func randomWithdrawFields( ) (acc simtypes.Account, withdrawal sdk.Coin, skip bool) { acc, _ = simtypes.RandomAcc(r, accs) - uTokens := getSpendableUTokens(ctx, acc.Address, bk, lk) + uTokens := getSpendableUTokens(ctx, acc.Address, bk) uTokens = uTokens.Add(lk.GetBorrowerCollateral(ctx, acc.Address)...) uTokens = simtypes.RandSubsetCoins(r, uTokens) @@ -416,24 +421,64 @@ func randomWithdrawFields( return acc, randomCoin(r, uTokens), false } -// getSpendableUTokens returns all uTokens from an account's spendable coins. -func getSpendableUTokens( - ctx sdk.Context, addr sdk.AccAddress, - bk types.BankKeeper, lk keeper.Keeper, -) sdk.Coins { - uTokens := sdk.NewCoins() - for _, coin := range bk.SpendableCoins(ctx, addr) { - if lk.IsAcceptedUToken(ctx, coin.Denom) { - uTokens = uTokens.Add(coin) - } +// randomCollateralizeFields returns a random account and a uToken from its spendable +// coins. It returns skip=true if the account has zero spendable uTokens. +func randomCollateralizeFields( + r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, bk types.BankKeeper, +) (acc simtypes.Account, spendableToken sdk.Coin, skip bool) { + acc, _ = simtypes.RandomAcc(r, accs) + + uTokens := getSpendableUTokens(ctx, acc.Address, bk) + + uTokens = simtypes.RandSubsetCoins(r, uTokens) + + if uTokens.Empty() { + return acc, sdk.Coin{}, true } - return uTokens + return acc, randomCoin(r, uTokens), false +} + +// randomDecollateralizeFields returns a random account and a uToken from its collateral +// coins. It returns skip=true if the account has zero collateral. +func randomDecollateralizeFields( + r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, lk keeper.Keeper, +) (acc simtypes.Account, spendableToken sdk.Coin, skip bool) { + acc, _ = simtypes.RandomAcc(r, accs) + + uTokens := lk.GetBorrowerCollateral(ctx, acc.Address) + + uTokens = simtypes.RandSubsetCoins(r, uTokens) + + if uTokens.Empty() { + return acc, sdk.Coin{}, true + } + + return acc, randomCoin(r, uTokens), false +} + +// randomBorrowFields returns a random account and an sdk.Coin from all +// the registered tokens with an random amount [0, 150]. +// It returns skip=true if no registered token was found. +func randomBorrowFields( + r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, lk keeper.Keeper, +) (acc simtypes.Account, token sdk.Coin, skip bool) { + acc, _ = simtypes.RandomAcc(r, accs) + + allTokens := lk.GetAllRegisteredTokens(ctx) + if len(allTokens) == 0 { + return acc, sdk.Coin{}, true + } + + registeredToken := allTokens[r.Int31n(int32(len(allTokens)))] + token = sdk.NewCoin(registeredToken.BaseDenom, simtypes.RandomAmount(r, sdk.NewInt(150))) + + return acc, token, false } -// randomBorrowedFields returns a random account and an sdk.Coin from an open borrow position. +// randomRepayFields returns a random account and an sdk.Coin from an open borrow position. // It returns skip=true if no borrow position was open. -func randomBorrowedFields( +func randomRepayFields( r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, lk keeper.Keeper, ) (acc simtypes.Account, borrowToken sdk.Coin, skip bool) { acc, _ = simtypes.RandomAcc(r, accs) @@ -476,7 +521,7 @@ func randomLiquidateFields( return liquidator, borrower, sdk.Coin{}, "", true } - rewardDenom = lk.FromUTokenToTokenDenom(ctx, randomCoin(r, collateral).Denom) + rewardDenom = types.ToTokenDenom(randomCoin(r, collateral).Denom) return liquidator, borrower, randomCoin(r, borrowed), rewardDenom, false } diff --git a/x/leverage/simulation/operations_test.go b/x/leverage/simulation/operations_test.go index 706a891a1c..5cf67d64ae 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -93,9 +93,7 @@ func (s *SimTestSuite) SetupTest() { } tokens := []types.Token{umeeToken, atomIBCToken, uabc} - leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) - for _, token := range tokens { s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, token)) app.OracleKeeper.SetExchangeRate(ctx, token.SymbolDenom, sdk.MustNewDecFromStr("100.0")) @@ -193,7 +191,7 @@ func (s *SimTestSuite) TestSimulateMsgWithdraw() { supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(100)) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { - s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) + s.Require().NoError(s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken)) }) s.app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: s.app.LastBlockHeight() + 1, AppHash: s.app.LastCommitID().Hash}}) @@ -244,8 +242,11 @@ func (s *SimTestSuite) TestSimulateMsgBorrow() { func (s *SimTestSuite) TestSimulateMsgCollateralize() { r := rand.New(rand.NewSource(1)) + supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 100) - accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) {}) + accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { + s.Require().NoError(s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken)) + }) s.app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: s.app.LastBlockHeight() + 1, AppHash: s.app.LastCommitID().Hash}}) @@ -259,14 +260,23 @@ func (s *SimTestSuite) TestSimulateMsgCollateralize() { s.Require().True(operationMsg.OK) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Borrower) s.Require().Equal(types.EventTypeCollateralize, msg.Type()) - s.Require().Equal("0u/uabc", msg.Coin.String()) + s.Require().Equal("73u/uumee", msg.Coin.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgDecollateralize() { r := rand.New(rand.NewSource(1)) + supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 100) - accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) {}) + accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { + uToken, err := s.app.LeverageKeeper.ExchangeToken(s.ctx, supplyToken) + if err != nil { + s.Require().NoError(err) + } + + s.Require().NoError(s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken)) + s.Require().NoError(s.app.LeverageKeeper.Collateralize(s.ctx, fundedAccount.Address, uToken)) + }) s.app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: s.app.LastBlockHeight() + 1, AppHash: s.app.LastCommitID().Hash}}) @@ -280,7 +290,7 @@ func (s *SimTestSuite) TestSimulateMsgDecollateralize() { s.Require().True(operationMsg.OK) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Borrower) s.Require().Equal(types.EventTypeDecollateralize, msg.Type()) - s.Require().Equal("0u/uabc", msg.Coin.String()) + s.Require().Equal("73u/uumee", msg.Coin.String()) s.Require().Len(futureOperations, 0) } diff --git a/x/leverage/simulation/params.go b/x/leverage/simulation/params.go index e202060fc2..ade2fbfec7 100644 --- a/x/leverage/simulation/params.go +++ b/x/leverage/simulation/params.go @@ -34,5 +34,10 @@ func ParamChanges(r *rand.Rand) []simtypes.ParamChange { return fmt.Sprintf("\"%s\"", GenSmallLiquidationSize(r)) }, ), + simulation.NewSimParamChange(types.ModuleName, string(types.KeyDirectLiquidationFee), + func(r *rand.Rand) string { + return fmt.Sprintf("\"%s\"", GenDirectLiquidationFee(r)) + }, + ), } } diff --git a/x/leverage/spec/07_params.md b/x/leverage/spec/07_params.md index 626809e990..ae24d83e26 100644 --- a/x/leverage/spec/07_params.md +++ b/x/leverage/spec/07_params.md @@ -8,6 +8,7 @@ The leverage module contains the following parameters: | MinimumCloseFactor | sdk.Dec | 0.01 | | OracleRewardFactor | sdk.Dec | 0.01 | | SmallLiquidationSize | sdk.Dec | 100.00 | +| DirectLiquidationFee | sdk.Dec | 0.1 | ## CompleteLiquidationThreshold @@ -28,4 +29,8 @@ the `x/oracle` reward pool. ## SmallLiquidationSize SmallLiquidationSize is the borrow value in USD below which [Close Factor](01_concepts.md#Close-Factor) -is always 1. \ No newline at end of file +is always 1. + +## DirectLiquidationFee + +DirectLiquidationFee is the reduction in Liquidation Incentive when liquidators choose to directly receive base assets instead of uTokens as liquidation rewards. \ No newline at end of file diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 8dbed3cac6..357fb61401 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -38,4 +38,6 @@ var ( 1126, "market total collateral would exceed MaxCollateralShare", ) + ErrNotUToken = sdkerrors.Register(ModuleName, 1127, "denom should be a uToken") + ErrUToken = sdkerrors.Register(ModuleName, 1128, "denom should not be a uToken") ) diff --git a/x/leverage/types/leverage.pb.go b/x/leverage/types/leverage.pb.go index 707184d645..98e8e8be45 100644 --- a/x/leverage/types/leverage.pb.go +++ b/x/leverage/types/leverage.pb.go @@ -41,6 +41,10 @@ type Params struct { // considered small enough to be liquidated in a single transaction, bypassing // dynamic close factor. SmallLiquidationSize github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,5,opt,name=small_liquidation_size,json=smallLiquidationSize,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"small_liquidation_size" yaml:"small_liquidation_size"` + // Direct Liquidation Fee is the reduction in liquidation incentive experienced + // by liquidators who choose to receive base assets instead of uTokens as + // liquidation rewards. + DirectLiquidationFee github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,6,opt,name=direct_liquidation_fee,json=directLiquidationFee,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"direct_liquidation_fee" yaml:"direct_liquidation_fee"` } func (m *Params) Reset() { *m = Params{} } @@ -192,62 +196,63 @@ func init() { func init() { proto.RegisterFile("umee/leverage/v1/leverage.proto", fileDescriptor_8cb1bf9ea641ecc6) } var fileDescriptor_8cb1bf9ea641ecc6 = []byte{ - // 872 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xcf, 0x6f, 0x1b, 0x45, - 0x14, 0xf6, 0x42, 0x1a, 0xe2, 0x69, 0x63, 0x27, 0xdb, 0x24, 0x5d, 0x41, 0xf0, 0x46, 0x23, 0x81, - 0x72, 0x69, 0x4c, 0x81, 0x53, 0x8e, 0x4e, 0x05, 0x0d, 0x6a, 0x0b, 0x4c, 0x8a, 0x2a, 0x71, 0x59, - 0x8d, 0xd7, 0x83, 0x3d, 0xf2, 0xcc, 0x8e, 0x99, 0x19, 0xff, 0xca, 0x85, 0x03, 0xe2, 0xc4, 0x85, - 0x23, 0x17, 0xa4, 0x1e, 0xf8, 0x23, 0xf8, 0x13, 0x72, 0xec, 0x11, 0x71, 0xb0, 0x20, 0xb9, 0x70, - 0xce, 0x5f, 0x80, 0x66, 0x66, 0xed, 0x5d, 0xa7, 0x4b, 0xa5, 0x95, 0x7b, 0xf2, 0xee, 0xf7, 0x9e, - 0xbf, 0xef, 0x9b, 0x37, 0x6f, 0xe6, 0x2d, 0x08, 0x87, 0x9c, 0x90, 0x26, 0x23, 0x23, 0x22, 0x71, - 0x97, 0x34, 0x47, 0x0f, 0x16, 0xcf, 0x47, 0x03, 0x29, 0xb4, 0xf0, 0xb7, 0x4c, 0xc2, 0xd1, 0x02, - 0x1c, 0x3d, 0x78, 0x77, 0xa7, 0x2b, 0xba, 0xc2, 0x06, 0x9b, 0xe6, 0xc9, 0xe5, 0xc1, 0x3f, 0xd6, - 0xc0, 0xfa, 0x57, 0x58, 0x62, 0xae, 0xfc, 0xdf, 0x3c, 0xd0, 0x88, 0x05, 0x1f, 0x30, 0xa2, 0x49, - 0xc4, 0xe8, 0xf7, 0x43, 0xda, 0xc1, 0x9a, 0x8a, 0x24, 0xd2, 0x3d, 0x49, 0x54, 0x4f, 0xb0, 0x4e, - 0xf0, 0xd6, 0x81, 0x77, 0x58, 0x6d, 0x3d, 0xbf, 0x98, 0x85, 0x95, 0xbf, 0x66, 0xe1, 0x87, 0x5d, - 0xaa, 0x7b, 0xc3, 0xf6, 0x51, 0x2c, 0x78, 0x33, 0x16, 0x8a, 0x0b, 0x95, 0xfe, 0xdc, 0x57, 0x9d, - 0x7e, 0x53, 0x4f, 0x07, 0x44, 0x1d, 0x3d, 0x24, 0xf1, 0xf5, 0x2c, 0xfc, 0x60, 0x8a, 0x39, 0x3b, - 0x86, 0xaf, 0x67, 0x87, 0x68, 0x7f, 0x9e, 0xf0, 0x38, 0x8b, 0x3f, 0x9b, 0x87, 0xfd, 0x1f, 0xc0, - 0x0e, 0xa7, 0x09, 0xe5, 0x43, 0x1e, 0xc5, 0x4c, 0x28, 0x12, 0x7d, 0x87, 0x63, 0x2d, 0x64, 0xf0, - 0xb6, 0x35, 0xf5, 0xa4, 0xb4, 0xa9, 0xf7, 0x9c, 0xa9, 0x22, 0x4e, 0x88, 0xfc, 0x14, 0x3e, 0x31, - 0xe8, 0x67, 0x16, 0x34, 0x06, 0x84, 0xc4, 0x31, 0x23, 0x91, 0x24, 0x63, 0x2c, 0x3b, 0x73, 0x03, - 0x6b, 0xab, 0x19, 0x28, 0xe2, 0x84, 0xc8, 0x77, 0x30, 0xb2, 0x68, 0x6a, 0xe0, 0x27, 0x0f, 0xec, - 0x29, 0x8e, 0x19, 0x5b, 0x2a, 0xa0, 0xa2, 0xe7, 0x24, 0xb8, 0x65, 0x3d, 0x7c, 0x59, 0xda, 0xc3, - 0xfb, 0xce, 0x43, 0x31, 0x2b, 0x44, 0x3b, 0x36, 0x90, 0xdb, 0x8e, 0x33, 0x7a, 0x4e, 0x8e, 0xd7, - 0x7e, 0x7d, 0x11, 0x56, 0xe0, 0xef, 0x35, 0x70, 0xeb, 0x99, 0xe8, 0x93, 0xc4, 0xff, 0x14, 0x80, - 0x36, 0x56, 0x24, 0xea, 0x90, 0x44, 0xf0, 0xc0, 0xb3, 0x56, 0x76, 0xaf, 0x67, 0xe1, 0xb6, 0x23, - 0xcf, 0x62, 0x10, 0x55, 0xcd, 0xcb, 0x43, 0xf3, 0xec, 0x27, 0xa0, 0x26, 0x89, 0x22, 0x72, 0xb4, - 0xd8, 0x49, 0xd7, 0x5e, 0x9f, 0x97, 0x5e, 0xc4, 0xae, 0xd3, 0x59, 0x66, 0x83, 0x68, 0x33, 0x05, - 0xd2, 0xea, 0x8d, 0xc1, 0x76, 0x2c, 0x18, 0xc3, 0x9a, 0x48, 0xcc, 0xa2, 0x31, 0xa1, 0xdd, 0x9e, - 0x4e, 0x9b, 0xe7, 0x8b, 0xd2, 0x92, 0xc1, 0xbc, 0xa3, 0x6f, 0x10, 0x42, 0xb4, 0x95, 0x61, 0xcf, - 0x2d, 0xe4, 0xff, 0xe8, 0x81, 0xdd, 0xe2, 0xf3, 0xe4, 0x3a, 0xe7, 0x69, 0x69, 0xf5, 0x7d, 0xa7, - 0xfe, 0x3f, 0xc7, 0x68, 0x87, 0x15, 0x1d, 0x1f, 0x05, 0xb6, 0xec, 0x46, 0xb4, 0x85, 0x94, 0x62, - 0x1c, 0x49, 0xac, 0xe7, 0x5d, 0x73, 0x5a, 0x5a, 0xff, 0x5e, 0x6e, 0x63, 0x73, 0x7c, 0x10, 0xd5, - 0x0c, 0xd4, 0xb2, 0x08, 0xc2, 0x9a, 0x18, 0xd1, 0x3e, 0x4d, 0xfa, 0x4b, 0xa2, 0xeb, 0xab, 0x89, - 0xde, 0xe4, 0x83, 0xa8, 0x66, 0xa0, 0x9c, 0xe8, 0x00, 0xd4, 0x39, 0x9e, 0x2c, 0x69, 0xbe, 0x63, - 0x35, 0x1f, 0x95, 0xd6, 0xdc, 0x4b, 0xef, 0x88, 0x65, 0x3a, 0x88, 0x36, 0x39, 0x9e, 0xe4, 0x14, - 0x75, 0xba, 0xcc, 0xa1, 0xa6, 0x8c, 0x9e, 0xdb, 0xc2, 0x07, 0x1b, 0x6f, 0x60, 0x99, 0x39, 0x3e, - 0x88, 0xea, 0x06, 0xfa, 0x26, 0x43, 0x5e, 0xe9, 0x2b, 0x9a, 0xc4, 0x24, 0xd1, 0x74, 0x44, 0x82, - 0xea, 0x9b, 0xeb, 0xab, 0x05, 0xe9, 0x72, 0x5f, 0x9d, 0xce, 0x61, 0xff, 0x18, 0xdc, 0x51, 0x53, - 0xde, 0x16, 0x2c, 0x3d, 0xfe, 0xc0, 0x6a, 0xdf, 0xbb, 0x9e, 0x85, 0x77, 0xd3, 0xbb, 0x25, 0x17, - 0x85, 0xe8, 0xb6, 0x7b, 0x75, 0x57, 0x40, 0x13, 0x6c, 0x90, 0xc9, 0x40, 0x24, 0x24, 0xd1, 0xc1, - 0xed, 0x03, 0xef, 0x70, 0xb3, 0x75, 0xf7, 0x7a, 0x16, 0xd6, 0xdd, 0xff, 0xe6, 0x11, 0x88, 0x16, - 0x49, 0xfe, 0x23, 0xb0, 0x4d, 0x12, 0xdc, 0x66, 0x24, 0xe2, 0xaa, 0x1b, 0xa9, 0xe1, 0x60, 0xc0, - 0xa6, 0xc1, 0x9d, 0x03, 0xef, 0x70, 0xa3, 0xb5, 0x9f, 0x9d, 0xca, 0x57, 0x52, 0x20, 0xaa, 0x3b, - 0xec, 0x89, 0xea, 0x9e, 0x59, 0xe4, 0x06, 0x93, 0xdb, 0xdc, 0x60, 0xf3, 0x35, 0x4c, 0x2e, 0x25, - 0xcf, 0xe4, 0x1a, 0xc0, 0xdf, 0x07, 0xd5, 0x36, 0xc3, 0x71, 0x9f, 0x51, 0xa5, 0x83, 0x9a, 0x61, - 0x40, 0x19, 0x60, 0xa7, 0x16, 0x9e, 0x44, 0xb9, 0x8b, 0x42, 0xf5, 0xb0, 0x24, 0x41, 0x7d, 0xc5, - 0xa9, 0x55, 0xc0, 0x69, 0xa6, 0x16, 0x9e, 0x9c, 0x2c, 0xd0, 0x33, 0x03, 0xda, 0xa1, 0x61, 0xb2, - 0x5d, 0x25, 0x96, 0x5a, 0x74, 0x6b, 0xb5, 0xa1, 0x51, 0xcc, 0x0a, 0x91, 0x59, 0xb0, 0xab, 0x72, - 0xbe, 0x5b, 0x7f, 0xf6, 0x40, 0xc0, 0x69, 0x92, 0x77, 0xed, 0xfa, 0x89, 0xea, 0x69, 0xb0, 0x6d, - 0x9d, 0x7c, 0x5d, 0xda, 0x49, 0xb8, 0x98, 0xe1, 0x85, 0xbc, 0x10, 0xed, 0x71, 0x9a, 0x64, 0x15, - 0x79, 0x3c, 0x0f, 0xf8, 0x6d, 0x00, 0x32, 0xfb, 0x81, 0x6f, 0xe5, 0x4f, 0x4a, 0xc8, 0x9f, 0x26, - 0x3a, 0x1b, 0x70, 0x19, 0x13, 0x44, 0xd5, 0xc5, 0xe2, 0x8f, 0xd7, 0xfe, 0x7d, 0x11, 0x7a, 0xad, - 0xa7, 0x17, 0xff, 0x34, 0x2a, 0x17, 0x97, 0x0d, 0xef, 0xe5, 0x65, 0xc3, 0xfb, 0xfb, 0xb2, 0xe1, - 0xfd, 0x72, 0xd5, 0xa8, 0xbc, 0xbc, 0x6a, 0x54, 0xfe, 0xbc, 0x6a, 0x54, 0xbe, 0xfd, 0x28, 0xa7, - 0x65, 0x3e, 0xd9, 0xee, 0x27, 0x44, 0x8f, 0x85, 0xec, 0xdb, 0x97, 0xe6, 0xe8, 0xe3, 0xe6, 0x24, - 0xfb, 0xca, 0xb3, 0xca, 0xed, 0x75, 0xfb, 0xe1, 0xf6, 0xc9, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, - 0xdc, 0xb5, 0x20, 0xe6, 0x03, 0x0a, 0x00, 0x00, + // 896 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x56, 0xbf, 0x6f, 0x1b, 0x37, + 0x14, 0xd6, 0xb5, 0xb6, 0x6b, 0x31, 0xb1, 0x64, 0x5f, 0x64, 0xe7, 0xd0, 0xba, 0x3a, 0x83, 0x40, + 0x0b, 0x2f, 0xb1, 0x9a, 0xb6, 0x93, 0x47, 0x39, 0x48, 0xe3, 0x22, 0x49, 0x5b, 0x3a, 0x45, 0x80, + 0x2e, 0x07, 0xea, 0xf4, 0x22, 0x11, 0xe2, 0x1d, 0x55, 0x92, 0xfa, 0xe5, 0xa5, 0x43, 0xd1, 0xa9, + 0x4b, 0xc7, 0x2e, 0x05, 0x32, 0xf4, 0x0f, 0xe9, 0xe8, 0x31, 0x63, 0xd1, 0x41, 0x68, 0xed, 0xa5, + 0xb3, 0xff, 0x82, 0xe2, 0xc8, 0x93, 0xee, 0xe4, 0xa8, 0x01, 0x0e, 0xca, 0xa4, 0xe3, 0xf7, 0x9e, + 0xbe, 0xef, 0x23, 0xf9, 0xc8, 0x47, 0xe4, 0x0f, 0x22, 0x80, 0x06, 0x87, 0x21, 0x48, 0xda, 0x81, + 0xc6, 0xf0, 0xfe, 0xfc, 0xfb, 0xa8, 0x2f, 0x85, 0x16, 0xee, 0x76, 0x92, 0x70, 0x34, 0x07, 0x87, + 0xf7, 0xdf, 0xaf, 0x75, 0x44, 0x47, 0x98, 0x60, 0x23, 0xf9, 0xb2, 0x79, 0xf8, 0x8f, 0x75, 0xb4, + 0xf1, 0x35, 0x95, 0x34, 0x52, 0xee, 0x6f, 0x0e, 0xaa, 0x87, 0x22, 0xea, 0x73, 0xd0, 0x10, 0x70, + 0xf6, 0xfd, 0x80, 0xb5, 0xa9, 0x66, 0x22, 0x0e, 0x74, 0x57, 0x82, 0xea, 0x0a, 0xde, 0xf6, 0xde, + 0x39, 0x70, 0x0e, 0xcb, 0xcd, 0xe7, 0x17, 0x53, 0xbf, 0xf4, 0xd7, 0xd4, 0xff, 0xb8, 0xc3, 0x74, + 0x77, 0xd0, 0x3a, 0x0a, 0x45, 0xd4, 0x08, 0x85, 0x8a, 0x84, 0x4a, 0x7f, 0xee, 0xa9, 0x76, 0xaf, + 0xa1, 0x27, 0x7d, 0x50, 0x47, 0x0f, 0x20, 0xbc, 0x9e, 0xfa, 0x1f, 0x4d, 0x68, 0xc4, 0x8f, 0xf1, + 0x9b, 0xd9, 0x31, 0xd9, 0x9f, 0x25, 0x3c, 0xce, 0xe2, 0xcf, 0x66, 0x61, 0xf7, 0x07, 0x54, 0x8b, + 0x58, 0xcc, 0xa2, 0x41, 0x14, 0x84, 0x5c, 0x28, 0x08, 0x5e, 0xd0, 0x50, 0x0b, 0xe9, 0xbd, 0x6b, + 0x4c, 0x3d, 0x29, 0x6c, 0xea, 0x03, 0x6b, 0x6a, 0x19, 0x27, 0x26, 0x6e, 0x0a, 0x9f, 0x24, 0xe8, + 0x43, 0x03, 0x26, 0x06, 0x84, 0xa4, 0x21, 0x87, 0x40, 0xc2, 0x88, 0xca, 0xf6, 0xcc, 0xc0, 0xda, + 0x6a, 0x06, 0x96, 0x71, 0x62, 0xe2, 0x5a, 0x98, 0x18, 0x34, 0x35, 0xf0, 0x93, 0x83, 0xf6, 0x54, + 0x44, 0x39, 0x5f, 0x58, 0x40, 0xc5, 0xce, 0xc1, 0x5b, 0x37, 0x1e, 0xbe, 0x2a, 0xec, 0xe1, 0x43, + 0xeb, 0x61, 0x39, 0x2b, 0x26, 0x35, 0x13, 0xc8, 0x6d, 0xc7, 0x19, 0x3b, 0x07, 0xe3, 0xa3, 0xcd, + 0x24, 0x84, 0x7a, 0xe1, 0x2f, 0x2f, 0x00, 0xbc, 0x8d, 0xd5, 0x7c, 0x2c, 0x67, 0xc5, 0xa4, 0x66, + 0x03, 0x39, 0x23, 0x0f, 0x01, 0x8e, 0xd7, 0x7e, 0x7d, 0xe9, 0x97, 0xf0, 0xef, 0x15, 0xb4, 0xfe, + 0x4c, 0xf4, 0x20, 0x76, 0x3f, 0x47, 0xa8, 0x45, 0x15, 0x04, 0x6d, 0x88, 0x45, 0xe4, 0x39, 0xc6, + 0xca, 0xee, 0xf5, 0xd4, 0xdf, 0xb1, 0xe4, 0x59, 0x0c, 0x93, 0x72, 0x32, 0x78, 0x90, 0x7c, 0xbb, + 0x31, 0xaa, 0x48, 0x50, 0x20, 0x87, 0xf3, 0x8a, 0xb2, 0x65, 0xfe, 0x45, 0xe1, 0x49, 0xec, 0x5a, + 0x9d, 0x45, 0x36, 0x4c, 0xb6, 0x52, 0x20, 0xdd, 0xc5, 0x11, 0xda, 0x09, 0x05, 0xe7, 0x54, 0x83, + 0xa4, 0x3c, 0x18, 0x01, 0xeb, 0x74, 0x75, 0x5a, 0xc4, 0x5f, 0x16, 0x96, 0xf4, 0x66, 0x27, 0xeb, + 0x06, 0x21, 0x26, 0xdb, 0x19, 0xf6, 0xdc, 0x40, 0xee, 0x8f, 0x0e, 0xda, 0x5d, 0x7e, 0xae, 0x6d, + 0x05, 0x3f, 0x2d, 0xac, 0xbe, 0x6f, 0xd5, 0xff, 0xe7, 0x38, 0xd7, 0xf8, 0xb2, 0x63, 0xac, 0xd0, + 0xb6, 0xd9, 0x88, 0x96, 0x90, 0x52, 0x8c, 0x02, 0x49, 0xf5, 0xac, 0x7a, 0x4f, 0x0b, 0xeb, 0xdf, + 0xcd, 0x6d, 0x6c, 0x8e, 0x0f, 0x93, 0x4a, 0x02, 0x35, 0x0d, 0x42, 0xa8, 0x86, 0x44, 0xb4, 0xc7, + 0xe2, 0xde, 0x82, 0xe8, 0xc6, 0x6a, 0xa2, 0x37, 0xf9, 0x30, 0xa9, 0x24, 0x50, 0x4e, 0xb4, 0x8f, + 0xaa, 0x11, 0x1d, 0x2f, 0x68, 0xbe, 0x67, 0x34, 0x1f, 0x15, 0xd6, 0xdc, 0x4b, 0xef, 0xaa, 0x45, + 0x3a, 0x4c, 0xb6, 0x22, 0x3a, 0xce, 0x29, 0xea, 0x74, 0x9a, 0x03, 0xcd, 0x38, 0x3b, 0x37, 0x0b, + 0xef, 0x6d, 0xbe, 0x85, 0x69, 0xe6, 0xf8, 0x30, 0xa9, 0x26, 0xd0, 0xb7, 0x19, 0xf2, 0x5a, 0x5d, + 0xb1, 0x38, 0x84, 0x58, 0xb3, 0x21, 0x78, 0xe5, 0xb7, 0x57, 0x57, 0x73, 0xd2, 0xc5, 0xba, 0x3a, + 0x9d, 0xc1, 0xee, 0x31, 0xba, 0xad, 0x26, 0x51, 0x4b, 0xf0, 0xf4, 0xf8, 0x23, 0xa3, 0x7d, 0xf7, + 0x7a, 0xea, 0xdf, 0x49, 0xef, 0xb8, 0x5c, 0x14, 0x93, 0x5b, 0x76, 0x68, 0xaf, 0x80, 0x06, 0xda, + 0x84, 0x71, 0x5f, 0xc4, 0x10, 0x6b, 0xef, 0xd6, 0x81, 0x73, 0xb8, 0xd5, 0xbc, 0x73, 0x3d, 0xf5, + 0xab, 0xf6, 0x7f, 0xb3, 0x08, 0x26, 0xf3, 0x24, 0xf7, 0x11, 0xda, 0x81, 0x98, 0xb6, 0x38, 0x04, + 0x91, 0xea, 0x04, 0x6a, 0xd0, 0xef, 0xf3, 0x89, 0x77, 0xfb, 0xc0, 0x39, 0xdc, 0x6c, 0xee, 0x67, + 0xa7, 0xf2, 0xb5, 0x14, 0x4c, 0xaa, 0x16, 0x7b, 0xa2, 0x3a, 0x67, 0x06, 0xb9, 0xc1, 0x64, 0x37, + 0xd7, 0xdb, 0x7a, 0x03, 0x93, 0x4d, 0xc9, 0x33, 0xd9, 0x02, 0x70, 0xf7, 0x51, 0xb9, 0xc5, 0x69, + 0xd8, 0xe3, 0x4c, 0x69, 0xaf, 0x92, 0x30, 0x90, 0x0c, 0x30, 0xdd, 0x93, 0x8e, 0x83, 0xdc, 0x45, + 0xa1, 0xba, 0x54, 0x82, 0x57, 0x5d, 0xb1, 0x7b, 0x2e, 0xe1, 0x4c, 0xba, 0x27, 0x1d, 0x9f, 0xcc, + 0xd1, 0xb3, 0x04, 0x34, 0x4d, 0x23, 0xc9, 0xb6, 0x2b, 0xb1, 0x50, 0xa2, 0xdb, 0xab, 0x35, 0x8d, + 0xe5, 0xac, 0x98, 0x24, 0x13, 0xb6, 0xab, 0x9c, 0xaf, 0xd6, 0x9f, 0x1d, 0xe4, 0x45, 0x2c, 0xce, + 0xbb, 0xb6, 0xf5, 0xc4, 0xf4, 0xc4, 0xdb, 0x31, 0x4e, 0xbe, 0x29, 0xec, 0xc4, 0x9f, 0xbf, 0x25, + 0x96, 0xf2, 0x62, 0xb2, 0x17, 0xb1, 0x38, 0x5b, 0x91, 0xc7, 0xb3, 0x80, 0xdb, 0x42, 0x28, 0xb3, + 0xef, 0xb9, 0x46, 0xfe, 0xa4, 0x80, 0xfc, 0x69, 0xac, 0xb3, 0x06, 0x97, 0x31, 0x61, 0x52, 0x9e, + 0x4f, 0xfe, 0x78, 0xed, 0xdf, 0x97, 0xbe, 0xd3, 0x7c, 0x7a, 0xf1, 0x4f, 0xbd, 0x74, 0x71, 0x59, + 0x77, 0x5e, 0x5d, 0xd6, 0x9d, 0xbf, 0x2f, 0xeb, 0xce, 0x2f, 0x57, 0xf5, 0xd2, 0xab, 0xab, 0x7a, + 0xe9, 0xcf, 0xab, 0x7a, 0xe9, 0xbb, 0x4f, 0x72, 0x5a, 0xc9, 0xd3, 0xf1, 0x5e, 0x0c, 0x7a, 0x24, + 0x64, 0xcf, 0x0c, 0x1a, 0xc3, 0x4f, 0x1b, 0xe3, 0xec, 0xb5, 0x69, 0x94, 0x5b, 0x1b, 0xe6, 0x01, + 0xf9, 0xd9, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xb0, 0xe6, 0xcb, 0xbb, 0x8b, 0x0a, 0x00, 0x00, } func (this *Token) Equal(that interface{}) bool { @@ -345,6 +350,16 @@ func (m *Params) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + { + size := m.DirectLiquidationFee.Size() + i -= size + if _, err := m.DirectLiquidationFee.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintLeverage(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x32 { size := m.SmallLiquidationSize.Size() i -= size @@ -611,6 +626,8 @@ func (m *Params) Size() (n int) { n += 1 + l + sovLeverage(uint64(l)) l = m.SmallLiquidationSize.Size() n += 1 + l + sovLeverage(uint64(l)) + l = m.DirectLiquidationFee.Size() + n += 1 + l + sovLeverage(uint64(l)) return n } @@ -838,6 +855,40 @@ func (m *Params) Unmarshal(dAtA []byte) error { return err } iNdEx = postIndex + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field DirectLiquidationFee", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowLeverage + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthLeverage + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthLeverage + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.DirectLiquidationFee.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipLeverage(dAtA[iNdEx:]) diff --git a/x/leverage/types/params.go b/x/leverage/types/params.go index 2641e3ac19..b59232c74e 100644 --- a/x/leverage/types/params.go +++ b/x/leverage/types/params.go @@ -15,6 +15,7 @@ var ( KeyMinimumCloseFactor = []byte("MinimumCloseFactor") KeyOracleRewardFactor = []byte("OracleRewardFactor") KeySmallLiquidationSize = []byte("SmallLiquidationSize") + KeyDirectLiquidationFee = []byte("DirectLiquidationFee") ) var ( @@ -22,6 +23,7 @@ var ( defaultMinimumCloseFactor = sdk.MustNewDecFromStr("0.01") defaultOracleRewardFactor = sdk.MustNewDecFromStr("0.01") defaultSmallLiquidationSize = sdk.MustNewDecFromStr("100.00") + defaultDirectLiquidationFee = sdk.MustNewDecFromStr("0.1") ) func NewParams() Params { @@ -52,6 +54,11 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { &p.SmallLiquidationSize, validateSmallLiquidationSize, ), + paramtypes.NewParamSetPair( + KeyDirectLiquidationFee, + &p.DirectLiquidationFee, + validateDirectLiquidationFee, + ), } } @@ -74,6 +81,7 @@ func DefaultParams() Params { MinimumCloseFactor: defaultMinimumCloseFactor, OracleRewardFactor: defaultOracleRewardFactor, SmallLiquidationSize: defaultSmallLiquidationSize, + DirectLiquidationFee: defaultDirectLiquidationFee, } } @@ -91,6 +99,9 @@ func (p Params) Validate() error { if err := validateSmallLiquidationSize(p.SmallLiquidationSize); err != nil { return err } + if err := validateDirectLiquidationFee(p.DirectLiquidationFee); err != nil { + return err + } return nil } @@ -151,3 +162,19 @@ func validateSmallLiquidationSize(i interface{}) error { return nil } + +func validateDirectLiquidationFee(i interface{}) error { + v, ok := i.(sdk.Dec) + if !ok { + return fmt.Errorf("invalid parameter type: %T", i) + } + + if v.IsNegative() { + return fmt.Errorf("direct liquidation fee cannot be negative: %d", v) + } + if v.GTE(sdk.OneDec()) { + return fmt.Errorf("direct liquidation fee must be less than 1: %d", v) + } + + return nil +} diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index 20bbb56ff1..6ac3ccc64a 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -13,9 +13,33 @@ const ( UTokenPrefix = "u/" ) -// UTokenFromTokenDenom returns the uToken denom given a token denom. -func UTokenFromTokenDenom(tokenDenom string) string { - return UTokenPrefix + tokenDenom +// HasUTokenPrefix detects the uToken prefix on a denom. +func HasUTokenPrefix(denom string) bool { + return strings.HasPrefix(denom, UTokenPrefix) +} + +// ToUTokenDenom adds the uToken prefix to a denom. Returns an empty string +// instead if the prefix was already present. +func ToUTokenDenom(denom string) string { + if HasUTokenPrefix(denom) { + return "" + } + return UTokenPrefix + denom +} + +// ToTokenDenom strips the uToken prefix from a denom, or returns an empty +// string if it was not present. Also returns an empty string if the prefix +// was repeated multiple times. +func ToTokenDenom(denom string) string { + if !HasUTokenPrefix(denom) { + return "" + } + s := strings.TrimPrefix(denom, UTokenPrefix) + if HasUTokenPrefix(s) { + // denom started with "u/u/" + return "" + } + return s } // Validate performs validation on an Token type returning an error if the Token @@ -24,7 +48,7 @@ func (t Token) Validate() error { if err := sdk.ValidateDenom(t.BaseDenom); err != nil { return err } - if strings.HasPrefix(t.BaseDenom, UTokenPrefix) { + if HasUTokenPrefix(t.BaseDenom) { // prevent base asset denoms that start with "u/" return sdkerrors.Wrap(ErrInvalidAsset, t.BaseDenom) } @@ -32,8 +56,8 @@ func (t Token) Validate() error { if err := sdk.ValidateDenom(t.SymbolDenom); err != nil { return err } - if strings.HasPrefix(t.SymbolDenom, UTokenPrefix) { - // prevent symbol (ticker) denoms that start with "u/" + if HasUTokenPrefix(t.SymbolDenom) { + // prevent symbol denoms that start with "u/" return sdkerrors.Wrap(ErrInvalidAsset, t.SymbolDenom) } diff --git a/x/leverage/types/token_test.go b/x/leverage/types/token_test.go index 52985c01ea..229491b8c3 100644 --- a/x/leverage/types/token_test.go +++ b/x/leverage/types/token_test.go @@ -9,11 +9,34 @@ import ( "github.com/umee-network/umee/v2/x/leverage/types" ) -func TestUTokenFromTokenDenom(t *testing.T) { - tokenDenom := "uumee" - uTokenDenom := types.UTokenFromTokenDenom(tokenDenom) - require.Equal(t, "u/"+tokenDenom, uTokenDenom) - require.NoError(t, sdk.ValidateDenom(uTokenDenom)) +func TestToTokenDenom(t *testing.T) { + // Turns uToken denoms into base tokens + require.Equal(t, "uumee", types.ToTokenDenom("u/uumee")) + require.Equal(t, "ibc/abcd", types.ToTokenDenom("u/ibc/abcd")) + + // Empty return for base tokens + require.Equal(t, "", types.ToTokenDenom("uumee")) + require.Equal(t, "", types.ToTokenDenom("ibc/abcd")) + + // Empty return on repreated prefix + require.Equal(t, "", types.ToTokenDenom("u/u/abcd")) + + // Edge cases + require.Equal(t, "", types.ToTokenDenom("u/")) + require.Equal(t, "", types.ToTokenDenom("")) +} + +func TestToUTokenDenom(t *testing.T) { + // Turns base token denoms into base uTokens + require.Equal(t, "u/uumee", types.ToUTokenDenom("uumee")) + require.Equal(t, "u/ibc/abcd", types.ToUTokenDenom("ibc/abcd")) + + // Empty return for uTokens + require.Equal(t, "", types.ToUTokenDenom("u/uumee")) + require.Equal(t, "", types.ToUTokenDenom("u/ibc/abcd")) + + // Edge cases + require.Equal(t, "u/", types.ToUTokenDenom("")) } func validToken() types.Token { diff --git a/x/leverage/types/tx.pb.go b/x/leverage/types/tx.pb.go index 9a3c7fda1f..3c518eff3f 100644 --- a/x/leverage/types/tx.pb.go +++ b/x/leverage/types/tx.pb.go @@ -287,9 +287,10 @@ type MsgLiquidate struct { // Repayment is the maximum amount of base tokens that the liquidator is willing // to repay. Repayment types.Coin `protobuf:"bytes,3,opt,name=repayment,proto3" json:"repayment"` - // RewardDenom is the base token denom that the liquidator is willing to accept - // as a liquidation reward. The uToken equivalent of any base token rewards - // will be taken from the borrower's collateral. + // RewardDenom is the denom that the liquidator will receive as a liquidation reward. + // If it is a uToken, the liquidator will receive uTokens from the borrower's + // collateral. If it is a base token, the uTokens will be redeemed directly at + // a reduced Liquidation Incentive, and the liquidator will receive base tokens. RewardDenom string `protobuf:"bytes,4,opt,name=reward_denom,json=rewardDenom,proto3" json:"reward_denom,omitempty"` }