diff --git a/CHANGELOG.md b/CHANGELOG.md index a19659a58d..970bf09e03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ Ref: https://keepachangelog.com/en/1.0.0/ - [1012](https://github.com/umee-network/umee/pull/1012) Improve negative time elapsed error message - [1236](https://github.com/umee-network/umee/pull/1236) Improve leverage event fields. - [1294](https://github.com/umee-network/umee/pull/1294) Simplify window progress query math. +- [1300](https://github.com/umee-network/umee/pull/1300) Improve leverage test suite and error specificity. ### Bug Fixes diff --git a/app/app.go b/app/app.go index ed05faa67f..a858a62182 100644 --- a/app/app.go +++ b/app/app.go @@ -704,7 +704,6 @@ func (app *UmeeApp) setAnteHandler(txConfig client.TxConfig) { OracleKeeper: app.OracleKeeper, }, ) - if err != nil { panic(err) } diff --git a/x/leverage/client/tests/suite.go b/x/leverage/client/tests/suite.go index edf3315bde..b4a55e910a 100644 --- a/x/leverage/client/tests/suite.go +++ b/x/leverage/client/tests/suite.go @@ -17,8 +17,6 @@ import ( type IntegrationTestSuite struct { suite.Suite - abort bool // stop interdependent tests on the first error for clarity - cfg network.Config network *network.Network } @@ -44,30 +42,29 @@ func (s *IntegrationTestSuite) TearDownSuite() { s.network.Cleanup() } -// TestCases are queries and transactions that can be run, and return a boolean -// which indicates to abort the test suite if true -type TestCase interface { - Run(s *IntegrationTestSuite) bool +// runTestQuery +func (s *IntegrationTestSuite) runTestQueries(tqs ...testQuery) { + for _, t := range tqs { + t.Run(s) + } } // runTestCases runs test transactions or queries, stopping early if an error occurs -func (s *IntegrationTestSuite) runTestCases(tcs ...TestCase) { - for _, t := range tcs { - if !s.abort { - s.abort = t.Run(s) - } +func (s *IntegrationTestSuite) runTestTransactions(txs ...testTransaction) { + for _, t := range txs { + t.Run(s) } } type testTransaction struct { - name string + msg string command *cobra.Command args []string expectedErr *errors.Error } type testQuery struct { - name string + msg string command *cobra.Command args []string expectErr bool @@ -75,7 +72,7 @@ type testQuery struct { expectedResponse proto.Message } -func (t testTransaction) Run(s *IntegrationTestSuite) (abort bool) { +func (t testTransaction) Run(s *IntegrationTestSuite) { clientCtx := s.network.Validators[0].ClientCtx txFlags := []string{ @@ -87,36 +84,21 @@ func (t testTransaction) Run(s *IntegrationTestSuite) (abort bool) { t.args = append(t.args, txFlags...) - s.Run(t.name, func() { - out, err := clitestutil.ExecTestCLICmd(clientCtx, t.command, t.args) - s.Require().NoError(err) - if err != nil { - abort = true - } - - resp := &sdk.TxResponse{} - err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), resp) - s.Require().NoError(err, out.String()) - if err != nil { - abort = true - } - - if t.expectedErr == nil { - s.Require().Equal(0, int(resp.Code), "events %v", resp.Events) - if int(resp.Code) != 0 { - abort = true - } - } else { - s.Require().Equal(int(t.expectedErr.ABCICode()), int(resp.Code)) - if int(resp.Code) != int(t.expectedErr.ABCICode()) { - abort = true - } - } - }) - return abort + out, err := clitestutil.ExecTestCLICmd(clientCtx, t.command, t.args) + s.Require().NoError(err, t.msg) + + resp := &sdk.TxResponse{} + err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), resp) + s.Require().NoError(err, t.msg) + + if t.expectedErr == nil { + s.Require().Equal(0, int(resp.Code), t.msg) + } else { + s.Require().Equal(int(t.expectedErr.ABCICode()), int(resp.Code), t.msg) + } } -func (t testQuery) Run(s *IntegrationTestSuite) (abort bool) { +func (t testQuery) Run(s *IntegrationTestSuite) { clientCtx := s.network.Validators[0].ClientCtx queryFlags := []string{ @@ -125,31 +107,16 @@ func (t testQuery) Run(s *IntegrationTestSuite) (abort bool) { t.args = append(t.args, queryFlags...) - s.Run(t.name, func() { - out, err := clitestutil.ExecTestCLICmd(clientCtx, t.command, t.args) - - if t.expectErr { - s.Require().Error(err) - if err == nil { - abort = true - } - } else { - s.Require().NoError(err) - if err != nil { - abort = true - } - - err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), t.responseType) - s.Require().NoError(err, out.String()) - if err != nil { - abort = true - } - - s.Require().Equal(t.expectedResponse, t.responseType) - if !s.Assert().Equal(t.expectedResponse, t.responseType) { - abort = true - } - } - }) - return abort + out, err := clitestutil.ExecTestCLICmd(clientCtx, t.command, t.args) + + if t.expectErr { + s.Require().Error(err, t.msg) + } else { + s.Require().NoError(err, t.msg) + + err = clientCtx.Codec.UnmarshalJSON(out.Bytes(), t.responseType) + s.Require().NoError(err, t.msg) + + s.Require().Equal(t.expectedResponse, t.responseType) + } } diff --git a/x/leverage/client/tests/tests.go b/x/leverage/client/tests/tests.go index 0e32c5a2d4..e05ca96012 100644 --- a/x/leverage/client/tests/tests.go +++ b/x/leverage/client/tests/tests.go @@ -9,8 +9,8 @@ import ( ) func (s *IntegrationTestSuite) TestInvalidQueries() { - invalidQueries := []TestCase{ - testQuery{ + invalidQueries := []testQuery{ + { "query market summary - invalid denom", cli.GetCmdQueryMarketSummary(), []string{ @@ -20,7 +20,7 @@ func (s *IntegrationTestSuite) TestInvalidQueries() { nil, nil, }, - testQuery{ + { "query account balances - invalid address", cli.GetCmdQueryAccountBalances(), []string{ @@ -30,7 +30,7 @@ func (s *IntegrationTestSuite) TestInvalidQueries() { nil, nil, }, - testQuery{ + { "query account summary - invalid address", cli.GetCmdQueryAccountSummary(), []string{ @@ -43,7 +43,7 @@ func (s *IntegrationTestSuite) TestInvalidQueries() { } // These queries do not require any borrower setup because they contain invalid arguments - s.runTestCases(invalidQueries...) + s.runTestQueries(invalidQueries...) } func (s *IntegrationTestSuite) TestLeverageScenario() { @@ -51,8 +51,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { oraclePrice := sdk.MustNewDecFromStr("0.00003421") - initialQueries := []TestCase{ - testQuery{ + initialQueries := []testQuery{ + { "query params", cli.GetCmdQueryParams(), []string{}, @@ -62,7 +62,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { Params: types.DefaultParams(), }, }, - testQuery{ + { "query registered tokens", cli.GetCmdQueryRegisteredTokens(), []string{}, @@ -94,7 +94,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { }, }, }, - testQuery{ + { "query market summary - zero supply", cli.GetCmdQueryMarketSummary(), []string{ @@ -202,8 +202,8 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { nil, } - nonzeroQueries := []TestCase{ - testQuery{ + nonzeroQueries := []testQuery{ + { "query account balances", cli.GetCmdQueryAccountBalances(), []string{ @@ -223,7 +223,7 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { ), }, }, - testQuery{ + { "query account summary", cli.GetCmdQueryAccountSummary(), []string{ @@ -250,20 +250,20 @@ func (s *IntegrationTestSuite) TestLeverageScenario() { } // These queries do not require any borrower setup - s.runTestCases(initialQueries...) + s.runTestQueries(initialQueries...) // These transactions will set up nonzero leverage positions and allow nonzero query results - s.runTestCases( + s.runTestTransactions( supply, addCollateral, borrow, ) // These queries run while the supplying and borrowing is active to produce nonzero output - s.runTestCases(nonzeroQueries...) + s.runTestQueries(nonzeroQueries...) // These transactions run after nonzero queries are finished - s.runTestCases( + s.runTestTransactions( liquidate, repay, removeCollateral, diff --git a/x/leverage/fixtures/token.go b/x/leverage/fixtures/token.go index 3b90e0649a..813fd28b88 100644 --- a/x/leverage/fixtures/token.go +++ b/x/leverage/fixtures/token.go @@ -6,6 +6,11 @@ import ( "github.com/umee-network/umee/v3/x/leverage/types" ) +const ( + // AtomDenom is an ibc denom to be used as ATOM's BaseDenom during testing + AtomDenom = "ibc/CDC4587874B85BEA4FCEC3CEA5A1195139799A1FEE711A07D972537E18FDA39D" +) + // Token returns a valid token func Token(base, symbol string) types.Token { return types.Token{ diff --git a/x/leverage/keeper/borrows_test.go b/x/leverage/keeper/borrows_test.go index 1b4fffc055..2e5b3ea76a 100644 --- a/x/leverage/keeper/borrows_test.go +++ b/x/leverage/keeper/borrows_test.go @@ -7,198 +7,213 @@ import ( ) func (s *IntegrationTestSuite) TestGetBorrow() { + ctx, require := s.ctx, s.Require() + // get uumee borrow amount of empty account address (zero) - borrowed := s.tk.GetBorrow(s.ctx, sdk.AccAddress{}, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 0), borrowed) + borrowed := s.tk.GetBorrow(ctx, sdk.AccAddress{}, umeeDenom) + require.Equal(coin(umeeDenom, 0), borrowed) - // creates account which has borrowed 123 uumee - addr := s.setupAccount(umeeDenom, 1000, 1000, 123, true) + // creates account which has supplied and collateralized 1000 uumee, and borrowed 123 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + s.borrow(addr, coin(umeeDenom, 123)) // confirm borrowed amount is 123 uumee - borrowed = s.tk.GetBorrow(s.ctx, addr, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 123), borrowed) + borrowed = s.tk.GetBorrow(ctx, addr, umeeDenom) + require.Equal(coin(umeeDenom, 123), borrowed) // unregistered denom (zero) - borrowed = s.tk.GetBorrow(s.ctx, addr, "abcd") - s.Require().Equal(sdk.NewInt64Coin("abcd", 0), borrowed) + borrowed = s.tk.GetBorrow(ctx, addr, "abcd") + require.Equal(coin("abcd", 0), borrowed) // we do not test empty denom, as that will cause a panic } func (s *IntegrationTestSuite) TestSetBorrow() { + ctx, require := s.ctx, s.Require() + // empty account address - err := s.tk.SetBorrow(s.ctx, sdk.AccAddress{}, sdk.NewInt64Coin(umeeDenom, 123)) - s.Require().EqualError(err, "empty address") + err := s.tk.SetBorrow(ctx, sdk.AccAddress{}, coin(umeeDenom, 123)) + require.ErrorIs(err, types.ErrEmptyAddress) - addr := sdk.AccAddress{0x00} + addr := s.newAccount() // set nonzero borrow, and confirm effect - err = s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 123)) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 123), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) + err = s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 123)) + require.NoError(err) + require.Equal(coin(umeeDenom, 123), s.tk.GetBorrow(ctx, addr, umeeDenom)) // set zero borrow, and confirm effect - err = s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 0)) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 0), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) + err = s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 0)) + require.NoError(err) + require.Equal(coin(umeeDenom, 0), s.tk.GetBorrow(ctx, addr, umeeDenom)) // unregistered (but valid) denom - err = s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin("abcd", 123)) - s.Require().NoError(err) + err = s.tk.SetBorrow(ctx, addr, coin("abcd", 123)) + require.NoError(err) // interest scalar test - ensure borrowing smallest possible amount doesn't round to zero at scalar = 1.0001 - s.Require().NoError(s.tk.SetInterestScalar(s.ctx, umeeDenom, sdk.MustNewDecFromStr("1.0001"))) - s.Require().NoError(s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 1))) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 1), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) + require.NoError(s.tk.SetInterestScalar(ctx, umeeDenom, sdk.MustNewDecFromStr("1.0001"))) + require.NoError(s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 1))) + require.Equal(coin(umeeDenom, 1), s.tk.GetBorrow(ctx, addr, umeeDenom)) // interest scalar test - scalar changing after borrow (as it does when interest accrues) - s.Require().NoError(s.tk.SetInterestScalar(s.ctx, umeeDenom, sdk.MustNewDecFromStr("1.0"))) - s.Require().NoError(s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 200))) - s.Require().NoError(s.tk.SetInterestScalar(s.ctx, umeeDenom, sdk.MustNewDecFromStr("2.33"))) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 466), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) + require.NoError(s.tk.SetInterestScalar(ctx, umeeDenom, sdk.MustNewDecFromStr("1.0"))) + require.NoError(s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 200))) + require.NoError(s.tk.SetInterestScalar(ctx, umeeDenom, sdk.MustNewDecFromStr("2.33"))) + require.Equal(coin(umeeDenom, 466), s.tk.GetBorrow(ctx, addr, umeeDenom)) // interest scalar extreme case - rounding up becomes apparent at high borrow amount - s.Require().NoError(s.tk.SetInterestScalar(s.ctx, umeeDenom, sdk.MustNewDecFromStr("555444333222111"))) - s.Require().NoError(s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 1))) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 1), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) - s.Require().NoError(s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 20000))) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 20001), s.tk.GetBorrow(s.ctx, addr, umeeDenom)) - - // we do not test empty denom, as that will cause a panic + require.NoError(s.tk.SetInterestScalar(ctx, umeeDenom, sdk.MustNewDecFromStr("555444333222111"))) + require.NoError(s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 1))) + require.Equal(coin(umeeDenom, 1), s.tk.GetBorrow(ctx, addr, umeeDenom)) + require.NoError(s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 20000))) + require.Equal(coin(umeeDenom, 20001), s.tk.GetBorrow(ctx, addr, umeeDenom)) } func (s *IntegrationTestSuite) TestGetTotalBorrowed() { + ctx, require := s.ctx, s.Require() + // unregistered denom (zero) - borrowed := s.tk.GetTotalBorrowed(s.ctx, "abcd") - s.Require().Equal(sdk.NewInt64Coin("abcd", 0), borrowed) + borrowed := s.tk.GetTotalBorrowed(ctx, "abcd") + require.Equal(coin("abcd", 0), borrowed) - // creates account which has borrowed 123 uumee - _ = s.setupAccount(umeeDenom, 1000, 1000, 123, true) + // creates account which has supplied and collateralized 1000 uumee, and borrowed 123 uumee + borrower := s.newAccount(coin(umeeDenom, 1000)) + s.supply(borrower, coin(umeeDenom, 1000)) + s.collateralize(borrower, coin("u/"+umeeDenom, 1000)) + s.borrow(borrower, coin(umeeDenom, 123)) // confirm total borrowed amount is 123 uumee - borrowed = s.tk.GetTotalBorrowed(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 123), borrowed) + borrowed = s.tk.GetTotalBorrowed(ctx, umeeDenom) + require.Equal(coin(umeeDenom, 123), borrowed) - // creates account which has borrowed 456 uumee - _ = s.setupAccount(umeeDenom, 2000, 2000, 456, true) + // creates account which has supplied and collateralized 1000 uumee, and borrowed 234 uumee + borrower2 := s.newAccount(coin(umeeDenom, 1000)) + s.supply(borrower2, coin(umeeDenom, 1000)) + s.collateralize(borrower2, coin("u/"+umeeDenom, 1000)) + s.borrow(borrower2, coin(umeeDenom, 234)) - // confirm total borrowed amount is 579 uumee - borrowed = s.tk.GetTotalBorrowed(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 579), borrowed) + // confirm total borrowed amount is 357 uumee + borrowed = s.tk.GetTotalBorrowed(ctx, umeeDenom) + require.Equal(coin(umeeDenom, 357), borrowed) // interest scalar test - scalar changing after borrow (as it does when interest accrues) - s.Require().NoError(s.tk.SetInterestScalar(s.ctx, umeeDenom, sdk.MustNewDecFromStr("2.00"))) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 1158), s.tk.GetTotalBorrowed(s.ctx, umeeDenom)) - - // we do not test empty denom, as that will cause a panic + require.NoError(s.tk.SetInterestScalar(ctx, umeeDenom, sdk.MustNewDecFromStr("2.00"))) + require.Equal(coin(umeeDenom, 714), s.tk.GetTotalBorrowed(ctx, umeeDenom)) } func (s *IntegrationTestSuite) TestGetAvailableToBorrow() { + ctx, require := s.ctx, s.Require() + // unregistered denom (zero) - available := s.tk.GetAvailableToBorrow(s.ctx, "abcd") - s.Require().Equal(sdk.ZeroInt(), available) + available := s.tk.GetAvailableToBorrow(ctx, "abcd") + require.Equal(sdk.ZeroInt(), available) - // creates account which has supplied 1000 uumee, and borrowed 0 uumee - _ = s.setupAccount(umeeDenom, 1000, 1000, 0, true) + // creates account which has supplied and collateralized 1000 uumee + supplier := s.newAccount(coin(umeeDenom, 1000)) + s.supply(supplier, coin(umeeDenom, 1000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 1000)) // confirm lending pool is 1000 uumee - available = s.tk.GetAvailableToBorrow(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt(1000), available) + available = s.tk.GetAvailableToBorrow(ctx, umeeDenom) + require.Equal(sdk.NewInt(1000), available) - // creates account which has supplied 1000 uumee, and borrowed 123 uumee - _ = s.setupAccount(umeeDenom, 1000, 1000, 123, true) + // creates account which has supplied and collateralized 1000 uumee, and borrowed 123 uumee + borrower := s.newAccount(coin(umeeDenom, 1000)) + s.supply(borrower, coin(umeeDenom, 1000)) + s.collateralize(borrower, coin("u/"+umeeDenom, 1000)) + s.borrow(borrower, coin(umeeDenom, 123)) // confirm lending pool is 1877 uumee - available = s.tk.GetAvailableToBorrow(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt(1877), available) + available = s.tk.GetAvailableToBorrow(ctx, umeeDenom) + require.Equal(sdk.NewInt(1877), available) // artificially reserve 200 uumee, reducing available amount to 1677 - s.Require().NoError(s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeDenom, 200))) - available = s.tk.GetAvailableToBorrow(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt(1677), available) - - // we do not test empty denom, as that will cause a panic + s.setReserves(coin(umeeDenom, 200)) + available = s.tk.GetAvailableToBorrow(ctx, umeeDenom) + require.Equal(sdk.NewInt(1677), available) } func (s *IntegrationTestSuite) TestDeriveBorrowUtilization() { + ctx, require := s.ctx, s.Require() + // unregistered denom (0 borrowed and 0 lending pool is considered 100%) - utilization := s.tk.SupplyUtilization(s.ctx, "abcd") - s.Require().Equal(sdk.OneDec(), utilization) + utilization := s.tk.SupplyUtilization(ctx, "abcd") + require.Equal(sdk.OneDec(), utilization) - // creates account which has supplied 1000 uumee, and borrowed 0 uumee - addr := s.setupAccount(umeeDenom, 1000, 1000, 0, true) + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) // All tests below are commented with the following equation in mind: // utilization = (Total Borrowed / (Total Borrowed + Module Balance - Reserved Amount)) // 0% utilization (0 / 0+1000-0) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.ZeroDec(), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.ZeroDec(), utilization) // user borrows 200 uumee, reducing module account to 800 uumee - s.Require().NoError(s.tk.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 200))) + s.borrow(addr, coin(umeeDenom, 200)) // 20% utilization (200 / 200+800-0) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("0.2"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("0.2"), utilization) // artificially reserve 200 uumee - s.Require().NoError(s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeDenom, 200))) + s.setReserves(coin(umeeDenom, 200)) // 25% utilization (200 / 200+800-200) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("0.25"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("0.25"), utilization) - // Setting umee collateral weight to 1.0 to allow user to borrow heavily - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("1") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("1") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // user borrows 600 uumee, reducing module account to 0 uumee - s.Require().NoError(s.tk.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 600))) + // user borrows 600 uumee (disregard borrow limit), reducing module account to 0 uumee + s.forceBorrow(addr, coin(umeeDenom, 600)) // 100% utilization (800 / 800+200-200)) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("1.0"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("1.0"), utilization) // artificially set user borrow to 1200 umee - s.Require().NoError(s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeDenom, 1200))) + require.NoError(s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 1200))) // still 100% utilization (1200 / 1200+200-200) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("1.0"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("1.0"), utilization) // artificially set reserves to 800 uumee - s.Require().NoError(s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeDenom, 800))) + s.setReserves(coin(umeeDenom, 800)) // edge case interpreted as 100% utilization (1200 / 1200+200-800) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("1.0"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("1.0"), utilization) // artificially set reserves to 4000 uumee - s.Require().NoError(s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeDenom, 4000))) + s.setReserves(coin(umeeDenom, 4000)) // impossible case interpreted as 100% utilization (1200 / 1200+200-4000) - utilization = s.tk.SupplyUtilization(s.ctx, umeeDenom) - s.Require().Equal(sdk.MustNewDecFromStr("1.0"), utilization) + utilization = s.tk.SupplyUtilization(ctx, umeeDenom) + require.Equal(sdk.MustNewDecFromStr("1.0"), utilization) } func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { + app, ctx, require := s.app, s.ctx, s.Require() + // Empty coins - borrowLimit, err := s.app.LeverageKeeper.CalculateBorrowLimit(s.ctx, sdk.NewCoins()) - s.Require().NoError(err) - s.Require().Equal(sdk.ZeroDec(), borrowLimit) + borrowLimit, err := app.LeverageKeeper.CalculateBorrowLimit(ctx, sdk.NewCoins()) + require.NoError(err) + require.Equal(sdk.ZeroDec(), borrowLimit) // Unregistered asset - invalidCoins := sdk.NewCoins(sdk.NewInt64Coin("abcd", 1000)) - _, err = s.app.LeverageKeeper.CalculateBorrowLimit(s.ctx, invalidCoins) - s.Require().EqualError(err, "abcd: invalid asset") + invalidCoins := sdk.NewCoins(coin("abcd", 1000)) + _, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, invalidCoins) + require.ErrorIs(err, types.ErrNotUToken) // Create collateral uTokens (1k u/umee) umeeCollatDenom := types.ToUTokenDenom(umeeDenom) - umeeCollateral := sdk.NewCoins(sdk.NewInt64Coin(umeeCollatDenom, 1000000000)) + umeeCollateral := sdk.NewCoins(coin(umeeCollatDenom, 1000_000000)) // Manually compute borrow limit using collateral weight of 0.25 // and placeholder of 1 umee = $4.21. @@ -207,13 +222,13 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { Mul(sdk.MustNewDecFromStr("0.25")) // Check borrow limit vs. manually computed value - borrowLimit, err = s.app.LeverageKeeper.CalculateBorrowLimit(s.ctx, umeeCollateral) - s.Require().NoError(err) - s.Require().Equal(expectedUmeeLimit, borrowLimit) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, umeeCollateral) + require.NoError(err) + require.Equal(expectedUmeeLimit, borrowLimit) // Create collateral atom uTokens (1k u/uatom) - atomCollatDenom := types.ToUTokenDenom(atomIBCDenom) - atomCollateral := sdk.NewCoins(sdk.NewInt64Coin(atomCollatDenom, 1000000000)) + atomCollatDenom := types.ToUTokenDenom(atomDenom) + atomCollateral := sdk.NewCoins(coin(atomCollatDenom, 1000_000000)) // Manually compute borrow limit using collateral weight of 0.25 // and placeholder of 1 atom = $39.38 @@ -222,16 +237,16 @@ func (s *IntegrationTestSuite) TestCalculateBorrowLimit() { Mul(sdk.MustNewDecFromStr("0.25")) // Check borrow limit vs. manually computed value - borrowLimit, err = s.app.LeverageKeeper.CalculateBorrowLimit(s.ctx, atomCollateral) - s.Require().NoError(err) - s.Require().Equal(expectedAtomLimit, borrowLimit) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, atomCollateral) + require.NoError(err) + require.Equal(expectedAtomLimit, borrowLimit) // Compute the expected borrow limit of the two combined collateral coins expectedCombinedLimit := expectedUmeeLimit.Add(expectedAtomLimit) combinedCollateral := umeeCollateral.Add(atomCollateral...) // Check borrow limit vs. manually computed value - borrowLimit, err = s.app.LeverageKeeper.CalculateBorrowLimit(s.ctx, combinedCollateral) - s.Require().NoError(err) - s.Require().Equal(expectedCombinedLimit, borrowLimit) + borrowLimit, err = app.LeverageKeeper.CalculateBorrowLimit(ctx, combinedCollateral) + require.NoError(err) + require.Equal(expectedCombinedLimit, borrowLimit) } diff --git a/x/leverage/keeper/collateral.go b/x/leverage/keeper/collateral.go index a446f48cdf..26077f0db8 100644 --- a/x/leverage/keeper/collateral.go +++ b/x/leverage/keeper/collateral.go @@ -3,7 +3,6 @@ package keeper import ( sdkmath "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/umee-network/umee/v3/x/leverage/types" ) @@ -64,8 +63,8 @@ func (k Keeper) GetCollateralAmount(ctx sdk.Context, borrowerAddr sdk.AccAddress // stored value is cleared. A negative amount or invalid coin causes an error. // This function does not move coins to or from the module account. func (k Keeper) setCollateralAmount(ctx sdk.Context, borrowerAddr sdk.AccAddress, collateral sdk.Coin) error { - if !collateral.IsValid() { - return sdkerrors.Wrap(types.ErrInvalidAsset, collateral.String()) + if err := collateral.Validate(); err != nil { + return err } if borrowerAddr.Empty() { diff --git a/x/leverage/keeper/collateral_test.go b/x/leverage/keeper/collateral_test.go index 9e99bf16fd..8ffdd438a6 100644 --- a/x/leverage/keeper/collateral_test.go +++ b/x/leverage/keeper/collateral_test.go @@ -7,88 +7,111 @@ import ( ) func (s *IntegrationTestSuite) TestGetCollateralAmount() { + ctx, require := s.ctx, s.Require() uDenom := types.ToUTokenDenom(umeeDenom) - // get u/umee collateral amount of empty account address (zero) - collateral := s.tk.GetCollateralAmount(s.ctx, sdk.AccAddress{}, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 0), collateral) + // get u/umee collateral amount of empty account address + collateral := s.tk.GetCollateralAmount(ctx, sdk.AccAddress{}, uDenom) + require.Equal(coin(uDenom, 0), collateral) - // get u/umee collateral amount of non-empty account address (zero) - collateral = s.tk.GetCollateralAmount(s.ctx, sdk.AccAddress{0x01}, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 0), collateral) + // fund an account + addr := s.newAccount(coin(umeeDenom, 1000)) - // creates account which has 1000 u/umee but not enabled as collateral - addr := s.setupAccount(umeeDenom, 1000, 1000, 0, false) + // get u/umee collateral amount of non-empty account address + collateral = s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 0), collateral) + + // supply 1000 u/uumee but do not collateralize + s.supply(addr, coin(umeeDenom, 1000)) // confirm collateral amount is 0 u/uumee - collateral = s.tk.GetCollateralAmount(s.ctx, addr, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 0), collateral) // enable u/umee as collateral - s.Require().NoError(s.tk.Collateralize(s.ctx, addr, sdk.NewInt64Coin(uDenom, 1000))) + s.collateralize(addr, coin(uDenom, 1000)) // confirm collateral amount is 1000 u/uumee - collateral = s.tk.GetCollateralAmount(s.ctx, addr, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 1000), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 1000), collateral) // collateral amount of non-utoken denom (zero) - collateral = s.tk.GetCollateralAmount(s.ctx, addr, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, umeeDenom) + require.Equal(coin(umeeDenom, 0), collateral) // collateral amount of unregistered denom (zero) - collateral = s.tk.GetCollateralAmount(s.ctx, addr, "abcd") - s.Require().Equal(sdk.NewInt64Coin("abcd", 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, "abcd") + require.Equal(coin("abcd", 0), collateral) // disable u/umee as collateral - s.Require().NoError(s.tk.Decollateralize(s.ctx, addr, sdk.NewInt64Coin(uDenom, 1000))) + s.decollateralize(addr, coin(uDenom, 1000)) // confirm collateral amount is 0 u/uumee - collateral = s.tk.GetCollateralAmount(s.ctx, addr, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 0), collateral) // we do not test empty denom, as that will cause a panic } func (s *IntegrationTestSuite) TestSetCollateralAmount() { + ctx, require := s.ctx, s.Require() 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)) - s.Require().EqualError(err, "empty address") + err := s.tk.SetCollateralAmount(ctx, sdk.AccAddress{}, coin(uDenom, 0)) + require.ErrorIs(err, types.ErrEmptyAddress) - addr := sdk.AccAddress{0x01} + addr := s.newAccount() // force invalid denom - err = s.tk.SetCollateralAmount(s.ctx, addr, sdk.Coin{Denom: "", Amount: sdk.ZeroInt()}) - s.Require().EqualError(err, "0: invalid asset") + err = s.tk.SetCollateralAmount(ctx, addr, sdk.Coin{Denom: "", Amount: sdk.ZeroInt()}) + require.ErrorContains(err, "invalid denom") // set u/umee collateral amount - s.Require().NoError(s.tk.SetCollateralAmount(s.ctx, addr, sdk.NewInt64Coin(uDenom, 10))) + require.NoError(s.tk.SetCollateralAmount(ctx, addr, coin(uDenom, 10))) // confirm effect - collateral := s.tk.GetCollateralAmount(s.ctx, addr, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 10), collateral) + collateral := s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 10), collateral) // set u/umee collateral amount to zero - s.Require().NoError(s.tk.SetCollateralAmount(s.ctx, addr, sdk.NewInt64Coin(uDenom, 0))) + require.NoError(s.tk.SetCollateralAmount(ctx, addr, coin(uDenom, 0))) // confirm effect - collateral = s.tk.GetCollateralAmount(s.ctx, addr, uDenom) - s.Require().Equal(sdk.NewInt64Coin(uDenom, 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, uDenom) + require.Equal(coin(uDenom, 0), collateral) // set unregistered token collateral amount - s.Require().NoError(s.tk.SetCollateralAmount(s.ctx, addr, sdk.NewInt64Coin("abcd", 10))) + require.NoError(s.tk.SetCollateralAmount(ctx, addr, coin("abcd", 10))) // confirm effect - collateral = s.tk.GetCollateralAmount(s.ctx, addr, "abcd") - s.Require().Equal(sdk.NewInt64Coin("abcd", 10), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, "abcd") + require.Equal(coin("abcd", 10), collateral) // set unregistered token collateral amount to zero - s.Require().NoError(s.tk.SetCollateralAmount(s.ctx, addr, sdk.NewInt64Coin("abcd", 0))) + require.NoError(s.tk.SetCollateralAmount(ctx, addr, coin("abcd", 0))) // confirm effect - collateral = s.tk.GetCollateralAmount(s.ctx, addr, "abcd") - s.Require().Equal(sdk.NewInt64Coin("abcd", 0), collateral) + collateral = s.tk.GetCollateralAmount(ctx, addr, "abcd") + require.Equal(coin("abcd", 0), collateral) // we do not test empty denom, as that will cause a panic } + +func (s *IntegrationTestSuite) TestTotalCollateral() { + app, ctx, require := s.app, s.ctx, s.Require() + + // Test zero collateral + uDenom := types.ToUTokenDenom(umeeDenom) + collateral := app.LeverageKeeper.GetTotalCollateral(ctx, uDenom) + require.Equal(sdk.ZeroInt(), collateral, "zero collateral") + + // create a supplier which will have 100 u/UMEE collateral + addr := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(addr, coin(umeeDenom, 100_000000)) + s.collateralize(addr, coin(uDenom, 100_000000)) + + // Test nonzero collateral + collateral = app.LeverageKeeper.GetTotalCollateral(ctx, uDenom) + require.Equal(sdk.NewInt(100_000000), collateral, "nonzero collateral") +} diff --git a/x/leverage/keeper/exchange_rate.go b/x/leverage/keeper/exchange_rate.go index 2bcb08eb0f..6423cd10ae 100644 --- a/x/leverage/keeper/exchange_rate.go +++ b/x/leverage/keeper/exchange_rate.go @@ -10,13 +10,13 @@ import ( // ExchangeToken converts an sdk.Coin containing a base asset to its value as a // uToken. func (k Keeper) ExchangeToken(ctx sdk.Context, token sdk.Coin) (sdk.Coin, error) { - if !token.IsValid() { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, token.String()) + if err := token.Validate(); err != nil { + return sdk.Coin{}, err } uTokenDenom := types.ToUTokenDenom(token.Denom) if uTokenDenom == "" { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, token.Denom) + return sdk.Coin{}, sdkerrors.Wrap(types.ErrUToken, token.Denom) } exchangeRate := k.DeriveExchangeRate(ctx, token.Denom) @@ -28,13 +28,13 @@ func (k Keeper) ExchangeToken(ctx sdk.Context, token sdk.Coin) (sdk.Coin, error) // ExchangeUToken converts an sdk.Coin containing a uToken to its value in a base // token. func (k Keeper) ExchangeUToken(ctx sdk.Context, uToken sdk.Coin) (sdk.Coin, error) { - if !uToken.IsValid() { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, uToken.String()) + if err := uToken.Validate(); err != nil { + return sdk.Coin{}, err } tokenDenom := types.ToTokenDenom(uToken.Denom) if tokenDenom == "" { - return sdk.Coin{}, sdkerrors.Wrap(types.ErrInvalidAsset, uToken.Denom) + return sdk.Coin{}, sdkerrors.Wrap(types.ErrNotUToken, uToken.Denom) } exchangeRate := k.DeriveExchangeRate(ctx, tokenDenom) @@ -46,8 +46,8 @@ func (k Keeper) ExchangeUToken(ctx sdk.Context, uToken sdk.Coin) (sdk.Coin, erro // ExchangeUTokens converts an sdk.Coins containing uTokens to their values in base // tokens. func (k Keeper) ExchangeUTokens(ctx sdk.Context, uTokens sdk.Coins) (sdk.Coins, error) { - if !uTokens.IsValid() { - return sdk.Coins{}, sdkerrors.Wrap(types.ErrInvalidAsset, uTokens.String()) + if err := uTokens.Validate(); err != nil { + return sdk.Coins{}, err } tokens := sdk.Coins{} diff --git a/x/leverage/keeper/exchange_rate_test.go b/x/leverage/keeper/exchange_rate_test.go new file mode 100644 index 0000000000..d215b73351 --- /dev/null +++ b/x/leverage/keeper/exchange_rate_test.go @@ -0,0 +1,32 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + umeeapp "github.com/umee-network/umee/v3/app" +) + +func (s *IntegrationTestSuite) TestDeriveExchangeRate() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + + // artificially increase total borrows (by affecting a single address) + err := s.tk.SetBorrow(ctx, addr, coin(umeeDenom, 2000)) + require.NoError(err) + + // artificially set reserves + s.setReserves(coin(umeeDenom, 300)) + + // expected token:uToken exchange rate + // = (total borrows + module balance - reserves) / utoken supply + // = 2000 + 1000 - 300 / 1000 + // = 2.7 + + // get derived exchange rate + rate := app.LeverageKeeper.DeriveExchangeRate(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("2.7"), rate) +} diff --git a/x/leverage/keeper/grpc_query_test.go b/x/leverage/keeper/grpc_query_test.go index b1fb613664..49c01053e2 100644 --- a/x/leverage/keeper/grpc_query_test.go +++ b/x/leverage/keeper/grpc_query_test.go @@ -9,25 +9,31 @@ import ( ) func (s *IntegrationTestSuite) TestQuerier_RegisteredTokens() { - resp, err := s.queryClient.RegisteredTokens(s.ctx.Context(), &types.QueryRegisteredTokens{}) - s.Require().NoError(err) - s.Require().Len(resp.Registry, 2, "token registry length") + ctx, require := s.ctx, s.Require() + + resp, err := s.queryClient.RegisteredTokens(ctx.Context(), &types.QueryRegisteredTokens{}) + require.NoError(err) + require.Len(resp.Registry, 2, "token registry length") } func (s *IntegrationTestSuite) TestQuerier_Params() { - resp, err := s.queryClient.Params(s.ctx.Context(), &types.QueryParams{}) - s.Require().NoError(err) - s.Require().Equal(types.DefaultParams(), resp.Params) + ctx, require := s.ctx, s.Require() + + resp, err := s.queryClient.Params(ctx.Context(), &types.QueryParams{}) + require.NoError(err) + require.Equal(types.DefaultParams(), resp.Params) } func (s *IntegrationTestSuite) TestQuerier_MarketSummary() { + require := s.Require() + req := &types.QueryMarketSummary{} _, err := s.queryClient.MarketSummary(context.Background(), req) - s.Require().Error(err) + require.ErrorContains(err, "empty denom") req = &types.QueryMarketSummary{Denom: "uumee"} resp, err := s.queryClient.MarketSummary(context.Background(), req) - s.Require().NoError(err) + require.NoError(err) oraclePrice := sdk.MustNewDecFromStr("0.00000421") @@ -51,61 +57,73 @@ func (s *IntegrationTestSuite) TestQuerier_MarketSummary() { AvailableWithdraw: sdk.ZeroInt(), AvailableCollateralize: sdk.ZeroInt(), } - s.Require().Equal(expected, *resp) + require.Equal(expected, *resp) } func (s *IntegrationTestSuite) TestQuerier_AccountBalances() { - addr, _ := s.initBorrowScenario() + ctx, require := s.ctx, s.Require() - resp, err := s.queryClient.AccountBalances(s.ctx.Context(), &types.QueryAccountBalances{Address: addr.String()}) - s.Require().NoError(err) + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + + resp, err := s.queryClient.AccountBalances(ctx.Context(), &types.QueryAccountBalances{Address: addr.String()}) + require.NoError(err) expected := types.QueryAccountBalancesResponse{ Supplied: sdk.NewCoins( - sdk.NewCoin(umeeDenom, sdk.NewInt(1000000000)), + coin(umeeDenom, 1000), ), Collateral: sdk.NewCoins( - sdk.NewCoin(types.ToUTokenDenom(umeeDenom), sdk.NewInt(1000000000)), + coin("u/"+umeeDenom, 1000), ), Borrowed: nil, } - s.Require().Equal(expected, *resp) + require.Equal(expected, *resp) } func (s *IntegrationTestSuite) TestQuerier_AccountSummary() { - addr, _ := s.initBorrowScenario() + ctx, require := s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 UMEE + addr := s.newAccount(coin(umeeDenom, 1000_000000)) + s.supply(addr, coin(umeeDenom, 1000_000000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000_000000)) - resp, err := s.queryClient.AccountSummary(s.ctx.Context(), &types.QueryAccountSummary{Address: addr.String()}) - s.Require().NoError(err) + resp, err := s.queryClient.AccountSummary(ctx.Context(), &types.QueryAccountSummary{Address: addr.String()}) + require.NoError(err) expected := types.QueryAccountSummaryResponse{ // This result is umee's oracle exchange rate from // from .Reset() in x/leverage/keeper/oracle_test.go // times the amount of umee, then sometimes times params // from newToken in x/leverage/keeper/keeper_test.go - // (1000 / 1000000) * 4.21 = 4210 + // (1000) * 4.21 = 4210 SuppliedValue: sdk.MustNewDecFromStr("4210"), - // (1000 / 1000000) * 4.21 = 4210 + // (1000) * 4.21 = 4210 CollateralValue: sdk.MustNewDecFromStr("4210"), // Nothing borrowed BorrowedValue: sdk.ZeroDec(), - // (1000 / 1000000) * 4.21 * 0.25 = 1052.5 + // (1000) * 4.21 * 0.25 = 1052.5 BorrowLimit: sdk.MustNewDecFromStr("1052.5"), - // (1000 / 1000000) * 4.21 * 0.25 = 1052.5 + // (1000) * 4.21 * 0.25 = 1052.5 LiquidationThreshold: sdk.MustNewDecFromStr("1052.5"), } - s.Require().Equal(expected, *resp) + require.Equal(expected, *resp) } func (s *IntegrationTestSuite) TestQuerier_LiquidationTargets() { - resp, err := s.queryClient.LiquidationTargets(s.ctx.Context(), &types.QueryLiquidationTargets{}) - s.Require().NoError(err) + ctx, require := s.ctx, s.Require() + + resp, err := s.queryClient.LiquidationTargets(ctx.Context(), &types.QueryLiquidationTargets{}) + require.NoError(err) expected := types.QueryLiquidationTargetsResponse{ Targets: nil, } - s.Require().Equal(expected, *resp) + require.Equal(expected, *resp) } diff --git a/x/leverage/keeper/interest_test.go b/x/leverage/keeper/interest_test.go new file mode 100644 index 0000000000..a88fc6da9a --- /dev/null +++ b/x/leverage/keeper/interest_test.go @@ -0,0 +1,91 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + umeeapp "github.com/umee-network/umee/v3/app" +) + +func (s *IntegrationTestSuite) TestAccrueZeroInterest() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 UMEE + addr := s.newAccount(coin(umeeDenom, 1000_000000)) + s.supply(addr, coin(umeeDenom, 1000_000000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000_000000)) + + // user borrows 40 umee + s.borrow(addr, coin(umeeapp.BondDenom, 40_000000)) + + // verify user's loan amount (40 umee) + loanBalance := app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) + require.Equal(coin(umeeapp.BondDenom, 40_000000), loanBalance) + + // Because no time has passed since genesis (due to test setup) this will not + // increase borrowed amount. + err := app.LeverageKeeper.AccrueAllInterest(ctx) + require.NoError(err) + + // verify user's loan amount (40 umee) + loanBalance = app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) + require.Equal(coin(umeeapp.BondDenom, 40_000000), loanBalance) + + // borrow APY at utilization = 4% + // when kink utilization = 80%, and base/kink APY are 0.02 and 0.22 + borrowAPY := app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.03"), borrowAPY) + + // supply APY when borrow APY is 3% + // and utilization is 4%, and reservefactor is 20%, and OracleRewardFactor is 1% + // 0.03 * 0.04 * (1 - 0.21) = 0.000948 + supplyAPY := app.LeverageKeeper.DeriveSupplyAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.000948"), supplyAPY) +} + +func (s *IntegrationTestSuite) TestDynamicInterest() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 UMEE + addr := s.newAccount(coin(umeeDenom, 1000_000000)) + s.supply(addr, coin(umeeDenom, 1000_000000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000_000000)) + + // Base interest rate (0% utilization) + rate := app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.02"), rate) + + // user borrows 200 umee, utilization 200/1000 + s.borrow(addr, coin(umeeapp.BondDenom, 200_000000)) + + // Between base interest and kink (20% utilization) + rate = app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.07"), rate) + + // user borrows 600 more umee (ignores collateral), utilization 800/1000 + s.forceBorrow(addr, coin(umeeapp.BondDenom, 600_000000)) + + // Kink interest rate (80% utilization) + rate = app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.22"), rate) + + // user borrows 100 more umee (ignores collateral), utilization 900/1000 + s.forceBorrow(addr, coin(umeeapp.BondDenom, 100_000000)) + + // Between kink interest and max (90% utilization) + rate = app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("0.87"), rate) + + // user borrows 100 more umee (ignores collateral), utilization 1000/1000 + s.forceBorrow(addr, coin(umeeapp.BondDenom, 100_000000)) + + // Max interest rate (100% utilization) + rate = app.LeverageKeeper.DeriveBorrowAPY(ctx, umeeapp.BondDenom) + require.Equal(sdk.MustNewDecFromStr("1.52"), rate) +} + +func (s *IntegrationTestSuite) TestDynamicInterest_InvalidAsset() { + app, ctx, require := s.app, s.ctx, s.Require() + + rate := app.LeverageKeeper.DeriveBorrowAPY(ctx, "uabc") + require.Equal(sdk.ZeroDec(), rate) +} diff --git a/x/leverage/keeper/invariants.go b/x/leverage/keeper/invariants.go index 1b10bf7b0e..af2d60e28e 100644 --- a/x/leverage/keeper/invariants.go +++ b/x/leverage/keeper/invariants.go @@ -9,6 +9,7 @@ import ( const ( routeInterestScalars = "interest-scalars" + routeExchangeRates = "exchange-rates" routeReserveAmount = "reserve-amount" routeCollateralAmount = "collateral-amount" routeBorrowAmount = "borrow-amount" @@ -24,6 +25,7 @@ func RegisterInvariants(ir sdk.InvariantRegistry, k Keeper) { ir.RegisterRoute(types.ModuleName, routeBorrowAPY, BorrowAPYInvariant(k)) ir.RegisterRoute(types.ModuleName, routeSupplyAPY, SupplyAPYInvariant(k)) ir.RegisterRoute(types.ModuleName, routeInterestScalars, InterestScalarsInvariant(k)) + ir.RegisterRoute(types.ModuleName, routeExchangeRates, ExchangeRatesInvariant(k)) } // AllInvariants runs all invariants of the x/leverage module. @@ -54,7 +56,12 @@ func AllInvariants(k Keeper) sdk.Invariant { return res, stop } - return InterestScalarsInvariant(k)(ctx) + res, stop = InterestScalarsInvariant(k)(ctx) + if stop { + return res, stop + } + + return ExchangeRatesInvariant(k)(ctx) } } @@ -297,3 +304,39 @@ func InterestScalarsInvariant(k Keeper) sdk.Invariant { ), broken } } + +// ExchangeRatesInvariant checks that all denoms have an uToken exchange rate >= 1 +func ExchangeRatesInvariant(k Keeper) sdk.Invariant { + return func(ctx sdk.Context) (string, bool) { + var ( + msg string + count int + ) + + tokenPrefix := types.KeyPrefixRegisteredToken + + // Iterate through all denoms of registered tokens in the + // keeper, ensuring none have an interest scalar less than one. + err := k.iterate(ctx, tokenPrefix, func(key, _ []byte) error { + denom := types.DenomFromKey(key, tokenPrefix) + + exchangeRate := k.DeriveExchangeRate(ctx, denom) + + if exchangeRate.LT(sdk.OneDec()) { + count++ + msg += fmt.Sprintf("\t%s exchange rate %s is less than one\n", denom, exchangeRate.String()) + } + return nil + }) + if err != nil { + msg += fmt.Sprintf("\tSome error occurred while iterating through the uToken exchange rates %+v\n", err) + } + + broken := count != 0 + + return sdk.FormatInvariant( + types.ModuleName, routeExchangeRates, + fmt.Sprintf("amount of uToken exchange rates lower than one %d\n%s", count, msg), + ), broken + } +} diff --git a/x/leverage/keeper/invariants_test.go b/x/leverage/keeper/invariants_test.go new file mode 100644 index 0000000000..b398454ab5 --- /dev/null +++ b/x/leverage/keeper/invariants_test.go @@ -0,0 +1,65 @@ +package keeper_test + +import ( + umeeapp "github.com/umee-network/umee/v3/app" + "github.com/umee-network/umee/v3/x/leverage/keeper" + "github.com/umee-network/umee/v3/x/leverage/types" +) + +func (s *IntegrationTestSuite) TestReserveAmountInvariant() { + app, ctx, require := s.app, s.ctx, s.Require() + + // artificially set reserves + s.setReserves(coin(umeeapp.BondDenom, 300_000000)) + + // check invariants + _, broken := keeper.ReserveAmountInvariant(app.LeverageKeeper)(ctx) + require.False(broken) +} + +func (s *IntegrationTestSuite) TestCollateralAmountInvariant() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 UMEE + addr := s.newAccount(coin(umeeDenom, 1000_000000)) + s.supply(addr, coin(umeeDenom, 1000_000000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000_000000)) + + // check invariant + _, broken := keeper.CollateralAmountInvariant(app.LeverageKeeper)(ctx) + require.False(broken) + + uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) + + // withdraw the supplied umee in the initBorrowScenario + s.withdraw(addr, coin(uTokenDenom, 1000_000000)) + + // check invariant + _, broken = keeper.CollateralAmountInvariant(app.LeverageKeeper)(ctx) + require.False(broken) +} + +func (s *IntegrationTestSuite) TestBorrowAmountInvariant() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 UMEE + addr := s.newAccount(coin(umeeDenom, 1000_000000)) + s.supply(addr, coin(umeeDenom, 1000_000000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000_000000)) + + // user borrows 20 umee + s.borrow(addr, coin(umeeapp.BondDenom, 20_000000)) + + // check invariant + _, broken := keeper.BorrowAmountInvariant(app.LeverageKeeper)(ctx) + require.False(broken) + + // user repays 30 umee, actually only 20 because is the min between + // the amount borrowed and the amount repaid + _, err := app.LeverageKeeper.Repay(ctx, addr, coin(umeeapp.BondDenom, 30_000000)) + require.NoError(err) + + // check invariant + _, broken = keeper.BorrowAmountInvariant(app.LeverageKeeper)(ctx) + require.False(broken) +} diff --git a/x/leverage/keeper/iter_test.go b/x/leverage/keeper/iter_test.go new file mode 100644 index 0000000000..7503e6b973 --- /dev/null +++ b/x/leverage/keeper/iter_test.go @@ -0,0 +1,119 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrOneAsset() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + + // user borrows 250 umee (max current allowed) + s.borrow(addr, coin(umeeDenom, 250)) + + zeroAddresses, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{}, zeroAddresses) + + // Note: Setting umee liquidation threshold to 0.05 to make the user eligible to liquidation + umeeToken := newToken("uumee", "UMEE") + umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") + umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") + + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, umeeToken)) + + targetAddress, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{addr}, targetAddress) +} + +func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrTwoAsset() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + + // user borrows 250 umee (max current allowed) + s.borrow(addr, coin(umeeDenom, 250)) + + zeroAddresses, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{}, zeroAddresses) + + // mints and send to addr 100 atom and already + // enable 50 u/atom as collateral. + s.fundAccount(addr, coin(atomDenom, 100_000000)) + s.supply(addr, coin(atomDenom, 50_000000)) + s.collateralize(addr, coin("u/"+atomDenom, 50_000000)) + + // user borrows 4 atom (max current allowed - 1) user amount enabled as collateral * CollateralWeight + // = (50 * 0.1) - 1 + // = 4app. + s.borrow(addr, coin(atomDenom, 4_000000)) + + // Note: Setting umee liquidation threshold to 0.05 to make the user eligible for liquidation + umeeToken := newToken("uumee", "UMEE") + umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") + umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") + + require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) + + // Note: Setting atom collateral weight to 0.01 to make the user eligible for liquidation + atomIBCToken := newToken(atomDenom, "ATOM") + atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") + atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") + + require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, atomIBCToken)) + + targetAddr, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{addr}, targetAddr) +} + +func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_TwoAddr() { + app, ctx, require := s.app, s.ctx, s.Require() + + // creates account which has supplied and collateralized 1000 uumee + addr := s.newAccount(coin(umeeDenom, 1000)) + s.supply(addr, coin(umeeDenom, 1000)) + s.collateralize(addr, coin("u/"+umeeDenom, 1000)) + + // user borrows 250 umee (max current allowed) + s.borrow(addr, coin(umeeDenom, 250)) + + zeroAddresses, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{}, zeroAddresses) + + // creates another account which has supplied and collateralized 100 uatom + addr2 := s.newAccount(coin(atomDenom, 100)) + s.supply(addr2, coin(atomDenom, 100)) + s.collateralize(addr2, coin("u/"+atomDenom, 100)) + + // borrows atom (max current allowed - 1) + s.borrow(addr2, coin(atomDenom, 24)) + + // Note: Setting umee liquidation threshold to 0.05 to make the first supplier eligible for liquidation + umeeToken := newToken("uumee", "UMEE") + umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") + umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") + + require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) + + // Note: Setting atom collateral weight to 0.01 to make the second supplier eligible for liquidation + atomIBCToken := newToken(atomDenom, "ATOM") + atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") + atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") + + require.NoError(app.LeverageKeeper.SetTokenSettings(s.ctx, atomIBCToken)) + + targets, err := app.LeverageKeeper.GetEligibleLiquidationTargets(ctx) + require.NoError(err) + require.Equal([]sdk.AccAddress{addr, addr2}, targets) +} diff --git a/x/leverage/keeper/keeper.go b/x/leverage/keeper/keeper.go index 0ab8047769..c87efa4dd0 100644 --- a/x/leverage/keeper/keeper.go +++ b/x/leverage/keeper/keeper.go @@ -118,12 +118,9 @@ func (k Keeper) Supply(ctx sdk.Context, supplierAddr sdk.AccAddress, coin sdk.Co // balances are insufficient to withdraw the full amount requested, returns an error. // Returns the amount of base tokens received. func (k Keeper) Withdraw(ctx sdk.Context, supplierAddr sdk.AccAddress, uToken sdk.Coin) (sdk.Coin, error) { - if err := uToken.Validate(); err != nil { + if err := k.validateUToken(uToken); err != nil { return sdk.Coin{}, err } - if !types.HasUTokenPrefix(uToken.Denom) { - return sdk.Coin{}, types.ErrNotUToken.Wrap(uToken.Denom) - } // calculate base asset amount to withdraw token, err := k.ExchangeUToken(ctx, uToken) @@ -256,14 +253,14 @@ 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.Coin, error) { - if err := payment.Validate(); err != nil { + if err := k.validateRepay(payment); err != nil { return sdk.Coin{}, err } // determine amount of selected denom currently owed owed := k.GetBorrow(ctx, borrowerAddr, payment.Denom) if owed.IsZero() { - return sdk.Coin{}, types.ErrInvalidRepayment.Wrapf("No %s borrowed ", payment.Denom) + return sdk.Coin{}, types.ErrDenomNotBorrowed.Wrap(payment.Denom) } // prevent overpaying @@ -278,7 +275,7 @@ func (k Keeper) Repay(ctx sdk.Context, borrowerAddr sdk.AccAddress, payment sdk. // Collateralize enables selected uTokens for use as collateral by a single borrower. func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, coin sdk.Coin) error { - if err := k.validateCollateralAsset(ctx, coin); err != nil { + if err := k.validateCollateralize(ctx, coin); err != nil { return err } @@ -292,14 +289,14 @@ func (k Keeper) Collateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, coin // Decollateralize disables selected uTokens for use as collateral by a single borrower. func (k Keeper) Decollateralize(ctx sdk.Context, borrowerAddr sdk.AccAddress, coin sdk.Coin) error { - if err := coin.Validate(); err != nil { + if err := k.validateUToken(coin); err != nil { return err } // Detect where sufficient collateral exists to disable collateral := k.GetBorrowerCollateral(ctx, borrowerAddr) if collateral.AmountOf(coin.Denom).LT(coin.Amount) { - return types.ErrInsufficientBalance + return types.ErrInsufficientCollateral } // Determine what borrow limit would be AFTER disabling this denom as collateral @@ -368,7 +365,7 @@ func (k Keeper) Liquidate( if tokenRepay.IsZero() { // Zero repay amount returned from liquidation computation means the target was eligible for liquidation // but the proposed reward and repayment would have zero effect. - return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationInvalid + return sdk.Coin{}, sdk.Coin{}, sdk.Coin{}, types.ErrLiquidationRepayZero } // repay some of the borrower's debt using the liquidator's balance diff --git a/x/leverage/keeper/keeper_test.go b/x/leverage/keeper/keeper_test.go index d5dfe310c4..683e78a644 100644 --- a/x/leverage/keeper/keeper_test.go +++ b/x/leverage/keeper/keeper_test.go @@ -1,1084 +1,938 @@ package keeper_test import ( - "fmt" - "testing" - "time" - - sdkmath "cosmossdk.io/math" - "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" - minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" - "github.com/stretchr/testify/suite" - tmrand "github.com/tendermint/tendermint/libs/rand" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - - umeeapp "github.com/umee-network/umee/v3/app" - "github.com/umee-network/umee/v3/x/leverage" - "github.com/umee-network/umee/v3/x/leverage/fixtures" - "github.com/umee-network/umee/v3/x/leverage/keeper" - "github.com/umee-network/umee/v3/x/leverage/types" -) + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" -const ( - initialPower = int64(10000000000) - atomIBCDenom = "ibc/CDC4587874B85BEA4FCEC3CEA5A1195139799A1FEE711A07D972537E18FDA39D" - umeeDenom = umeeapp.BondDenom -) - -var ( - initTokens = sdk.TokensFromConsensusPower(initialPower, sdk.DefaultPowerReduction) - initCoins = sdk.NewCoins(sdk.NewCoin(umeeapp.BondDenom, initTokens)) + "github.com/umee-network/umee/v3/x/leverage/types" ) -// creates a test token with reasonable initial parameters -func newToken(base, symbol string) types.Token { - return fixtures.Token(base, symbol) -} - -type IntegrationTestSuite struct { - suite.Suite - - ctx sdk.Context - app *umeeapp.UmeeApp - tk keeper.TestKeeper - queryClient types.QueryClient - setupAccountCounter sdkmath.Int -} - -func TestKeeperTestSuite(t *testing.T) { - suite.Run(t, new(IntegrationTestSuite)) -} - -func (s *IntegrationTestSuite) SetupTest() { - app := umeeapp.Setup(s.T(), false, 1) - ctx := app.BaseApp.NewContext(false, tmproto.Header{ - ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)), - Height: 1, - Time: time.Unix(0, 0), - }) - - umeeToken := newToken(umeeapp.BondDenom, "UMEE") - atomIBCToken := newToken(atomIBCDenom, "ATOM") - - // we only override the Leverage keeper so we can supply a custom mock oracle - k, tk := keeper.NewTestKeeper( - s.Require(), - app.AppCodec(), - app.GetKey(types.ModuleName), - app.GetSubspace(types.ModuleName), - app.BankKeeper, - newMockOracleKeeper(), - ) - - s.tk = tk - app.LeverageKeeper = k - app.LeverageKeeper = *app.LeverageKeeper.SetHooks(types.NewMultiHooks()) - - leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) - s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, umeeToken)) - s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, atomIBCToken)) - - queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry()) - types.RegisterQueryServer(queryHelper, keeper.NewQuerier(app.LeverageKeeper)) - - s.app = app - s.ctx = ctx - s.setupAccountCounter = sdkmath.ZeroInt() - s.queryClient = types.NewQueryClient(queryHelper) -} - -// setupAccount executes some common boilerplate before a test, where a user account is given tokens of a given denom, -// may also supply them to receive uTokens, and may also enable those uTokens as collateral and borrow tokens in the same denom. -func (s *IntegrationTestSuite) setupAccount(denom string, mintAmount, supplyAmount, borrowAmount int64, collateral bool) sdk.AccAddress { - // create a unique address - s.setupAccountCounter = s.setupAccountCounter.Add(sdk.OneInt()) - addrStr := fmt.Sprintf("%-20s", "addr"+s.setupAccountCounter.String()+"_______________") - addr := sdk.AccAddress([]byte(addrStr)) - - // register the account in AccountKeeper - acct := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr) - s.app.AccountKeeper.SetAccount(s.ctx, acct) - - if mintAmount > 0 { - // mint and send mintAmount tokens to account - s.Require().NoError(s.app.BankKeeper.MintCoins(s.ctx, minttypes.ModuleName, - sdk.NewCoins(sdk.NewInt64Coin(denom, mintAmount)), - )) - s.Require().NoError(s.app.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, minttypes.ModuleName, addr, - sdk.NewCoins(sdk.NewInt64Coin(denom, mintAmount)), - )) - } - - if supplyAmount > 0 { - // account supplies supplyAmount tokens and receives uTokens - uTokens, err := s.app.LeverageKeeper.Supply(s.ctx, addr, sdk.NewInt64Coin(denom, supplyAmount)) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(denom), supplyAmount), uTokens) +func (s *IntegrationTestSuite) TestSupply() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + expectedUTokens sdk.Coin + err error } - if collateral { - // account enables associated uToken as collateral - collat, err := s.app.LeverageKeeper.ExchangeToken(s.ctx, sdk.NewInt64Coin(denom, supplyAmount)) - s.Require().NoError(err) - err = s.app.LeverageKeeper.Collateralize(s.ctx, addr, collat) - s.Require().NoError(err) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.5 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 60_000000)) + + tcs := []testCase{ + { + "unregistered denom", + supplier, + coin("abcd", 80_000000), + sdk.Coin{}, + types.ErrNotRegisteredToken, + }, + { + "uToken", + supplier, + coin("u/"+umeeDenom, 80_000000), + sdk.Coin{}, + types.ErrUToken, + }, + { + "no balance", + borrower, + coin(umeeDenom, 20_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "insufficient balance", + supplier, + coin(umeeDenom, 120_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + { + "valid supply", + supplier, + coin(umeeDenom, 80_000000), + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional supply", + supplier, + coin(umeeDenom, 20_000000), + coin("u/"+umeeDenom, 20_000000), + nil, + }, + { + "high exchange rate", + supplier, + coin(atomDenom, 60_000000), + coin("u/"+atomDenom, 40_000000), + nil, + }, } - if borrowAmount > 0 { - // account borrows borrowAmount tokens - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(denom, borrowAmount)) - s.Require().NoError(err) + for _, tc := range tcs { + if tc.err != nil { + _, err := app.LeverageKeeper.Supply(ctx, tc.addr, tc.coin) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := tc.coin.Denom + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of supply function + uToken, err := app.LeverageKeeper.Supply(ctx, tc.addr, tc.coin) + require.NoError(err, tc.msg) + require.Equal(tc.expectedUTokens, uToken, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance decreased and uToken balance increased by the expected amounts + require.Equal(iBalance.Sub(tc.coin).Add(tc.expectedUTokens), fBalance, tc.msg, "token balance") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "uToken collateral") + // verify uToken supply increased by the expected amount + require.Equal(iUTokenSupply.Add(tc.expectedUTokens), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } } - - // return the account addresse - return addr -} - -func (s *IntegrationTestSuite) TestSupply_InvalidAsset() { - addr := sdk.AccAddress([]byte("addr________________")) - acc := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, addr) - s.app.AccountKeeper.SetAccount(s.ctx, acc) - - // create coins of an unregistered base asset type "uabcd" - invalidCoin := sdk.NewInt64Coin("uabcd", 1000000000) // 1k abcd - invalidCoins := sdk.NewCoins(invalidCoin) - - // mint and send coins - s.Require().NoError(s.app.BankKeeper.MintCoins(s.ctx, minttypes.ModuleName, invalidCoins)) - s.Require().NoError(s.app.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, minttypes.ModuleName, addr, invalidCoins)) - - // supplying should fail as we have not registered token "uabcd" - uTokens, err := s.app.LeverageKeeper.Supply(s.ctx, addr, invalidCoin) - s.Require().Error(err) - s.Require().Equal(sdk.Coin{}, uTokens) -} - -func (s *IntegrationTestSuite) TestSupply_Valid() { - app, ctx := s.app, s.ctx - - addr := sdk.AccAddress([]byte("addr____________1234")) - acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr) - app.AccountKeeper.SetAccount(ctx, acc) - - // mint and send coins - s.Require().NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, initCoins)) - s.Require().NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, initCoins)) - - // supply asset - uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) - uToken, err := s.app.LeverageKeeper.Supply(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 1000000000)) // 1k umee - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(uTokenDenom, 1000000000), uToken) // 1k u/umee - - // verify the total supply of the minted uToken - supply := s.app.LeverageKeeper.GetUTokenSupply(ctx, uTokenDenom) - expected := sdk.NewInt64Coin(uTokenDenom, 1000000000) // 1k u/umee - s.Require().Equal(expected, supply) - - // verify the user's balances - tokenBalance := app.BankKeeper.GetBalance(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(initTokens.Sub(sdk.NewInt(1000000000)), tokenBalance.Amount) - - uTokenBalance := app.BankKeeper.GetBalance(ctx, addr, uTokenDenom) - s.Require().Equal(int64(1000000000), uTokenBalance.Amount.Int64()) -} - -func (s *IntegrationTestSuite) TestWithdraw_Valid() { - app, ctx := s.app, s.ctx - - addr := sdk.AccAddress([]byte("addr________________")) - acc := app.AccountKeeper.NewAccountWithAddress(ctx, addr) - app.AccountKeeper.SetAccount(ctx, acc) - - // mint and send coins - s.Require().NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, initCoins)) - s.Require().NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, initCoins)) - - // supply asset - _, err := s.app.LeverageKeeper.Supply(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 1000000000)) // 1k umee - s.Require().NoError(err) - - // verify the total supply of the minted uToken - 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) - - // withdraw the total amount of assets supplied - uToken := expected - withdrawn, err := s.app.LeverageKeeper.Withdraw(ctx, addr, uToken) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 1000000000), withdrawn) // 1k umee - - // verify total supply of the uTokens - supply = s.app.LeverageKeeper.GetUTokenSupply(ctx, uTokenDenom) - s.Require().Equal(int64(0), supply.Amount.Int64()) - - // verify the user's balances - tokenBalance := app.BankKeeper.GetBalance(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(initTokens, tokenBalance.Amount) - - uTokenBalance := app.BankKeeper.GetBalance(ctx, addr, uTokenDenom) - s.Require().Equal(int64(0), uTokenBalance.Amount.Int64()) -} - -func (s *IntegrationTestSuite) TestSetReserves() { - // get initial reserves - amount := s.app.LeverageKeeper.GetReserveAmount(s.ctx, umeeapp.BondDenom) - s.Require().Equal(amount, sdk.ZeroInt()) - - // artifically reserve 200 umee - err := s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 200000000)) - s.Require().NoError(err) - - // get new reserves - amount = s.app.LeverageKeeper.GetReserveAmount(s.ctx, umeeapp.BondDenom) - s.Require().Equal(amount, sdk.NewInt(200000000)) -} - -func (s *IntegrationTestSuite) TestGetToken() { - uabc := newToken("uabc", "ABC") - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, uabc)) - - t, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, "uabc") - s.Require().NoError(err) - s.Require().Equal(t.ReserveFactor, sdk.MustNewDecFromStr("0.2")) - s.Require().Equal(t.CollateralWeight, sdk.MustNewDecFromStr("0.25")) - s.Require().Equal(t.LiquidationThreshold, sdk.MustNewDecFromStr("0.25")) - s.Require().Equal(t.BaseBorrowRate, sdk.MustNewDecFromStr("0.02")) - s.Require().Equal(t.KinkBorrowRate, sdk.MustNewDecFromStr("0.22")) - s.Require().Equal(t.MaxBorrowRate, sdk.MustNewDecFromStr("1.52")) - s.Require().Equal(t.KinkUtilization, sdk.MustNewDecFromStr("0.8")) - s.Require().Equal(t.LiquidationIncentive, sdk.MustNewDecFromStr("0.1")) - - s.Require().NoError(t.AssertBorrowEnabled()) - s.Require().NoError(t.AssertSupplyEnabled()) - s.Require().NoError(t.AssertNotBlacklisted()) -} - -// initialize the common starting scenario from which borrow and repay tests stem: -// Umee and u/umee are registered assets; a "supplier" account has 9k umee and 1k u/umee; -// the leverage module has 1k umee in its lending pool (module account); and a "bum" -// account has been created with no assets. -func (s *IntegrationTestSuite) initBorrowScenario() (supplier, bum sdk.AccAddress) { - app, ctx := s.app, s.ctx - - // create an account and address which will represent a supplier - supplierAddr := sdk.AccAddress([]byte("addr______________01")) - supplierAcc := app.AccountKeeper.NewAccountWithAddress(ctx, supplierAddr) - app.AccountKeeper.SetAccount(ctx, supplierAcc) - - // create an account and address which will represent a user with no assets - bumAddr := sdk.AccAddress([]byte("addr______________02")) - bumAcc := app.AccountKeeper.NewAccountWithAddress(ctx, bumAddr) - app.AccountKeeper.SetAccount(ctx, bumAcc) - - // mint and send 10k umee to supplier - s.Require().NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee - )) - s.Require().NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, supplierAddr, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee, - )) - - // supplier supplies 1000 umee and receives 1k u/umee - supplyCoin := sdk.NewInt64Coin(umeeapp.BondDenom, 1000000000) - _, err := s.app.LeverageKeeper.Supply(ctx, supplierAddr, supplyCoin) - s.Require().NoError(err) - - // supplier enables u/umee as collateral - collat, err := s.app.LeverageKeeper.ExchangeToken(ctx, supplyCoin) - s.Require().NoError(err) - err = s.app.LeverageKeeper.Collateralize(ctx, supplierAddr, collat) - s.Require().NoError(err) - - // return the account addresses - return supplierAddr, bumAddr -} - -// mintAndSupplyAtom mints a amount of atoms to an address -// account has been created with no assets. -func (s *IntegrationTestSuite) mintAndSupplyAtom(mintTo sdk.AccAddress, amountToMint, amountToSupply int64) { - app, ctx := s.app, s.ctx - - // mint and send atom to mint addr - s.Require().NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, - sdk.NewCoins(sdk.NewInt64Coin(atomIBCDenom, amountToMint)), // amountToMint Atom - )) - s.Require().NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, mintTo, - sdk.NewCoins(sdk.NewInt64Coin(atomIBCDenom, amountToMint)), // amountToMint Atom, - )) - - // user supplies amountToSupply atom and receives amountToSupply u/atom - supplyCoin := sdk.NewInt64Coin(atomIBCDenom, amountToSupply) - _, err := s.app.LeverageKeeper.Supply(ctx, mintTo, supplyCoin) - s.Require().NoError(err) - - // user enables u/atom as collateral - collat, err := s.app.LeverageKeeper.ExchangeToken(ctx, supplyCoin) - s.Require().NoError(err) - err = s.app.LeverageKeeper.Collateralize(ctx, mintTo, collat) - s.Require().NoError(err) } -func (s *IntegrationTestSuite) TestBorrow_Invalid() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral - - // user attempts to borrow 200 u/umee, fails because uTokens cannot be borrowed - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 200000000)) - s.Require().Error(err) - - // user attempts to borrow 200 abcd, fails because "abcd" is not a valid denom - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin("uabcd", 200000000)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestBorrow_InsufficientCollateral() { - _, bumAddr := s.initBorrowScenario() // create initial conditions - - // The "bum" user from the init scenario is being used because it - // possesses no assets or collateral. - - // bum attempts to borrow 200 umee, fails because of insufficient collateral - err := s.app.LeverageKeeper.Borrow(s.ctx, bumAddr, sdk.NewInt64Coin(umeeapp.BondDenom, 200000000)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestBorrow_InsufficientLendingPool() { - // Any user from the init scenario can perform this test, because it errors on module balance - addr, _ := s.initBorrowScenario() - - // user attempts to borrow 20000 umee, fails because of insufficient module account balance - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000000)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestRepay_Invalid() { - // Any user from the init scenario can be used for this test. - addr, _ := s.initBorrowScenario() - - // user attempts to repay 200 abcd, fails because "abcd" is not an accepted asset type - _, err := s.app.LeverageKeeper.Repay(s.ctx, addr, sdk.NewInt64Coin("uabcd", 200000000)) - s.Require().Error(err) - - // user attempts to repay 200 u/umee, fails because utokens are not loanable assets - _, err = s.app.LeverageKeeper.Repay(s.ctx, addr, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 200000000)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestBorrow_Valid() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral - - // user borrows 20 umee - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000)) - s.Require().NoError(err) - - // verify user's new loan amount in the correct denom (20 umee) - loanBalance := s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000)) - - // verify user's total loan balance (sdk.Coins) is also 20 umee (no other coins present) - totalLoanBalance := s.app.LeverageKeeper.GetBorrowerBorrows(s.ctx, addr) - s.Require().Equal(totalLoanBalance, sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 20000000))) - - // verify user's new umee balance (10 - 1k from initial + 20 from loan = 9020 umee) - tokenBalance := s.app.BankKeeper.GetBalance(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9020000000)) - - // verify user's uToken balance remains at 0 u/umee from initial conditions - uTokenBalance := s.app.BankKeeper.GetBalance(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 0)) - - // verify user's uToken collateral remains at 1000 u/umee from initial conditions - collateralBalance := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(collateralBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 1000000000)) -} - -func (s *IntegrationTestSuite) TestBorrow_BorrowLimit() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral - - // 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 := 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() - - // user borrows umee up to 90% of their borrow limit - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewCoin(umeeapp.BondDenom, amountToBorrow)) - s.Require().NoError(err) - - // user tries to borrow the same amount again, fails due to borrow limit - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewCoin(umeeapp.BondDenom, amountToBorrow)) - s.Require().Error(err) - - // user tries to disable u/umee as collateral, fails due to borrow limit - err = s.app.LeverageKeeper.Decollateralize(s.ctx, addr, sdk.NewCoin(uDenom, collateral.Amount)) - s.Require().Error(err) - - // user tries to withdraw all its u/umee, fails due to borrow limit - _, err = s.app.LeverageKeeper.Withdraw(s.ctx, addr, sdk.NewCoin(uDenom, collateral.Amount)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestBorrow_Reserved() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - addr, _ := s.initBorrowScenario() - - // artifically reserve 200 umee - err := s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 200000000)) - s.Require().NoError(err) - - // Note: Setting umee collateral weight to 1.0 to allow user to borrow heavily - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("1.0") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("1.0") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // Supplier tries to borrow 1000 umee, insufficient balance because 200 of the - // module's 1000 umee are reserved. - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 1000000000)) - s.Require().Error(err) - - // user borrows 800 umee - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 800000000)) - s.Require().NoError(err) -} - -func (s *IntegrationTestSuite) TestRepay_Valid() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - addr, _ := s.initBorrowScenario() - app, ctx := s.app, s.ctx - - // user borrows 20 umee - err := s.app.LeverageKeeper.Borrow(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000)) - s.Require().NoError(err) - - // user repays 8 umee - repaid, err := s.app.LeverageKeeper.Repay(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 8000000)) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 8000000), repaid) - - // verify user's new loan amount (12 umee) - loanBalance := s.app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 12000000)) - - // verify user's new umee balance (10 - 1k from initial + 20 from loan - 8 repaid = 9012 umee) - tokenBalance := app.BankKeeper.GetBalance(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9012000000)) - - // verify user's uToken balance remains at 0 u/umee from initial conditions - uTokenBalance := s.app.BankKeeper.GetBalance(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 0)) - - // verify user's uToken collateral remains at 1000 u/umee from initial conditions - collateralBalance := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(collateralBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 1000000000)) - - // user repays 12 umee (loan repaid in full) - repaid, err = s.app.LeverageKeeper.Repay(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 12000000)) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 12000000), repaid) - - // verify user's new loan amount in the correct denom (zero) - loanBalance = s.app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 0)) - - // verify user's new umee balance (10 - 1k from initial + 20 from loan - 20 repaid = 9000 umee) - tokenBalance = app.BankKeeper.GetBalance(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9000000000)) - - // verify user's uToken balance remains at 0 u/umee from initial conditions - uTokenBalance = s.app.BankKeeper.GetBalance(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 0)) - - // verify user's uToken collateral remains at 1000 u/umee from initial conditions - collateralBalance = s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(collateralBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 1000000000)) -} - -func (s *IntegrationTestSuite) TestRepay_Overpay() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - addr, _ := s.initBorrowScenario() - app, ctx := s.app, s.ctx - - // user borrows 20 umee - err := s.app.LeverageKeeper.Borrow(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000)) - s.Require().NoError(err) - - // user repays 30 umee - should automatically reduce to 20 (the loan amount) and succeed - coinToRepay := sdk.NewInt64Coin(umeeapp.BondDenom, 30000000) - repaid, err := s.app.LeverageKeeper.Repay(ctx, addr, coinToRepay) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 20000000), repaid) - - // verify that coinToRepay has not been modified - s.Require().Equal(sdk.NewInt(30000000), coinToRepay.Amount) - - // verify user's new loan amount is 0 umee - loanBalance := s.app.LeverageKeeper.GetBorrow(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 0)) - - // verify user's new umee balance (10 - 1k from initial + 20 from loan - 20 repaid = 9000 umee) - tokenBalance := app.BankKeeper.GetBalance(ctx, addr, umeeapp.BondDenom) - s.Require().Equal(tokenBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 9000000000)) - - // verify user's uToken balance remains at 0 u/umee from initial conditions - uTokenBalance := s.app.BankKeeper.GetBalance(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(uTokenBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 0)) - - // verify user's uToken collateral remains at 1000 u/umee from initial conditions - collateralBalance := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, "u/"+umeeapp.BondDenom) - s.Require().Equal(collateralBalance, sdk.NewInt64Coin("u/"+umeeapp.BondDenom, 1000000000)) - - // user repays 50 umee - this time it fails because the loan no longer exists - _, err = s.app.LeverageKeeper.Repay(ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 50000000)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestRepayBadDebt() { - // Creating a supplier so module account has some uumee - _ = s.setupAccount(umeeDenom, 200000000, 200000000, 0, false) // 200 umee - - // Using an address with no assets - addr := s.setupAccount(umeeDenom, 0, 0, 0, false) - - // Create an uncollateralized debt position - badDebt := sdk.NewInt64Coin(umeeDenom, 100000000) // 100 umee - err := s.tk.SetBorrow(s.ctx, addr, badDebt) - s.Require().NoError(err) - - // Manually mark the bad debt for repayment - s.Require().NoError(s.tk.SetBadDebtAddress(s.ctx, addr, umeeDenom, true)) - - // Manually set reserves to 60 umee - reserve := sdk.NewInt64Coin(umeeDenom, 60000000) - err = s.tk.SetReserveAmount(s.ctx, reserve) - s.Require().NoError(err) - - // Sweep all bad debts, which should repay 60 umee of the bad debt (partial repayment) - err = s.app.LeverageKeeper.SweepBadDebts(s.ctx) - s.Require().NoError(err) - - // Confirm that a debt of 40 umee remains - remainingDebt := s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 40000000), remainingDebt) - - // Confirm that reserves are exhausted - remainingReserve := s.app.LeverageKeeper.GetReserveAmount(s.ctx, umeeDenom) - s.Require().Equal(sdk.ZeroInt(), remainingReserve) - - // Manually set reserves to 70 umee - reserve = sdk.NewInt64Coin(umeeDenom, 70000000) - err = s.tk.SetReserveAmount(s.ctx, reserve) - s.Require().NoError(err) - - // Sweep all bad debts, which should fully repay the bad debt this time - err = s.app.LeverageKeeper.SweepBadDebts(s.ctx) - s.Require().NoError(err) - - // Confirm that the debt is eliminated - remainingDebt = s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 0), remainingDebt) - - // Confirm that reserves are now at 30 umee - remainingReserve = s.app.LeverageKeeper.GetReserveAmount(s.ctx, umeeDenom) - s.Require().Equal(sdk.NewInt(30000000), remainingReserve) - - // Sweep all bad debts - but there are none - err = s.app.LeverageKeeper.SweepBadDebts(s.ctx) - s.Require().NoError(err) -} - -func (s *IntegrationTestSuite) TestDeriveExchangeRate() { - // The init scenario is being used so module balance starts at 1000 umee - // and the uToken supply starts at 1000 due to supplier account - _, addr := s.initBorrowScenario() - - // artificially increase total borrows (by affecting a single address) - err := s.tk.SetBorrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 2000000000)) // 2000 umee - s.Require().NoError(err) - - // artificially set reserves - err = s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 300000000)) // 300 umee - s.Require().NoError(err) - - // expected token:uToken exchange rate - // = (total borrows + module balance - reserves) / utoken supply - // = 2000 + 1000 - 300 / 1000 - // = 2.7 - - // get derived exchange rate - rate := s.app.LeverageKeeper.DeriveExchangeRate(s.ctx, umeeapp.BondDenom) - s.Require().Equal(sdk.MustNewDecFromStr("2.7"), rate) -} - -func (s *IntegrationTestSuite) TestAccrueZeroInterest() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - addr, _ := s.initBorrowScenario() - - // user borrows 40 umee - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 40000000)) - s.Require().NoError(err) - - // verify user's loan amount (40 umee) - loanBalance := s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 40000000)) - - // Because no time has passed since genesis (due to test setup) this will not - // increase borrowed amount. - err = s.app.LeverageKeeper.AccrueAllInterest(s.ctx) - s.Require().NoError(err) - - // verify user's loan amount (40 umee) - loanBalance = s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 40000000)) - - // borrow APY at utilization = 4% - // when kink utilization = 80%, and base/kink APY are 0.02 and 0.22 - borrowAPY := s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().Equal(sdk.MustNewDecFromStr("0.03"), borrowAPY) - - // supply APY when borrow APY is 3% - // and utilization is 4%, and reservefactor is 20%, and OracleRewardFactor is 1% - // 0.03 * 0.04 * (1 - 0.21) = 0.000948 - supplyAPY := s.app.LeverageKeeper.DeriveSupplyAPY(s.ctx, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("0.000948"), supplyAPY) -} - -func (s *IntegrationTestSuite) TestAccrueInterest_Invalid() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - addr, _ := s.initBorrowScenario() - - // user borrows 40 umee - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 40000000)) - s.Require().NoError(err) - - // Setting last interest time to a negative value is not allowed - err = s.tk.SetLastInterestTime(s.ctx, -1) - s.Require().ErrorIs(err, types.ErrNegativeTimeElapsed) - - // Setting last interest time ahead greatly succeeds - err = s.tk.SetLastInterestTime(s.ctx, 100_000_000) // 3 years - s.Require().NoError(err) - - // Interest will not accrue (suite BlockTime = 0) but will not error either - err = s.app.LeverageKeeper.AccrueAllInterest(s.ctx) - s.Require().NoError(err) - - // Setting last interest time back to an earlier time is not allowed - err = s.tk.SetLastInterestTime(s.ctx, 1000) - s.Require().ErrorIs(err, types.ErrNegativeTimeElapsed) - - // verify user's loan amount is unchanged (40 umee) - loanBalance := s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(loanBalance, sdk.NewInt64Coin(umeeapp.BondDenom, 40000000)) -} - -func (s *IntegrationTestSuite) TestDynamicInterest() { - // Init scenario is being used because the module account (lending pool) - // already has 1000 umee. - addr, _ := s.initBorrowScenario() - - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("1.0") // to allow high utilization - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("1.0") // to allow high utilization - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // Base interest rate (0% utilization) - rate := s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().Equal(rate, sdk.MustNewDecFromStr("0.02")) - - // user borrows 200 umee, utilization 200/1000 - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 200000000)) - s.Require().NoError(err) - - // Between base interest and kink (20% utilization) - rate = s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().Equal(rate, sdk.MustNewDecFromStr("0.07")) - - // user borrows 600 more umee, utilization 800/1000 - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 600000000)) - s.Require().NoError(err) - - // Kink interest rate (80% utilization) - rate = s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(rate, sdk.MustNewDecFromStr("0.22")) - - // user borrows 100 more umee, utilization 900/1000 - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 100000000)) - s.Require().NoError(err) - - // Between kink interest and max (90% utilization) - rate = s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(rate, sdk.MustNewDecFromStr("0.87")) - - // user borrows 100 more umee, utilization 1000/1000 - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 100000000)) - s.Require().NoError(err) - - // Max interest rate (100% utilization) - rate = s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(rate, sdk.MustNewDecFromStr("1.52")) -} - -func (s *IntegrationTestSuite) TestDynamicInterest_InvalidAsset() { - rate := s.app.LeverageKeeper.DeriveBorrowAPY(s.ctx, "uabc") - s.Require().Equal(rate, sdk.ZeroDec()) -} - -func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrOneAsset() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee enabled as collateral. - addr, _ := s.initBorrowScenario() - - // user borrows 100 umee (max current allowed) user amount enabled as collateral * CollateralWeight - // = 1000 * 0.1 - // = 100 - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 100000000)) - s.Require().NoError(err) - - zeroAddresses, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{}, zeroAddresses) - - // Note: Setting umee liquidation threshold to 0.05 to make the user eligible to liquidation - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - targetAddress, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{addr}, targetAddress) - - // if it tries to borrow any other asset it should return an error - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(atomIBCDenom, 1)) - s.Require().Error(err) -} - -func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_OneAddrTwoAsset() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee enabled as collateral. - addr, _ := s.initBorrowScenario() - - // user borrows 100 umee (max current allowed) user amount enabled as collateral * CollateralWeight - // = 1000 * 0.1 - // = 100 - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 100000000)) - s.Require().NoError(err) - - zeroAddresses, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{}, zeroAddresses) - - mintAmountAtom := int64(100000000) // 100 atom - supplyAmountAtom := int64(50000000) // 50 atom - - // mints and send to user 100 atom and already - // enable 50 u/atom as collateral. - s.mintAndSupplyAtom(addr, mintAmountAtom, supplyAmountAtom) - - // user borrows 4 atom (max current allowed - 1) user amount enabled as collateral * CollateralWeight - // = (50 * 0.1) - 1 - // = 4 - err = s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(atomIBCDenom, 4000000)) // 4 atom - s.Require().NoError(err) - - // Note: Setting umee liquidation threshold to 0.05 to make the user eligible for liquidation - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // Note: Setting atom collateral weight to 0.01 to make the user eligible for liquidation - atomIBCToken := newToken(atomIBCDenom, "ATOM") - atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") - atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") +func (s *IntegrationTestSuite) TestWithdraw() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + expectFromBalance sdk.Coins + expectFromCollateral sdk.Coins + expectedTokens sdk.Coin + err error + } - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, atomIBCToken)) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a supplier with 100 UMEE and 100 ATOM, then supply 100 UMEE and 50 ATOM + // also collateralize 75 of supplied UMEE + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 75_000000)) + s.supply(supplier, coin(atomDenom, 50_000000)) + + // create and modify a borrower to force the uToken exchange rate of ATOM from 1 to 1.2 + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + s.tk.SetBorrow(ctx, borrower, coin(atomDenom, 40_000000)) + + // create an additional UMEE supplier + other := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(other, coin(umeeDenom, 100_000000)) + + tcs := []testCase{ + { + "unregistered base token", + supplier, + coin("abcd", 80_000000), + nil, + nil, + sdk.Coin{}, + types.ErrNotUToken, + }, + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + nil, + nil, + sdk.Coin{}, + types.ErrNotUToken, + }, + { + "insufficient uTokens", + supplier, + coin("u/"+umeeDenom, 120_000000), + nil, + nil, + sdk.Coin{}, + types.ErrInsufficientBalance, + }, + { + "withdraw from balance", + supplier, + coin("u/"+umeeDenom, 10_000000), + sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), + nil, + coin(umeeDenom, 10_000000), + nil, + }, + { + "some from collateral", + supplier, + coin("u/"+umeeDenom, 80_000000), + sdk.NewCoins(coin("u/"+umeeDenom, 15_000000)), + sdk.NewCoins(coin("u/"+umeeDenom, 65_000000)), + coin(umeeDenom, 80_000000), + nil, + }, + { + "only from collateral", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + sdk.NewCoins(coin("u/"+umeeDenom, 10_000000)), + coin(umeeDenom, 10_000000), + nil, + }, + { + "high exchange rate", + supplier, + coin("u/"+atomDenom, 50_000000), + sdk.NewCoins(coin("u/"+atomDenom, 50_000000)), + nil, + coin(atomDenom, 60_000000), + nil, + }, + { + "borrow limit", + borrower, + coin("u/"+atomDenom, 50_000000), + nil, + nil, + sdk.Coin{}, + types.ErrUndercollaterized, + }, + } - targetAddr, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{addr}, targetAddr) + for _, tc := range tcs { + if tc.err != nil { + _, err := app.LeverageKeeper.Withdraw(ctx, tc.addr, tc.uToken) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the outputs of withdraw function + token, err := app.LeverageKeeper.Withdraw(ctx, tc.addr, tc.uToken) + + require.NoError(err, tc.msg) + require.Equal(tc.expectedTokens, token, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance increased by the expected amount + require.Equal(iBalance.Add(tc.expectedTokens).Sub(tc.expectFromBalance...), + fBalance, tc.msg, "token balance") + // verify uToken collateral decreased by the expected amount + s.requireEqualCoins(iCollateral.Sub(tc.expectFromCollateral...), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply decreased by the expected amount + require.Equal(iUTokenSupply.Sub(tc.uToken), fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } -func (s *IntegrationTestSuite) TestGetEligibleLiquidationTargets_TwoAddr() { - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee enabled as collateral. - supplierAddr, anotherSupplier := s.initBorrowScenario() - - // supplier borrows 100 umee (max current allowed) supplier amount enabled as collateral * CollateralWeight - // = 1000 * 0.1 - // = 100 - err := s.app.LeverageKeeper.Borrow(s.ctx, supplierAddr, sdk.NewInt64Coin(umeeapp.BondDenom, 100000000)) - s.Require().NoError(err) - - zeroAddresses, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{}, zeroAddresses) - - mintAmountAtom := int64(100000000) // 100 atom - supplyAmountAtom := int64(50000000) // 50 atom - - // mints and send to anotherSupplier 100 atom and already - // enable 50 u/atom as collateral. - s.mintAndSupplyAtom(anotherSupplier, mintAmountAtom, supplyAmountAtom) - - // anotherSupplier borrows 4 atom (max current allowed - 1) anotherSupplier amount enabled as collateral * CollateralWeight - // = (50 * 0.1) - 1 - // = 4 - err = s.app.LeverageKeeper.Borrow(s.ctx, anotherSupplier, sdk.NewInt64Coin(atomIBCDenom, 4000000)) // 4 atom - s.Require().NoError(err) - - // Note: Setting umee liquidation threshold to 0.05 to make the supplier eligible for liquidation - umeeToken := newToken("uumee", "UMEE") - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.05") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.05") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // Note: Setting atom collateral weight to 0.01 to make the supplier eligible for liquidation - atomIBCToken := newToken(atomIBCDenom, "ATOM") - atomIBCToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") - atomIBCToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, atomIBCToken)) - - supplierAddress, err := s.app.LeverageKeeper.GetEligibleLiquidationTargets(s.ctx) - s.Require().NoError(err) - s.Require().Equal([]sdk.AccAddress{supplierAddr, anotherSupplier}, supplierAddress) -} +func (s *IntegrationTestSuite) TestCollateralize() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + err error + } -func (s *IntegrationTestSuite) TestReserveAmountInvariant() { - // artificially set reserves - err := s.tk.SetReserveAmount(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 300000000)) // 300 umee - s.Require().NoError(err) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a supplier with 200 UMEE, then supply 100 UMEE + supplier := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + + tcs := []testCase{ + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + types.ErrNotUToken, + }, + { + "unregistered uToken", + supplier, + coin("u/abcd", 80_000000), + types.ErrNotRegisteredToken, + }, + { + "wrong balance", + supplier, + coin("u/"+atomDenom, 10_000000), + sdkerrors.ErrInsufficientFunds, + }, + { + "valid collateralize", + supplier, + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional collateralize", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + }, + { + "insufficient balance", + supplier, + coin("u/"+umeeDenom, 40_000000), + sdkerrors.ErrInsufficientFunds, + }, + } - // check invariant - _, broken := keeper.ReserveAmountInvariant(s.app.LeverageKeeper)(s.ctx) - s.Require().False(broken) + for _, tc := range tcs { + if tc.err != nil { + err := app.LeverageKeeper.Collateralize(ctx, tc.addr, tc.uToken) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of collateralize function + err := app.LeverageKeeper.Collateralize(ctx, tc.addr, tc.uToken) + require.NoError(err, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify uToken balance decreased by the expected amount + require.Equal(iBalance.Sub(tc.uToken), fBalance, tc.msg, "uToken balance") + // verify uToken collateral increased by the expected amount + require.Equal(iCollateral.Add(tc.uToken), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } -func (s *IntegrationTestSuite) TestCollateralAmountInvariant() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral - - // check invariant - _, broken := keeper.CollateralAmountInvariant(s.app.LeverageKeeper)(s.ctx) - s.Require().False(broken) - - uTokenDenom := types.ToUTokenDenom(umeeapp.BondDenom) +func (s *IntegrationTestSuite) TestDecollateralize() { + type testCase struct { + msg string + addr sdk.AccAddress + uToken sdk.Coin + err error + } - // withdraw the supplyed umee in the initBorrowScenario - _, err := s.app.LeverageKeeper.Withdraw(s.ctx, addr, sdk.NewInt64Coin(uTokenDenom, 1000000000)) - s.Require().NoError(err) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a supplier with 200 UMEE, then supply and collateralize 100 UMEE + supplier := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000)) + s.collateralize(supplier, coin("u/"+umeeDenom, 100_000000)) + + // create a borrower which supplies, collateralizes, then borrows ATOM + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + s.borrow(borrower, coin(atomDenom, 10_000000)) + + tcs := []testCase{ + { + "base token", + supplier, + coin(umeeDenom, 80_000000), + types.ErrNotUToken, + }, + { + "no collateral", + supplier, + coin("u/"+atomDenom, 40_000000), + types.ErrInsufficientCollateral, + }, + { + "valid decollateralize", + supplier, + coin("u/"+umeeDenom, 80_000000), + nil, + }, + { + "additional decollateralize", + supplier, + coin("u/"+umeeDenom, 10_000000), + nil, + }, + { + "insufficient collateral", + supplier, + coin("u/"+umeeDenom, 40_000000), + types.ErrInsufficientCollateral, + }, + { + "borrow limit", + borrower, + coin("u/"+atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + } - // check invariant - _, broken = keeper.CollateralAmountInvariant(s.app.LeverageKeeper)(s.ctx) - s.Require().False(broken) + for _, tc := range tcs { + if tc.err != nil { + err := app.LeverageKeeper.Decollateralize(ctx, tc.addr, tc.uToken) + require.ErrorIs(err, tc.err, tc.msg) + } else { + denom := types.ToTokenDenom(tc.uToken.Denom) + + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of decollateralize function + err := app.LeverageKeeper.Decollateralize(ctx, tc.addr, tc.uToken) + require.NoError(err, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify uToken balance increased by the expected amount + require.Equal(iBalance.Add(tc.uToken), fBalance, tc.msg, "uToken balance") + // verify uToken collateral decreased by the expected amount + require.Equal(iCollateral.Sub(tc.uToken), fCollateral, tc.msg, "uToken collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins are unchanged + require.Equal(iBorrowed, fBorrowed, tc.msg, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } -func (s *IntegrationTestSuite) TestBorrowAmountInvariant() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral - - // user borrows 20 umee - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 20000000)) - s.Require().NoError(err) - - // check invariant - _, broken := keeper.BorrowAmountInvariant(s.app.LeverageKeeper)(s.ctx) - s.Require().False(broken) +func (s *IntegrationTestSuite) TestBorrow() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + err error + } - // user repays 30 umee, actually only 20 because is the min between - // the amount borrowed and the amount repaid - _, err = s.app.LeverageKeeper.Repay(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 30000000)) - s.Require().NoError(err) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a supplier which supplies UMEE and ATOM + supplier := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(supplier, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + + // create a borrower which supplies and collateralizes 100 ATOM + borrower := s.newAccount(coin(atomDenom, 100_000000)) + s.supply(borrower, coin(atomDenom, 100_000000)) + s.collateralize(borrower, coin("u/"+atomDenom, 100_000000)) + + tcs := []testCase{ + { + "uToken", + borrower, + coin("u/"+umeeDenom, 100_000000), + types.ErrUToken, + }, + { + "unregistered token", + borrower, + coin("abcd", 100_000000), + types.ErrNotRegisteredToken, + }, + { + "lending pool insufficient", + borrower, + coin(umeeDenom, 200_000000), + types.ErrLendingPoolInsufficient, + }, + { + "valid borrow", + borrower, + coin(umeeDenom, 70_000000), + nil, + }, + { + "additional borrow", + borrower, + coin(umeeDenom, 30_000000), + nil, + }, + { + "atom borrow", + borrower, + coin(atomDenom, 1_000000), + nil, + }, + { + "borrow limit", + borrower, + coin(atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + { + "zero collateral", + supplier, + coin(atomDenom, 100_000000), + types.ErrUndercollaterized, + }, + } - // check invariant - _, broken = keeper.BorrowAmountInvariant(s.app.LeverageKeeper)(s.ctx) - s.Require().False(broken) + for _, tc := range tcs { + if tc.err != nil { + err := app.LeverageKeeper.Borrow(ctx, tc.addr, tc.coin) + require.ErrorIs(err, tc.err, tc.msg) + } else { + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of borrow function + err := app.LeverageKeeper.Borrow(ctx, tc.addr, tc.coin) + require.NoError(err, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance is increased by expected amount + require.Equal(iBalance.Add(tc.coin), fBalance, tc.msg, "balances") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins increased by expected amount + require.Equal(iBorrowed.Add(tc.coin), fBorrowed, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } -func (s *IntegrationTestSuite) TestWithdraw_InsufficientCollateral() { - // Create a supplier with 1 u/umee collateral by supplying 1 umee - supplierAddr := s.setupAccount(umeeapp.BondDenom, 1000000, 1000000, 0, true) - - // Create an additional supplier so lending pool has extra umee - _ = s.setupAccount(umeeapp.BondDenom, 1000000, 1000000, 0, true) - - // verify collateral amount and total supply of minted uTokens - 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) - s.Require().Equal(sdk.NewInt64Coin(uTokenDenom, 2000000), supply) // 2 u/umee +func (s *IntegrationTestSuite) TestRepay() { + type testCase struct { + msg string + addr sdk.AccAddress + coin sdk.Coin + expectedRepay sdk.Coin + err error + } - // withdraw more collateral than available - uToken := collateral.Add(sdk.NewInt64Coin(uTokenDenom, 1)) + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a borrower which supplies and collateralizes UMEE, then borrows 10 UMEE + borrower := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(borrower, coin(umeeDenom, 150_000000)) + s.collateralize(borrower, coin("u/"+umeeDenom, 120_000000)) + s.borrow(borrower, coin(umeeDenom, 10_000000)) + + // create and fund a borrower which engages in a supply->borrow->supply loop + looper := s.newAccount(coin(umeeDenom, 50_000000)) + s.supply(looper, coin(umeeDenom, 50_000000)) + s.collateralize(looper, coin("u/"+umeeDenom, 50_000000)) + s.borrow(looper, coin(umeeDenom, 5_000000)) + s.supply(looper, coin(umeeDenom, 5_000000)) + + tcs := []testCase{ + { + "uToken", + borrower, + coin("u/"+umeeDenom, 100_000000), + sdk.Coin{}, + types.ErrUToken, + }, + { + "unregistered token", + borrower, + coin("abcd", 100_000000), + sdk.Coin{}, + types.ErrDenomNotBorrowed, + }, + { + "not borrowed", + borrower, + coin(atomDenom, 100_000000), + sdk.Coin{}, + types.ErrDenomNotBorrowed, + }, + { + "valid repay", + borrower, + coin(umeeDenom, 1_000000), + coin(umeeDenom, 1_000000), + nil, + }, + { + "additional repay", + borrower, + coin(umeeDenom, 3_000000), + coin(umeeDenom, 3_000000), + nil, + }, + { + "overpay", + borrower, + coin(umeeDenom, 30_000000), + coin(umeeDenom, 6_000000), + nil, + }, + { + "insufficient balance", + looper, + coin(umeeDenom, 1_000000), + sdk.Coin{}, + sdkerrors.ErrInsufficientFunds, + }, + } - withdrawn, err := s.app.LeverageKeeper.Withdraw(s.ctx, supplierAddr, uToken) - s.Require().EqualError(err, - "0 uToken balance + 1000000 from collateral is less than 1000001u/uumee to withdraw: insufficient balance", - ) - s.Require().Equal(sdk.Coin{}, withdrawn) + for _, tc := range tcs { + if tc.err != nil { + _, err := app.LeverageKeeper.Repay(ctx, tc.addr, tc.coin) + require.ErrorIs(err, tc.err, tc.msg) + } else { + // initial state + iBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + iCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + iUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + iExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + iBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify the output of repay function + repaid, err := app.LeverageKeeper.Repay(ctx, tc.addr, tc.coin) + require.NoError(err, tc.msg) + require.Equal(tc.expectedRepay, repaid, tc.msg) + + // final state + fBalance := app.BankKeeper.GetAllBalances(ctx, tc.addr) + fCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.addr) + fUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + fExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.coin.Denom) + fBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.addr) + + // verify token balance is decreased by expected amount + require.Equal(iBalance.Sub(tc.expectedRepay), fBalance, tc.msg, "balances") + // verify uToken collateral unchanged + require.Equal(iCollateral, fCollateral, tc.msg, "collateral") + // verify uToken supply is unchanged + require.Equal(iUTokenSupply, fUTokenSupply, tc.msg, "uToken supply") + // verify uToken exchange rate is unchanged + require.Equal(iExchangeRate, fExchangeRate, tc.msg, "uToken exchange rate") + // verify borrowed coins decreased by expected amount + s.requireEqualCoins(iBorrowed.Sub(tc.expectedRepay), fBorrowed, "borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } -func (s *IntegrationTestSuite) TestTotalCollateral() { - // Test zero collateral - uDenom := types.ToUTokenDenom(umeeDenom) - collateral := s.app.LeverageKeeper.GetTotalCollateral(s.ctx, uDenom) - s.Require().Equal(sdk.ZeroInt(), collateral) - - // Uses borrow scenario, because supplier possesses collateral - _, _ = s.initBorrowScenario() +func (s *IntegrationTestSuite) TestLiquidate() { + type testCase struct { + msg string + liquidator sdk.AccAddress + borrower sdk.AccAddress + attemptedRepay sdk.Coin + rewardDenom string + expectedRepay sdk.Coin + expectedLiquidate sdk.Coin + expectedReward sdk.Coin + err error + } - // Test nonzero collateral - collateral = s.app.LeverageKeeper.GetTotalCollateral(s.ctx, uDenom) - s.Require().Equal(sdk.NewInt(1000000000), collateral) -} + app, ctx, require := s.app, s.ctx, s.Require() + + // create and fund a liquidator which supplies plenty of UMEE and ATOM to the module + supplier := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + s.supply(supplier, coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + + // create and fund a liquidator which has 1000 UMEE and 1000 ATOM + liquidator := s.newAccount(coin(umeeDenom, 1000_000000), coin(atomDenom, 1000_000000)) + + // create a healthy borrower + healthyBorrower := s.newAccount(coin(umeeDenom, 100_000000)) + s.supply(healthyBorrower, coin(umeeDenom, 100_000000)) + s.collateralize(healthyBorrower, coin("u/"+umeeDenom, 100_000000)) + s.borrow(healthyBorrower, coin(umeeDenom, 10_000000)) + + // create a borrower which supplies and collateralizes 1000 ATOM + atomBorrower := s.newAccount(coin(atomDenom, 1000_000000)) + s.supply(atomBorrower, coin(atomDenom, 1000_000000)) + s.collateralize(atomBorrower, coin("u/"+atomDenom, 1000_000000)) + // artificially borrow 500 ATOM - this can be liquidated without bad debt + s.forceBorrow(atomBorrower, coin(atomDenom, 500_000000)) + + // create a borrower which collateralizes 110 UMEE + umeeBorrower := s.newAccount(coin(umeeDenom, 300_000000)) + s.supply(umeeBorrower, coin(umeeDenom, 200_000000)) + s.collateralize(umeeBorrower, coin("u/"+umeeDenom, 110_000000)) + // artificially borrow 200 UMEE - this will create a bad debt when liquidated + s.forceBorrow(umeeBorrower, coin(umeeDenom, 200_000000)) + + // creates a complex borrower with multiple denoms active + complexBorrower := s.newAccount(coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.supply(complexBorrower, coin(umeeDenom, 100_000000), coin(atomDenom, 100_000000)) + s.collateralize(complexBorrower, coin("u/"+umeeDenom, 100_000000), coin("u/"+atomDenom, 100_000000)) + // artificially borrow multiple denoms + s.forceBorrow(complexBorrower, coin(atomDenom, 30_000000), coin(umeeDenom, 30_000000)) + + // creates a realistic borrower with 400 UMEE collateral which will have a close factor < 1 + closeBorrower := s.newAccount(coin(umeeDenom, 400_000000)) + s.supply(closeBorrower, coin(umeeDenom, 400_000000)) + s.collateralize(closeBorrower, coin("u/"+umeeDenom, 400_000000)) + // artificially borrow just barely above liquidation threshold to simulate interest accruing + s.forceBorrow(closeBorrower, coin(umeeDenom, 102_000000)) + + tcs := []testCase{ + { + "healthy borrower", + liquidator, + healthyBorrower, + coin(atomDenom, 1_000000), + atomDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrLiquidationIneligible, + }, + { + "not borrowed denom", + liquidator, + umeeBorrower, + coin(atomDenom, 1_000000), + atomDenom, + sdk.Coin{}, + sdk.Coin{}, + sdk.Coin{}, + types.ErrLiquidationRepayZero, + }, + { + "direct atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 100_000000), + atomDenom, + coin(atomDenom, 100_000000), + coin("u/"+atomDenom, 109_000000), + coin(atomDenom, 109_000000), + nil, + }, + { + "u/atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 100_000000), + "u/" + atomDenom, + coin(atomDenom, 100_000000), + coin("u/"+atomDenom, 110_000000), + coin("u/"+atomDenom, 110_000000), + nil, + }, + { + "complete u/atom liquidation", + liquidator, + atomBorrower, + coin(atomDenom, 500_000000), + "u/" + atomDenom, + coin(atomDenom, 300_000000), + coin("u/"+atomDenom, 330_000000), + coin("u/"+atomDenom, 330_000000), + nil, + }, + { + "bad debt u/umee liquidation", + liquidator, + umeeBorrower, + coin(umeeDenom, 200_000000), + "u/" + umeeDenom, + coin(umeeDenom, 100_000000), + coin("u/"+umeeDenom, 110_000000), + coin("u/"+umeeDenom, 110_000000), + nil, + }, + { + "complex borrower", + liquidator, + complexBorrower, + coin(umeeDenom, 200_000000), + "u/" + atomDenom, + coin(umeeDenom, 30_000000), + coin("u/"+atomDenom, 3_527932), + coin("u/"+atomDenom, 3_527932), + nil, + }, + { + "close factor < 1", + liquidator, + closeBorrower, + coin(umeeDenom, 200_000000), + "u/" + umeeDenom, + coin(umeeDenom, 21_216000), + coin("u/"+umeeDenom, 23_337600), + coin("u/"+umeeDenom, 23_337600), + nil, + }, + } -func (s *IntegrationTestSuite) TestLiqudateBorrow() { - addr, _ := s.initBorrowScenario() - - // The "supplier" user from the init scenario is being used because it - // already has 1k u/umee for collateral. - - // user borrows 90 umee - err := s.app.LeverageKeeper.Borrow(s.ctx, addr, sdk.NewInt64Coin(umeeapp.BondDenom, 90000000)) - s.Require().NoError(err) - - // create an account and address which will represent a liquidator - liquidatorAddr := sdk.AccAddress([]byte("addr______________03")) - liquidatorAcc := s.app.AccountKeeper.NewAccountWithAddress(s.ctx, liquidatorAddr) - s.app.AccountKeeper.SetAccount(s.ctx, liquidatorAcc) - - // mint and send 10k umee to liquidator - s.Require().NoError(s.app.BankKeeper.MintCoins(s.ctx, minttypes.ModuleName, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee - )) - s.Require().NoError(s.app.BankKeeper.SendCoinsFromModuleToAccount(s.ctx, minttypes.ModuleName, liquidatorAddr, - sdk.NewCoins(sdk.NewInt64Coin(umeeapp.BondDenom, 10000000000)), // 10k umee, - )) - - // liquidator attempts to liquidate user, but user is ineligible (not over borrow limit) - repayment := sdk.NewInt64Coin(umeeapp.BondDenom, 30000000) // 30 umee - rewardDenom := types.ToUTokenDenom(umeeapp.BondDenom) - _, _, _, err = s.app.LeverageKeeper.Liquidate(s.ctx, liquidatorAddr, addr, repayment, rewardDenom) - s.Require().Error(err) - - // set umee liquidation threshold to 0.01 to allow liquidation - umeeToken, err := s.app.LeverageKeeper.GetTokenSettings(s.ctx, umeeDenom) - s.Require().NoError(err) - umeeToken.CollateralWeight = sdk.MustNewDecFromStr("0.01") - umeeToken.LiquidationThreshold = sdk.MustNewDecFromStr("0.01") - - s.Require().NoError(s.app.LeverageKeeper.SetTokenSettings(s.ctx, umeeToken)) - - // liquidator partially liquidates user, receiving some uTokens - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 10000000) // 10 umee - repaid, liquidated, reward, err := s.app.LeverageKeeper.Liquidate( - s.ctx, liquidatorAddr, addr, repayment, types.ToUTokenDenom(umeeDenom), - ) - s.Require().NoError(err) - s.Require().Equal(repayment, repaid) // 10 umee - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 11000000), liquidated) // 11 u/umee - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 11000000), reward) // 11 u/umee - - // verify borrower's new borrowed amount is 80 umee (still over borrow limit) - borrowed := s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 80000000), borrowed) - - // verify borrower's new collateral amount (1k - 11) = 989 u/umee - collateral := s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, types.ToUTokenDenom(umeeDenom)) - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 989000000), collateral) - - // verify liquidator's new u/umee balance = 11 = (10 + liquidation incentive) - uTokenBalance := s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(sdk.NewInt64Coin(rewardDenom, 11000000), uTokenBalance) - - // verify liquidator's new umee balance (10k - 11) = 9990 umee - tokenBalance := s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 9990000000), tokenBalance) - - // liquidator partially liquidates user, receiving base tokens directly at slightly reduced incentive - repaid, liquidated, reward, err = s.app.LeverageKeeper.Liquidate( - s.ctx, liquidatorAddr, addr, repayment, umeeDenom, - ) - s.Require().NoError(err) - s.Require().Equal(repayment, repaid) // 10 umee - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 10900000), liquidated) // 10.9 u/umee - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 10900000), reward) // 10.9 umee - - // verify borrower's new borrow amount is 70 umee (still over borrow limit) - borrowed = s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 70000000), borrowed) - - // verify borrower's new collateral amount (989 - 10.9) = 978.1 u/umee - collateral = s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, types.ToUTokenDenom(umeeDenom)) - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 978100000), collateral) - - // verify liquidator's u/umee balance = 11 (unchanged) - uTokenBalance = s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(sdk.NewInt64Coin(rewardDenom, 11000000), uTokenBalance) - - // verify liquidator's new umee balance (9990 - 10 + 10.9) = 9990.9 umee - tokenBalance = s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 9990900000), tokenBalance) - - // liquidator fully liquidates user, receiving more collateral and reducing borrowed amount to zero - repayment = sdk.NewInt64Coin(umeeapp.BondDenom, 300000000) // 300 umee - repaid, liquidated, reward, err = s.app.LeverageKeeper.Liquidate( - s.ctx, liquidatorAddr, addr, repayment, types.ToUTokenDenom(umeeDenom), - ) - s.Require().NoError(err) - s.Require().Equal(sdk.NewInt64Coin(umeeDenom, 70000000), repaid) // 70 umee - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 77000000), liquidated) // 77 u/umee - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 77000000), reward) // 77 u/umee - - // verify that repayment has not been modified - s.Require().Equal(sdk.NewInt(300000000), repayment.Amount) - - // verify liquidator's new u/umee balance = 88 = (11 + 77) - uTokenBalance = s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, rewardDenom) - s.Require().Equal(sdk.NewInt64Coin(rewardDenom, 88000000), uTokenBalance) - - // verify borrower's new borrowed amount is zero - borrowed = s.app.LeverageKeeper.GetBorrow(s.ctx, addr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 0), borrowed) - - // verify borrower's new collateral amount (978.1 - 77) = 901.1 u/umee - collateral = s.app.LeverageKeeper.GetCollateralAmount(s.ctx, addr, types.ToUTokenDenom(umeeDenom)) - s.Require().Equal(sdk.NewInt64Coin(types.ToUTokenDenom(umeeDenom), 901100000), collateral) - - // verify liquidator's new umee balance (9990.9 - 70) = 9920.9 umee - tokenBalance = s.app.BankKeeper.GetBalance(s.ctx, liquidatorAddr, umeeapp.BondDenom) - s.Require().Equal(sdk.NewInt64Coin(umeeapp.BondDenom, 9920900000), tokenBalance) + for _, tc := range tcs { + if tc.err != nil { + _, _, _, err := app.LeverageKeeper.Liquidate( + ctx, tc.liquidator, tc.borrower, tc.attemptedRepay, tc.rewardDenom, + ) + require.ErrorIs(err, tc.err, tc.msg) + } else { + baseRewardDenom := types.ToTokenDenom(tc.expectedLiquidate.Denom) + + // initial state (borrowed denom) + biUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + biExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) + + // initial state (liquidated denom) + liUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + liExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) + + // borrower initial state + biBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) + biCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) + biBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) + + // liquidator initial state + liBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) + liCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) + liBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) + + // verify the output of liquidate function + repaid, liquidated, reward, err := app.LeverageKeeper.Liquidate( + ctx, tc.liquidator, tc.borrower, tc.attemptedRepay, tc.rewardDenom, + ) + require.NoError(err, tc.msg) + require.Equal(tc.expectedRepay, repaid, tc.msg, "repaid") + require.Equal(tc.expectedLiquidate, liquidated, tc.msg, "liquidated") + require.Equal(tc.expectedReward, reward, tc.msg, "reward") + + // final state (liquidated denom) + lfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + lfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, baseRewardDenom) + + // borrower final state + bfBalance := app.BankKeeper.GetAllBalances(ctx, tc.borrower) + bfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.borrower) + bfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.borrower) + + // liquidator final state + lfBalance := app.BankKeeper.GetAllBalances(ctx, tc.liquidator) + lfCollateral := app.LeverageKeeper.GetBorrowerCollateral(ctx, tc.liquidator) + lfBorrowed := app.LeverageKeeper.GetBorrowerBorrows(ctx, tc.liquidator) + + // if borrowed denom and reward denom are different, then borrowed denom uTokens should be unaffected + if tc.rewardDenom != tc.attemptedRepay.Denom { + // final state (borrowed denom) + bfUTokenSupply := app.LeverageKeeper.GetAllUTokenSupply(ctx) + bfExchangeRate := app.LeverageKeeper.DeriveExchangeRate(ctx, tc.attemptedRepay.Denom) + + // verify borrowed denom uToken supply is unchanged + require.Equal(biUTokenSupply, bfUTokenSupply, tc.msg, "uToken supply (borrowed denom") + // verify borrowed denom uToken exchange rate is unchanged + require.Equal(biExchangeRate, bfExchangeRate, tc.msg, "uToken exchange rate (borrowed denom") + } + + // verify liquidated denom uToken supply is unchanged if indirect liquidation, or reduced if direct + expectedLiquidatedUTokenSupply := liUTokenSupply + if !types.HasUTokenPrefix(tc.rewardDenom) { + expectedLiquidatedUTokenSupply = expectedLiquidatedUTokenSupply.Sub(tc.expectedLiquidate) + } + require.Equal(expectedLiquidatedUTokenSupply, lfUTokenSupply, tc.msg, "uToken supply (liquidated denom") + // verify liquidated denom uToken exchange rate is unchanged + require.Equal(liExchangeRate, lfExchangeRate, tc.msg, "uToken exchange rate (liquidated denom") + + // verify borrower balances unchanged + require.Equal(biBalance, bfBalance, tc.msg, "borrower balances") + // verify borrower collateral reduced by the expected amount + s.requireEqualCoins(biCollateral.Sub(tc.expectedLiquidate), bfCollateral, tc.msg, "borrower collateral") + // verify borrowed coins decreased by expected amount + s.requireEqualCoins(biBorrowed.Sub(tc.expectedRepay), bfBorrowed, "borrowed coins") + + // verify liquidator balance changes by expected amounts + require.Equal(liBalance.Sub(tc.expectedRepay).Add(tc.expectedReward), lfBalance, + tc.msg, "liquidator balances") + // verify liquidator collateral unchanged + require.Equal(liCollateral, lfCollateral, tc.msg, "liquidator collateral") + // verify liquidator borrowed coins unchanged + s.requireEqualCoins(liBorrowed, lfBorrowed, "liquidator borrowed coins") + + // check all available invariants + s.checkInvariants(tc.msg) + } + } } diff --git a/x/leverage/keeper/math_test.go b/x/leverage/keeper/math_test.go index b2351466a0..e357b9b37f 100644 --- a/x/leverage/keeper/math_test.go +++ b/x/leverage/keeper/math_test.go @@ -15,28 +15,28 @@ func TestInterpolate(t *testing.T) { y2 := sdk.MustNewDecFromStr("17.4") // Sloped line, endpoint checks - x := Interpolate(x1, x1, y1, x2, y2) - require.Equal(t, x, y1) - x = Interpolate(x2, x1, y1, x2, y2) - require.Equal(t, x, y2) + result := Interpolate(x1, x1, y1, x2, y2) + require.Equal(t, y1, result) + result = Interpolate(x2, x1, y1, x2, y2) + require.Equal(t, y2, result) // Sloped line, point on segment - x = Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) - require.Equal(t, x, sdk.MustNewDecFromStr("13.2")) + result = Interpolate(sdk.MustNewDecFromStr("4.0"), x1, y1, x2, y2) + require.Equal(t, sdk.MustNewDecFromStr("13.2"), result) // Sloped line, point outside of segment - x = Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) - require.Equal(t, x, sdk.MustNewDecFromStr("9.0")) + result = Interpolate(sdk.MustNewDecFromStr("2.0"), x1, y1, x2, y2) + require.Equal(t, sdk.MustNewDecFromStr("9.0"), result) // Vertical line: always return y1 - x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) - require.Equal(t, x, y1) - x = Interpolate(x1, x1, y1, x1, y2) - require.Equal(t, x, y1) + result = Interpolate(sdk.ZeroDec(), x1, y1, x1, y2) + require.Equal(t, y1, result) + result = Interpolate(x1, x1, y1, x1, y2) + require.Equal(t, y1, result) // Undefined line (x1=x2, y1=y2): always return y1 - x = Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) - require.Equal(t, x, y1) - x = Interpolate(x1, x1, y1, x1, y1) - require.Equal(t, x, y1) + result = Interpolate(sdk.ZeroDec(), x1, y1, x1, y1) + require.Equal(t, y1, result) + result = Interpolate(x1, x1, y1, x1, y1) + require.Equal(t, y1, result) } diff --git a/x/leverage/keeper/oracle_test.go b/x/leverage/keeper/oracle_test.go index b6d9e10d19..3c6bacfddc 100644 --- a/x/leverage/keeper/oracle_test.go +++ b/x/leverage/keeper/oracle_test.go @@ -6,6 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" umeeapp "github.com/umee-network/umee/v3/app" + "github.com/umee-network/umee/v3/x/leverage/types" ) type mockOracleKeeper struct { @@ -43,69 +44,77 @@ func (m *mockOracleKeeper) GetExchangeRateBase(ctx sdk.Context, denom string) (s func (m *mockOracleKeeper) Reset() { m.exchangeRates = map[string]sdk.Dec{ umeeapp.BondDenom: sdk.MustNewDecFromStr("4.21"), - atomIBCDenom: sdk.MustNewDecFromStr("39.38"), + atomDenom: sdk.MustNewDecFromStr("39.38"), } } func (s *IntegrationTestSuite) TestOracle_TokenPrice() { - p, err := s.app.LeverageKeeper.TokenPrice(s.ctx, umeeapp.BondDenom) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("0.00000421"), p) + app, ctx, require := s.app, s.ctx, s.Require() - p, err = s.app.LeverageKeeper.TokenPrice(s.ctx, atomIBCDenom) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("0.00003938"), p) + p, err := app.LeverageKeeper.TokenPrice(ctx, umeeapp.BondDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("0.00000421"), p) - p, err = s.app.LeverageKeeper.TokenPrice(s.ctx, "foo") - s.Require().Error(err) - s.Require().Equal(sdk.ZeroDec(), p) + p, err = app.LeverageKeeper.TokenPrice(ctx, atomDenom) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("0.00003938"), p) + + p, err = app.LeverageKeeper.TokenPrice(ctx, "foo") + require.ErrorIs(err, types.ErrNotRegisteredToken) + require.Equal(sdk.ZeroDec(), p) } func (s *IntegrationTestSuite) TestOracle_TokenValue() { - // 2.4umee * $4.21 - v, err := s.app.LeverageKeeper.TokenValue(s.ctx, sdk.NewInt64Coin(umeeapp.BondDenom, 2400000)) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("10.104"), v) - - v, err = s.app.LeverageKeeper.TokenValue(s.ctx, sdk.NewInt64Coin("foo", 2400000)) - s.Require().Error(err) - s.Require().Equal(sdk.ZeroDec(), v) + app, ctx, require := s.app, s.ctx, s.Require() + + // 2.4 UMEE * $4.21 + v, err := app.LeverageKeeper.TokenValue(ctx, coin(umeeapp.BondDenom, 2_400000)) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("10.104"), v) + + v, err = app.LeverageKeeper.TokenValue(ctx, coin("foo", 2_400000)) + require.ErrorIs(err, types.ErrNotRegisteredToken) + require.Equal(sdk.ZeroDec(), v) } func (s *IntegrationTestSuite) TestOracle_TotalTokenValue() { - // (2.4umee * $4.21) + (4.7atom * $39.38) - v, err := s.app.LeverageKeeper.TotalTokenValue( - s.ctx, + app, ctx, require := s.app, s.ctx, s.Require() + + // (2.4 UMEE * $4.21) + (4.7 ATOM * $39.38) + v, err := app.LeverageKeeper.TotalTokenValue( + ctx, sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 2400000), - sdk.NewInt64Coin(atomIBCDenom, 4700000), + coin(umeeapp.BondDenom, 2_400000), + coin(atomDenom, 4_700000), ), ) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("195.19"), v) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("195.19"), v) // same result, as unregistered token is ignored - v, err = s.app.LeverageKeeper.TotalTokenValue( - s.ctx, + v, err = app.LeverageKeeper.TotalTokenValue( + ctx, sdk.NewCoins( - sdk.NewInt64Coin(umeeapp.BondDenom, 2400000), - sdk.NewInt64Coin(atomIBCDenom, 4700000), - sdk.NewInt64Coin("foo", 4700000), + coin(umeeapp.BondDenom, 2_400000), + coin(atomDenom, 4_700000), + coin("foo", 4_700000), ), ) - s.Require().NoError(err) - s.Require().Equal(sdk.MustNewDecFromStr("195.19"), v) + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("195.19"), v) } func (s *IntegrationTestSuite) TestOracle_PriceRatio() { - r, err := s.app.LeverageKeeper.PriceRatio(s.ctx, umeeapp.BondDenom, atomIBCDenom) - s.Require().NoError(err) + app, ctx, require := s.app, s.ctx, s.Require() + + r, err := app.LeverageKeeper.PriceRatio(ctx, umeeapp.BondDenom, atomDenom) + require.NoError(err) // $4.21 / $39.38 - s.Require().Equal(sdk.MustNewDecFromStr("0.106907059421025901"), r) + require.Equal(sdk.MustNewDecFromStr("0.106907059421025901"), r) - _, err = s.app.LeverageKeeper.PriceRatio(s.ctx, "foo", atomIBCDenom) - s.Require().Error(err) + _, err = app.LeverageKeeper.PriceRatio(ctx, "foo", atomDenom) + require.ErrorIs(err, types.ErrNotRegisteredToken) - _, err = s.app.LeverageKeeper.PriceRatio(s.ctx, umeeapp.BondDenom, "foo") - s.Require().Error(err) + _, err = app.LeverageKeeper.PriceRatio(ctx, umeeapp.BondDenom, "foo") + require.ErrorIs(err, types.ErrNotRegisteredToken) } diff --git a/x/leverage/keeper/reserves_test.go b/x/leverage/keeper/reserves_test.go new file mode 100644 index 0000000000..f098bb3992 --- /dev/null +++ b/x/leverage/keeper/reserves_test.go @@ -0,0 +1,76 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + + umeeapp "github.com/umee-network/umee/v3/app" +) + +func (s *IntegrationTestSuite) TestSetReserves() { + app, ctx, require := s.app, s.ctx, s.Require() + + // get initial reserves + amount := app.LeverageKeeper.GetReserveAmount(ctx, umeeapp.BondDenom) + require.Equal(sdk.ZeroInt(), amount) + + // artifically reserve 200 umee + s.setReserves(coin(umeeapp.BondDenom, 200_000000)) + // get new reserves + amount = app.LeverageKeeper.GetReserveAmount(ctx, umeeapp.BondDenom) + require.Equal(sdk.NewInt(200_000000), amount) +} + +func (s *IntegrationTestSuite) TestRepayBadDebt() { + app, ctx, require := s.app, s.ctx, s.Require() + + // Creating a supplier so module account has some uumee + addr := s.newAccount(coin(umeeDenom, 200_000000)) + s.supply(addr, coin(umeeDenom, 200_000000)) + + // Using an address with no assets + addr2 := s.newAccount() + + // Create an uncollateralized debt position + badDebt := coin(umeeDenom, 100_000000) + err := s.tk.SetBorrow(ctx, addr2, badDebt) + require.NoError(err) + + // Manually mark the bad debt for repayment + require.NoError(s.tk.SetBadDebtAddress(ctx, addr2, umeeDenom, true)) + + // Manually set reserves to 60 umee + reserve := coin(umeeDenom, 60_000000) + s.setReserves(reserve) + + // Sweep all bad debts, which should repay 60 umee of the bad debt (partial repayment) + err = app.LeverageKeeper.SweepBadDebts(ctx) + require.NoError(err) + + // Confirm that a debt of 40 umee remains + remainingDebt := app.LeverageKeeper.GetBorrow(ctx, addr2, umeeDenom) + require.Equal(coin(umeeDenom, 40_000000), remainingDebt) + + // Confirm that reserves are exhausted + remainingReserve := app.LeverageKeeper.GetReserveAmount(ctx, umeeDenom) + require.Equal(sdk.ZeroInt(), remainingReserve) + + // Manually set reserves to 70 umee + reserve = coin(umeeDenom, 70_000000) + s.setReserves(reserve) + + // Sweep all bad debts, which should fully repay the bad debt this time + err = app.LeverageKeeper.SweepBadDebts(ctx) + require.NoError(err) + + // Confirm that the debt is eliminated + remainingDebt = app.LeverageKeeper.GetBorrow(ctx, addr2, umeeDenom) + require.Equal(coin(umeeDenom, 0), remainingDebt) + + // Confirm that reserves are now at 30 umee + remainingReserve = app.LeverageKeeper.GetReserveAmount(ctx, umeeDenom) + require.Equal(sdk.NewInt(30_000000), remainingReserve) + + // Sweep all bad debts - but there are none + err = app.LeverageKeeper.SweepBadDebts(ctx) + require.NoError(err) +} diff --git a/x/leverage/keeper/suite_test.go b/x/leverage/keeper/suite_test.go new file mode 100644 index 0000000000..55e3f1f9ab --- /dev/null +++ b/x/leverage/keeper/suite_test.go @@ -0,0 +1,211 @@ +package keeper_test + +import ( + "fmt" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/baseapp" + sdk "github.com/cosmos/cosmos-sdk/types" + minttypes "github.com/cosmos/cosmos-sdk/x/mint/types" + "github.com/stretchr/testify/suite" + tmrand "github.com/tendermint/tendermint/libs/rand" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + umeeapp "github.com/umee-network/umee/v3/app" + "github.com/umee-network/umee/v3/x/leverage" + "github.com/umee-network/umee/v3/x/leverage/fixtures" + "github.com/umee-network/umee/v3/x/leverage/keeper" + "github.com/umee-network/umee/v3/x/leverage/types" +) + +const ( + umeeDenom = umeeapp.BondDenom + atomDenom = fixtures.AtomDenom +) + +type IntegrationTestSuite struct { + suite.Suite + + ctx sdk.Context + app *umeeapp.UmeeApp + tk keeper.TestKeeper + queryClient types.QueryClient + setupAccountCounter sdkmath.Int +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} + +func (s *IntegrationTestSuite) SetupTest() { + require := s.Require() + app := umeeapp.Setup(s.T(), false, 1) + ctx := app.BaseApp.NewContext(false, tmproto.Header{ + ChainID: fmt.Sprintf("test-chain-%s", tmrand.Str(4)), + Height: 1, + Time: time.Unix(0, 0), + }) + + umeeToken := newToken(umeeapp.BondDenom, "UMEE") + atomIBCToken := newToken(atomDenom, "ATOM") + + // we only override the Leverage keeper so we can supply a custom mock oracle + k, tk := keeper.NewTestKeeper( + s.Require(), + app.AppCodec(), + app.GetKey(types.ModuleName), + app.GetSubspace(types.ModuleName), + app.BankKeeper, + newMockOracleKeeper(), + ) + + s.tk = tk + app.LeverageKeeper = k + app.LeverageKeeper = *app.LeverageKeeper.SetHooks(types.NewMultiHooks()) + + leverage.InitGenesis(ctx, app.LeverageKeeper, *types.DefaultGenesis()) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, umeeToken)) + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, atomIBCToken)) + + queryHelper := baseapp.NewQueryServerTestHelper(ctx, app.InterfaceRegistry()) + types.RegisterQueryServer(queryHelper, keeper.NewQuerier(app.LeverageKeeper)) + + s.app = app + s.ctx = ctx + s.setupAccountCounter = sdkmath.ZeroInt() + s.queryClient = types.NewQueryClient(queryHelper) +} + +// requireEqualCoins compares two sdk.Coins in such a way that sdk.Coins(nil) == sdk.Coins([]sdk.Coin{}) +func (s *IntegrationTestSuite) requireEqualCoins(coinsA, coinsB sdk.Coins, msgAndArgs ...interface{}) { + s.Require().Equal( + sdk.NewCoins(coinsA...), + sdk.NewCoins(coinsB...), + msgAndArgs..., + ) +} + +// newToken creates a test token with reasonable initial parameters +func newToken(base, symbol string) types.Token { + return fixtures.Token(base, symbol) +} + +// coin creates a coin with a given base denom and amount +func coin(denom string, amount int64) sdk.Coin { + return sdk.NewInt64Coin(denom, amount) +} + +// newAccount creates a new account for testing, and funds it with any input tokens. +func (s *IntegrationTestSuite) newAccount(funds ...sdk.Coin) sdk.AccAddress { + app, ctx := s.app, s.ctx + + // create a unique address + s.setupAccountCounter = s.setupAccountCounter.Add(sdk.OneInt()) + addrStr := fmt.Sprintf("%-20s", "addr"+s.setupAccountCounter.String()+"_______________") + addr := sdk.AccAddress([]byte(addrStr)) + + // register the account in AccountKeeper + acct := app.AccountKeeper.NewAccountWithAddress(ctx, addr) + app.AccountKeeper.SetAccount(ctx, acct) + + s.fundAccount(addr, funds...) + + return addr +} + +// fundAccount mints and sends tokens to an account for testing. +func (s *IntegrationTestSuite) fundAccount(addr sdk.AccAddress, funds ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + coins := sdk.NewCoins(funds...) + if !coins.IsZero() { + // mint and send tokens to account + require.NoError(app.BankKeeper.MintCoins(ctx, minttypes.ModuleName, coins)) + require.NoError(app.BankKeeper.SendCoinsFromModuleToAccount(ctx, minttypes.ModuleName, addr, coins)) + } +} + +// supply tokens from an account and require no errors. Use when setting up leverage scenarios. +func (s *IntegrationTestSuite) supply(addr sdk.AccAddress, coins ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range coins { + _, err := app.LeverageKeeper.Supply(ctx, addr, coin) + require.NoError(err, "supply") + } +} + +// withdraw utokens from an account and require no errors. Use when setting up leverage scenarios. +func (s *IntegrationTestSuite) withdraw(addr sdk.AccAddress, coins ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range coins { + _, err := app.LeverageKeeper.Withdraw(ctx, addr, coin) + require.NoError(err, "withdraw") + } +} + +// collateralize uTokens from an account and require no errors. Use when setting up leverage scenarios. +func (s *IntegrationTestSuite) collateralize(addr sdk.AccAddress, uTokens ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range uTokens { + err := app.LeverageKeeper.Collateralize(ctx, addr, coin) + require.NoError(err, "collateralize") + } +} + +// decollateralize uTokens from an account and require no errors. Use when setting up leverage scenarios. +func (s *IntegrationTestSuite) decollateralize(addr sdk.AccAddress, uTokens ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range uTokens { + err := app.LeverageKeeper.Decollateralize(ctx, addr, coin) + require.NoError(err, "decollateralize") + } +} + +// borrow tokens as an account and require no errors. Use when setting up leverage scenarios. +func (s *IntegrationTestSuite) borrow(addr sdk.AccAddress, coins ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range coins { + err := app.LeverageKeeper.Borrow(ctx, addr, coin) + require.NoError(err, "borrow") + } +} + +// forceBorrow artificially borrows tokens with an account, ignoring collateral, to set up liquidation scenarios. +// this does not alter uToken exchange rates as artificially accruing interest would. +func (s *IntegrationTestSuite) forceBorrow(addr sdk.AccAddress, coins ...sdk.Coin) { + app, ctx, require := s.app, s.ctx, s.Require() + + for _, coin := range coins { + borrowed := s.tk.GetBorrow(ctx, addr, coin.Denom) + err := s.tk.SetBorrow(ctx, addr, borrowed.Add(coin)) + require.NoError(err, "forceBorrow") + } + + err := app.BankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleName, addr, coins) + require.NoError(err, "forceBorroww") +} + +// setReserves artificially sets reserves of one or more tokens to given values +func (s *IntegrationTestSuite) setReserves(coins ...sdk.Coin) { + ctx, require := s.ctx, s.Require() + + for _, coin := range coins { + err := s.tk.SetReserveAmount(ctx, coin) + require.NoError(err, "setReserves") + } +} + +// checkInvariants is used during other tests to quickly test all invariants +func (s *IntegrationTestSuite) checkInvariants(msg string) { + app, ctx, require := s.app, s.ctx, s.Require() + + desc, broken := keeper.AllInvariants(app.LeverageKeeper)(ctx) + require.False(broken, msg, desc) +} diff --git a/x/leverage/keeper/token.go b/x/leverage/keeper/token.go index 16f7deb541..fb7fe25eea 100644 --- a/x/leverage/keeper/token.go +++ b/x/leverage/keeper/token.go @@ -1,8 +1,6 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -20,7 +18,7 @@ func (k Keeper) SetTokenSettings(ctx sdk.Context, token types.Token) error { bz, err := k.cdc.Marshal(&token) if err != nil { - panic(fmt.Errorf("failed to encode token settings: %w", err)) + return err } k.hooks.AfterTokenRegistered(ctx, token) @@ -42,7 +40,7 @@ func (k Keeper) GetTokenSettings(ctx sdk.Context, denom string) (types.Token, er token := types.Token{} bz := store.Get(tokenKey) if len(bz) == 0 { - return token, sdkerrors.Wrap(types.ErrInvalidAsset, denom) + return token, sdkerrors.Wrap(types.ErrNotRegisteredToken, denom) } err := k.cdc.Unmarshal(bz, &token) diff --git a/x/leverage/keeper/token_test.go b/x/leverage/keeper/token_test.go new file mode 100644 index 0000000000..e92e130641 --- /dev/null +++ b/x/leverage/keeper/token_test.go @@ -0,0 +1,27 @@ +package keeper_test + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +func (s *IntegrationTestSuite) TestGetToken() { + app, ctx, require := s.app, s.ctx, s.Require() + + uabc := newToken("uabc", "ABC") + require.NoError(app.LeverageKeeper.SetTokenSettings(ctx, uabc)) + + t, err := app.LeverageKeeper.GetTokenSettings(ctx, "uabc") + require.NoError(err) + require.Equal(sdk.MustNewDecFromStr("0.2"), t.ReserveFactor) + require.Equal(sdk.MustNewDecFromStr("0.25"), t.CollateralWeight) + require.Equal(sdk.MustNewDecFromStr("0.25"), t.LiquidationThreshold) + require.Equal(sdk.MustNewDecFromStr("0.02"), t.BaseBorrowRate) + require.Equal(sdk.MustNewDecFromStr("0.22"), t.KinkBorrowRate) + require.Equal(sdk.MustNewDecFromStr("1.52"), t.MaxBorrowRate) + require.Equal(sdk.MustNewDecFromStr("0.8"), t.KinkUtilization) + require.Equal(sdk.MustNewDecFromStr("0.1"), t.LiquidationIncentive) + + require.NoError(t.AssertBorrowEnabled()) + require.NoError(t.AssertSupplyEnabled()) + require.NoError(t.AssertNotBlacklisted()) +} diff --git a/x/leverage/keeper/validate.go b/x/leverage/keeper/validate.go index 12b0fb8ab1..59e5eaedb3 100644 --- a/x/leverage/keeper/validate.go +++ b/x/leverage/keeper/validate.go @@ -9,6 +9,9 @@ import ( // validateAcceptedDenom validates an sdk.Coin and ensures it is a registered Token // with Blacklisted == false func (k Keeper) validateAcceptedDenom(ctx sdk.Context, denom string) error { + if types.HasUTokenPrefix(denom) { + return types.ErrUToken.Wrap(denom) + } token, err := k.GetTokenSettings(ctx, denom) if err != nil { return err @@ -44,22 +47,39 @@ func (k Keeper) validateAcceptedUToken(ctx sdk.Context, coin sdk.Coin) error { } // 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 { +func (k Keeper) validateSupply(ctx sdk.Context, coin sdk.Coin) error { + if err := coin.Validate(); err != nil { return err } - token, err := k.GetTokenSettings(ctx, loan.Denom) + if types.HasUTokenPrefix(coin.Denom) { + return types.ErrUToken.Wrap(coin.Denom) + } + token, err := k.GetTokenSettings(ctx, coin.Denom) if err != nil { return err } return token.AssertSupplyEnabled() } +// validateUToken validates an sdk.Coin and ensures its Denom is a uToken. Used by Withdraw and Decollateralize. +func (k Keeper) validateUToken(coin sdk.Coin) error { + if err := coin.Validate(); err != nil { + return err + } + if !types.HasUTokenPrefix(coin.Denom) { + return types.ErrNotUToken.Wrap(coin.Denom) + } + return nil +} + // validateBorrow validates an sdk.Coin and ensures its Denom is a Token with EnableMsgBorrow func (k Keeper) validateBorrow(ctx sdk.Context, borrow sdk.Coin) error { if err := borrow.Validate(); err != nil { return err } + if types.HasUTokenPrefix(borrow.Denom) { + return types.ErrUToken.Wrap(borrow.Denom) + } token, err := k.GetTokenSettings(ctx, borrow.Denom) if err != nil { return err @@ -67,9 +87,20 @@ func (k Keeper) validateBorrow(ctx sdk.Context, borrow sdk.Coin) error { return token.AssertBorrowEnabled() } -// validateCollateralAsset validates an sdk.Coin and ensures it is a uToken of an accepted +// validateRepay validates an sdk.Coin and ensures its Denom is not a uToken +func (k Keeper) validateRepay(coin sdk.Coin) error { + if err := coin.Validate(); err != nil { + return err + } + if types.HasUTokenPrefix(coin.Denom) { + return types.ErrUToken.Wrap(coin.Denom) + } + return nil +} + +// validateCollateralize 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 { +func (k Keeper) validateCollateralize(ctx sdk.Context, collateral sdk.Coin) error { if err := collateral.Validate(); err != nil { return err } diff --git a/x/leverage/simulation/operations.go b/x/leverage/simulation/operations.go index 667956a722..f415dcfea5 100644 --- a/x/leverage/simulation/operations.go +++ b/x/leverage/simulation/operations.go @@ -19,7 +19,7 @@ const ( DefaultWeightMsgSupply int = 100 DefaultWeightMsgWithdraw int = 85 DefaultWeightMsgBorrow int = 80 - DefaultWeightMsgCollateralize int = 60 + DefaultWeightMsgCollateralize int = 65 DefaultWeightMsgDecollateralize int = 60 DefaultWeightMsgRepay int = 70 DefaultWeightMsgLiquidate int = 75 @@ -88,28 +88,28 @@ func WeightedOperations( SimulateMsgSupply(ak, bk), ), simulation.NewWeightedOperation( - weightMsgWithdraw, - SimulateMsgWithdraw(ak, bk, lk), + weightMsgCollateralize, + SimulateMsgCollateralize(ak, bk, lk), ), simulation.NewWeightedOperation( weightMsgBorrow, SimulateMsgBorrow(ak, bk, lk), ), simulation.NewWeightedOperation( - weightMsgCollateralize, - SimulateMsgCollateralize(ak, bk, lk), - ), - simulation.NewWeightedOperation( - weightMsgDecollateralize, - SimulateMsgDecollateralize(ak, bk, lk), + weightMsgLiquidate, + SimulateMsgLiquidate(ak, bk, lk), ), simulation.NewWeightedOperation( weightMsgRepay, SimulateMsgRepay(ak, bk, lk), ), simulation.NewWeightedOperation( - weightMsgLiquidate, - SimulateMsgLiquidate(ak, bk, lk), + weightMsgDecollateralize, + SimulateMsgDecollateralize(ak, bk, lk), + ), + simulation.NewWeightedOperation( + weightMsgWithdraw, + SimulateMsgWithdraw(ak, bk, lk), ), } } @@ -355,7 +355,7 @@ func randomDecollateralizeFields( } // randomBorrowFields returns a random account and an sdk.Coin from all -// the registered tokens with an random amount [0, 150]. +// the registered tokens with an random amount [0, 10^6]. // It returns skip=true if no registered token was found. func randomBorrowFields( r *rand.Rand, ctx sdk.Context, accs []simtypes.Account, lk keeper.Keeper, @@ -368,7 +368,7 @@ func randomBorrowFields( } registeredToken := allTokens[r.Int31n(int32(len(allTokens)))] - token = sdk.NewCoin(registeredToken.BaseDenom, simtypes.RandomAmount(r, sdk.NewInt(150))) + token = sdk.NewCoin(registeredToken.BaseDenom, simtypes.RandomAmount(r, sdk.NewInt(1_000000))) return acc, token, false } @@ -401,10 +401,9 @@ func randomLiquidateFields( rewardDenom string, skip bool, ) { - idxLiquidator := r.Intn(len(accs) - 1) - - liquidator = accs[idxLiquidator] - borrower = accs[idxLiquidator+1] + // note: liquidator and borrower might even be the same account + liquidator, _ = simtypes.RandomAcc(r, accs) + borrower, _ = simtypes.RandomAcc(r, accs) collateral := lk.GetBorrowerCollateral(ctx, borrower.Address) if collateral.Empty() { @@ -418,6 +417,19 @@ func randomLiquidateFields( return liquidator, borrower, sdk.Coin{}, "", true } + liquidationThreshold, err := lk.CalculateLiquidationThreshold(ctx, collateral) + if err != nil { + return liquidator, borrower, sdk.Coin{}, "", true + } + borrowedValue, err := lk.TotalTokenValue(ctx, borrowed) + if err != nil { + return liquidator, borrower, sdk.Coin{}, "", true + } + if borrowedValue.LTE(liquidationThreshold) { + // borrower not eligible for liquidation + return liquidator, borrower, sdk.Coin{}, "", true + } + 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 8bfa26348b..5a16644ea3 100644 --- a/x/leverage/simulation/operations_test.go +++ b/x/leverage/simulation/operations_test.go @@ -14,6 +14,7 @@ import ( umeeapp "github.com/umee-network/umee/v3/app" "github.com/umee-network/umee/v3/x/leverage" + "github.com/umee-network/umee/v3/x/leverage/fixtures" "github.com/umee-network/umee/v3/x/leverage/simulation" "github.com/umee-network/umee/v3/x/leverage/types" ) @@ -32,73 +33,11 @@ func (s *SimTestSuite) SetupTest() { app := umeeapp.Setup(s.T(), checkTx, 1) ctx := app.NewContext(checkTx, tmproto.Header{}) - umeeToken := types.Token{ - BaseDenom: umeeapp.BondDenom, - ReserveFactor: sdk.MustNewDecFromStr("0.25"), - CollateralWeight: sdk.MustNewDecFromStr("0.5"), - LiquidationThreshold: sdk.MustNewDecFromStr("0.5"), - BaseBorrowRate: sdk.MustNewDecFromStr("0.02"), - KinkBorrowRate: sdk.MustNewDecFromStr("0.2"), - MaxBorrowRate: sdk.MustNewDecFromStr("1.0"), - KinkUtilization: sdk.MustNewDecFromStr("0.8"), - LiquidationIncentive: sdk.MustNewDecFromStr("0.1"), - SymbolDenom: umeeapp.DisplayDenom, - Exponent: 6, - EnableMsgSupply: true, - EnableMsgBorrow: true, - Blacklist: false, - MaxCollateralShare: sdk.MustNewDecFromStr("1"), - MaxSupplyUtilization: sdk.MustNewDecFromStr("0.9"), - MinCollateralLiquidity: sdk.MustNewDecFromStr("0"), - MaxSupply: sdk.NewInt(100000000000), - } - atomIBCToken := types.Token{ - BaseDenom: "ibc/CDC4587874B85BEA4FCEC3CEA5A1195139799A1FEE711A07D972537E18FDA39D", - ReserveFactor: sdk.MustNewDecFromStr("0.25"), - CollateralWeight: sdk.MustNewDecFromStr("0.8"), - LiquidationThreshold: sdk.MustNewDecFromStr("0.8"), - BaseBorrowRate: sdk.MustNewDecFromStr("0.05"), - KinkBorrowRate: sdk.MustNewDecFromStr("0.3"), - MaxBorrowRate: sdk.MustNewDecFromStr("0.9"), - KinkUtilization: sdk.MustNewDecFromStr("0.75"), - LiquidationIncentive: sdk.MustNewDecFromStr("0.11"), - SymbolDenom: "ATOM", - Exponent: 6, - EnableMsgSupply: true, - EnableMsgBorrow: true, - Blacklist: false, - MaxCollateralShare: sdk.MustNewDecFromStr("1"), - MaxSupplyUtilization: sdk.MustNewDecFromStr("0.9"), - MinCollateralLiquidity: sdk.MustNewDecFromStr("0"), - MaxSupply: sdk.NewInt(100000000000), - } - uabc := types.Token{ - BaseDenom: "uabc", - ReserveFactor: sdk.MustNewDecFromStr("0"), - CollateralWeight: sdk.MustNewDecFromStr("0.1"), - LiquidationThreshold: sdk.MustNewDecFromStr("0.1"), - BaseBorrowRate: sdk.MustNewDecFromStr("0.02"), - KinkBorrowRate: sdk.MustNewDecFromStr("0.22"), - MaxBorrowRate: sdk.MustNewDecFromStr("1.52"), - KinkUtilization: sdk.MustNewDecFromStr("0.87"), - LiquidationIncentive: sdk.MustNewDecFromStr("0.1"), - SymbolDenom: "ABC", - Exponent: 6, - EnableMsgSupply: true, - EnableMsgBorrow: true, - Blacklist: false, - MaxCollateralShare: sdk.MustNewDecFromStr("1"), - MaxSupplyUtilization: sdk.MustNewDecFromStr("0.9"), - MinCollateralLiquidity: sdk.MustNewDecFromStr("0"), - MaxSupply: sdk.NewInt(100000000000), - } - - 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")) - } + + // Use default umee token for sim tests + s.Require().NoError(app.LeverageKeeper.SetTokenSettings(ctx, fixtures.Token("uumee", "UMEE"))) + app.OracleKeeper.SetExchangeRate(ctx, "UMEE", sdk.MustNewDecFromStr("100.0")) s.app = app s.ctx = ctx @@ -118,7 +57,7 @@ func (s *SimTestSuite) unmarshal(op *simtypes.OperationMsg, msg proto.Message) { func (s *SimTestSuite) getTestingAccounts(r *rand.Rand, n int, cb func(fundedAccount simtypes.Account)) []simtypes.Account { accounts := simtypes.RandomAccounts(r, n) - initAmt := sdk.NewInt(200000000) // 200 * 10^6 + initAmt := sdk.NewInt(200_000000) accCoins := sdk.NewCoins() tokens := s.app.LeverageKeeper.GetAllRegisteredTokens(s.ctx) @@ -146,29 +85,29 @@ func (s *SimTestSuite) TestWeightedOperations() { cdc := s.app.AppCodec() appParams := make(simtypes.AppParams) - weightesOps := simulation.WeightedOperations(appParams, cdc, s.app.AccountKeeper, s.app.BankKeeper, s.app.LeverageKeeper) + weightedOps := simulation.WeightedOperations(appParams, cdc, s.app.AccountKeeper, s.app.BankKeeper, s.app.LeverageKeeper) - // setup 3 accounts + // setup 1 account, which will test all 7 operations in order. the order is designed such that each + // transaction with prerequisites (e.g. must collateralize before borrow) has a chance to succeed. r := rand.New(rand.NewSource(1)) - accs := s.getTestingAccounts(r, 3, func(acc simtypes.Account) {}) + accs := s.getTestingAccounts(r, 1, func(acc simtypes.Account) {}) expected := []struct { weight int opMsgName string }{ + // the order of expected ops must match the order of weightedOps. {simulation.DefaultWeightMsgSupply, sdk.MsgTypeURL(new(types.MsgSupply))}, - {simulation.DefaultWeightMsgWithdraw, sdk.MsgTypeURL(new(types.MsgWithdraw))}, - {simulation.DefaultWeightMsgBorrow, sdk.MsgTypeURL(new(types.MsgBorrow))}, {simulation.DefaultWeightMsgCollateralize, sdk.MsgTypeURL(new(types.MsgCollateralize))}, - {simulation.DefaultWeightMsgDecollateralize, sdk.MsgTypeURL(new(types.MsgDecollateralize))}, - {simulation.DefaultWeightMsgRepay, sdk.MsgTypeURL(new(types.MsgRepay))}, + {simulation.DefaultWeightMsgBorrow, sdk.MsgTypeURL(new(types.MsgBorrow))}, {simulation.DefaultWeightMsgLiquidate, sdk.MsgTypeURL(new(types.MsgLiquidate))}, + {simulation.DefaultWeightMsgRepay, sdk.MsgTypeURL(new(types.MsgRepay))}, + {simulation.DefaultWeightMsgDecollateralize, sdk.MsgTypeURL(new(types.MsgDecollateralize))}, + {simulation.DefaultWeightMsgWithdraw, sdk.MsgTypeURL(new(types.MsgWithdraw))}, } - for i, w := range weightesOps { - operationMsg, _, _ := w.Op()(r, s.app.BaseApp, s.ctx, accs, "") - // the following checks are very much dependent from the ordering of the output given - // by WeightedOperations. if the ordering in WeightedOperations changes some tests - // will fail + for i, w := range weightedOps { + operationMsg, _, err := w.Op()(r, s.app.BaseApp, s.ctx, accs, "") + s.Require().NoError(err) s.Require().Equal(expected[i].weight, w.Weight(), "weight should be the same") s.Require().Equal(expected[i].opMsgName, operationMsg.Name, "operation Msg name should be the same") } @@ -187,13 +126,13 @@ func (s *SimTestSuite) TestSimulateMsgSupply() { var msg types.MsgSupply s.unmarshal(&operationMsg, &msg) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Supplier) - s.Require().Equal("185121068uumee", msg.Asset.String()) + s.Require().Equal("4896096uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgWithdraw() { r := rand.New(rand.NewSource(1)) - supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(100)) + supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(10_000000)) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { _, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -210,13 +149,13 @@ func (s *SimTestSuite) TestSimulateMsgWithdraw() { s.unmarshal(&operationMsg, &msg) s.Require().True(operationMsg.OK) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Supplier) - s.Require().Equal("73u/uumee", msg.Asset.String()) + s.Require().Equal("560969u/uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgBorrow() { r := rand.New(rand.NewSource(8)) - supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(1000)) + supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(10_000000)) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { uToken, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -235,13 +174,13 @@ func (s *SimTestSuite) TestSimulateMsgBorrow() { var msg types.MsgBorrow s.unmarshal(&operationMsg, &msg) s.Require().Equal("umee1qnclgkcxtuledc8xhle4lqly2q0z96uqkks60s", msg.Borrower) - s.Require().Equal("67uumee", msg.Asset.String()) + s.Require().Equal("675395uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgCollateralize() { r := rand.New(rand.NewSource(1)) - supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 100) + supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 10_000000) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { _, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -257,13 +196,13 @@ func (s *SimTestSuite) TestSimulateMsgCollateralize() { var msg types.MsgCollateralize s.unmarshal(&operationMsg, &msg) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Borrower) - s.Require().Equal("73u/uumee", msg.Asset.String()) + s.Require().Equal("560969u/uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgDecollateralize() { r := rand.New(rand.NewSource(1)) - supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 100) + supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 10_000000) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { uToken, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -280,14 +219,14 @@ func (s *SimTestSuite) TestSimulateMsgDecollateralize() { var msg types.MsgDecollateralize s.unmarshal(&operationMsg, &msg) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Borrower) - s.Require().Equal("73u/uumee", msg.Asset.String()) + s.Require().Equal("560969u/uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgRepay() { r := rand.New(rand.NewSource(1)) - supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 100) - borrowToken := sdk.NewInt64Coin(umeeapp.BondDenom, 20) + supplyToken := sdk.NewInt64Coin(umeeapp.BondDenom, 10_000000) + borrowToken := sdk.NewInt64Coin(umeeapp.BondDenom, 1_000000) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { uToken, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -305,15 +244,15 @@ func (s *SimTestSuite) TestSimulateMsgRepay() { var msg types.MsgRepay s.unmarshal(&operationMsg, &msg) s.Require().Equal("umee1ghekyjucln7y67ntx7cf27m9dpuxxemn8w6h33", msg.Borrower) - s.Require().Equal("9uumee", msg.Asset.String()) + s.Require().Equal("560969uumee", msg.Asset.String()) s.Require().Len(futureOperations, 0) } func (s *SimTestSuite) TestSimulateMsgLiquidate() { r := rand.New(rand.NewSource(1)) - supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(100)) - uToken := sdk.NewCoin("u/"+umeeapp.BondDenom, sdk.NewInt(100)) - borrowToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(10)) + supplyToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(10_000000)) + uToken := sdk.NewCoin("u/"+umeeapp.BondDenom, sdk.NewInt(10_000000)) + borrowToken := sdk.NewCoin(umeeapp.BondDenom, sdk.NewInt(1_000000)) accs := s.getTestingAccounts(r, 3, func(fundedAccount simtypes.Account) { _, err := s.app.LeverageKeeper.Supply(s.ctx, fundedAccount.Address, supplyToken) @@ -326,12 +265,10 @@ func (s *SimTestSuite) TestSimulateMsgLiquidate() { op := simulation.SimulateMsgLiquidate(s.app.AccountKeeper, s.app.BankKeeper, s.app.LeverageKeeper) operationMsg, futureOperations, err := op(r, s.app.BaseApp, s.ctx, accs, "") - s.Require().EqualError(err, - "failed to execute message; message index: 0: borrower not eligible for liquidation", - ) + s.Require().NoError(err) // While it is no longer simple to create an eligible liquidation target using exported keeper methods here, - // we can still verify some properties of the resulting operation. + // we can still verify some properties of the resulting no-op. s.Require().Empty(operationMsg.Msg) s.Require().False(operationMsg.OK) s.Require().Len(futureOperations, 0) diff --git a/x/leverage/types/errors.go b/x/leverage/types/errors.go index 861cd0cb3b..960b568d2c 100644 --- a/x/leverage/types/errors.go +++ b/x/leverage/types/errors.go @@ -7,38 +7,44 @@ import ( ) var ( - ErrInvalidAsset = sdkerrors.Register(ModuleName, 1100, "invalid asset") - ErrInsufficientBalance = sdkerrors.Register(ModuleName, 1101, "insufficient balance") - ErrUndercollaterized = sdkerrors.Register(ModuleName, 1102, "borrow positions are undercollaterized") - ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 1103, "lending pool insufficient") - ErrInvalidRepayment = sdkerrors.Register(ModuleName, 1104, "invalid repayment") - ErrInvalidAddress = sdkerrors.Register(ModuleName, 1105, "invalid address") - ErrNegativeTotalBorrowed = sdkerrors.Register(ModuleName, 1106, "total borrowed was negative") - ErrInvalidUtilization = sdkerrors.Register(ModuleName, 1107, "invalid token utilization") - ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 1108, "borrower not eligible for liquidation") - ErrBadValue = sdkerrors.Register(ModuleName, 1109, "bad USD value") - ErrLiquidatorBalanceZero = sdkerrors.Register(ModuleName, 1110, "liquidator base asset balance is zero") - ErrNegativeTimeElapsed = sdkerrors.Register(ModuleName, 1111, "negative time elapsed since last interest time") - ErrInvalidOraclePrice = sdkerrors.Register(ModuleName, 1112, "invalid oracle price") - ErrNegativeAPY = sdkerrors.Register(ModuleName, 1113, "negative APY") - ErrInvalidExchangeRate = sdkerrors.Register(ModuleName, 1114, "exchange rate less than one") - ErrInconsistentTotalBorrow = sdkerrors.Register(ModuleName, 1115, "total adjusted borrow inconsistency") - ErrInvalidInteresrScalar = sdkerrors.Register(ModuleName, 1116, "interest scalar less than one") - ErrEmptyAddress = sdkerrors.Register(ModuleName, 1117, "empty address") - ErrLiquidationRewardRatio = sdkerrors.Register(ModuleName, 1118, "requested liquidation reward not met") - ErrSupplyNotAllowed = sdkerrors.Register(ModuleName, 1119, "supplying of asset disabled") - ErrBorrowNotAllowed = sdkerrors.Register(ModuleName, 1120, "borrowing of asset disabled") - ErrBlacklisted = sdkerrors.Register(ModuleName, 1121, "base denom blacklisted") - ErrCollateralWeightZero = sdkerrors.Register(ModuleName, 1122, "token collateral weight is zero") - ErrLiquidationInvalid = sdkerrors.Register(ModuleName, 1123, "liquidation invalid") - ErrMaxSupplyUtilization = sdkerrors.Register(ModuleName, 1124, "market would exceed MaxSupplyUtilization") - ErrMinCollateralLiquidity = sdkerrors.Register(ModuleName, 1125, "market would fall below MinCollateralLiquidity") - ErrMaxCollateralShare = sdkerrors.Register( - ModuleName, - 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") - ErrExcessiveTimeElapsed = sdkerrors.Register(ModuleName, 1130, "excessive time elapsed since last interest time") + // 1XX = General Validation + ErrEmptyAddress = sdkerrors.Register(ModuleName, 100, "empty address") + ErrNilAsset = sdkerrors.Register(ModuleName, 101, "nil asset") + + // 2XX = Token Registry + ErrNotRegisteredToken = sdkerrors.Register(ModuleName, 200, "not a registered Token") + ErrUToken = sdkerrors.Register(ModuleName, 201, "denom should not be a uToken") + ErrNotUToken = sdkerrors.Register(ModuleName, 202, "denom should be a uToken") + ErrSupplyNotAllowed = sdkerrors.Register(ModuleName, 203, "supplying of Token disabled") + ErrBorrowNotAllowed = sdkerrors.Register(ModuleName, 204, "borrowing of Token disabled") + ErrBlacklisted = sdkerrors.Register(ModuleName, 205, "blacklisted Token") + ErrCollateralWeightZero = sdkerrors.Register(ModuleName, 206, "collateral weight of Token is zero") + + // 3XX = User Positions + ErrInsufficientBalance = sdkerrors.Register(ModuleName, 300, "insufficient balance") + ErrInsufficientCollateral = sdkerrors.Register(ModuleName, 301, "insufficient collateral") + ErrDenomNotBorrowed = sdkerrors.Register(ModuleName, 302, "denom not borrowed") + ErrLiquidationRepayZero = sdkerrors.Register(ModuleName, 303, "liquidation would repay zero tokens") + + // 4XX = Price Sensitive + ErrBadValue = sdkerrors.Register(ModuleName, 400, "bad USD value") + ErrInvalidOraclePrice = sdkerrors.Register(ModuleName, 401, "invalid oracle price") + ErrUndercollaterized = sdkerrors.Register(ModuleName, 402, "borrow positions are undercollaterized") + ErrLiquidationIneligible = sdkerrors.Register(ModuleName, 403, "borrower not eligible for liquidation") + + // 5XX = Market Conditions + ErrLendingPoolInsufficient = sdkerrors.Register(ModuleName, 500, "lending pool insufficient") + ErrMaxSupplyUtilization = sdkerrors.Register(ModuleName, 501, "market would exceed MaxSupplyUtilization") + ErrMinCollateralLiquidity = sdkerrors.Register(ModuleName, 502, "market would fall below MinCollateralLiquidity") + ErrMaxCollateralShare = sdkerrors.Register(ModuleName, 503, "market would exceed MaxCollateralShare") + + // 6XX = Internal Failsafes + ErrInvalidUtilization = sdkerrors.Register(ModuleName, 600, "invalid token utilization") + ErrNegativeTotalBorrowed = sdkerrors.Register(ModuleName, 601, "total borrowed was negative") + ErrNegativeAPY = sdkerrors.Register(ModuleName, 602, "negative APY") + ErrNegativeTimeElapsed = sdkerrors.Register(ModuleName, 603, "negative time elapsed since last interest time") + ErrInvalidExchangeRate = sdkerrors.Register(ModuleName, 604, "exchange rate less than one") + ErrInconsistentTotalBorrow = sdkerrors.Register(ModuleName, 605, "total adjusted borrow inconsistency") + ErrInvalidInteresrScalar = sdkerrors.Register(ModuleName, 606, "interest scalar less than one") + ErrExcessiveTimeElapsed = sdkerrors.Register(ModuleName, 607, "excessive time elapsed since last interest time") ) diff --git a/x/leverage/types/token.go b/x/leverage/types/token.go index 6ac3ccc64a..e4ebb97c1f 100644 --- a/x/leverage/types/token.go +++ b/x/leverage/types/token.go @@ -50,7 +50,7 @@ func (t Token) Validate() error { } if HasUTokenPrefix(t.BaseDenom) { // prevent base asset denoms that start with "u/" - return sdkerrors.Wrap(ErrInvalidAsset, t.BaseDenom) + return ErrUToken.Wrap(t.BaseDenom) } if err := sdk.ValidateDenom(t.SymbolDenom); err != nil { @@ -58,7 +58,7 @@ func (t Token) Validate() error { } if HasUTokenPrefix(t.SymbolDenom) { // prevent symbol denoms that start with "u/" - return sdkerrors.Wrap(ErrInvalidAsset, t.SymbolDenom) + return ErrUToken.Wrap(t.SymbolDenom) } // Reserve factor and collateral weight range between 0 and 1, inclusive. diff --git a/x/leverage/types/tx.go b/x/leverage/types/tx.go index 30fdb7a14a..93df38a28f 100644 --- a/x/leverage/types/tx.go +++ b/x/leverage/types/tx.go @@ -2,7 +2,6 @@ package types import ( sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/umee-network/umee/v3/util/checkers" ) @@ -66,7 +65,7 @@ func (msg MsgCollateralize) Route() string { return sdk.MsgTypeURL(&msg) } func (msg MsgCollateralize) Type() string { return sdk.MsgTypeURL(&msg) } func (msg *MsgCollateralize) ValidateBasic() error { - return validateSenderAndAsset(msg.Borrower, nil) + return validateSenderAndAsset(msg.Borrower, &msg.Asset) } func (msg *MsgCollateralize) GetSigners() []sdk.AccAddress { @@ -90,7 +89,7 @@ func (msg MsgDecollateralize) Route() string { return sdk.MsgTypeURL(&msg) } func (msg MsgDecollateralize) Type() string { return sdk.MsgTypeURL(&msg) } func (msg *MsgDecollateralize) ValidateBasic() error { - return validateSenderAndAsset(msg.Borrower, nil) + return validateSenderAndAsset(msg.Borrower, &msg.Asset) } func (msg *MsgDecollateralize) GetSigners() []sdk.AccAddress { @@ -189,8 +188,11 @@ func validateSenderAndAsset(sender string, asset *sdk.Coin) error { if err != nil { return err } - if asset != nil && !asset.IsValid() { - return sdkerrors.Wrap(ErrInvalidAsset, asset.String()) + if asset == nil { + return ErrNilAsset + } + if err := asset.Validate(); err != nil { + return err } return nil }