0g-chain/x/hard/keeper/borrow_test.go
sesheffield 916ec6d30c
test(insolvency): add tests for Kava lend insolvency check (#1981)
* add additional tests that attempt to borrow funds from the insolvent market(should fail), and attempt to borrow funds from the not insolvent market (it will fail, but shouldn't). The not insolvent market should continue to processs borrows

* remove unused code

* make tests less specific for string contains

* change the structure for new and old tests and add more verbosity for other tests

* remove print

* remove unneeded code

* add paren

* remove duplicate test case with invalid test name, and update to use error contains in places where it was validating if true for strings contains

---------

Co-authored-by: Sam Sheffield <sam.sheffield@kavalabs.io>
2024-07-29 20:51:08 -04:00

809 lines
34 KiB
Go

package keeper_test
import (
"time"
sdkmath "cosmossdk.io/math"
"github.com/cometbft/cometbft/crypto"
tmproto "github.com/cometbft/cometbft/proto/tendermint/types"
tmtime "github.com/cometbft/cometbft/types/time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/hard"
"github.com/kava-labs/kava/x/hard/types"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
)
const (
USDX_CF = 1000000
KAVA_CF = 1000000
BTCB_CF = 100000000
BNB_CF = 100000000
BUSD_CF = 100000000
)
func (suite *KeeperTestSuite) TestBorrow() {
type setupArgs struct {
usdxBorrowLimit sdk.Dec
priceKAVA sdk.Dec
loanToValueKAVA sdk.Dec
priceBTCB sdk.Dec
loanToValueBTCB sdk.Dec
priceBNB sdk.Dec
loanToValueBNB sdk.Dec
borrower sdk.AccAddress
depositCoins []sdk.Coin
initialBorrowCoins sdk.Coins
}
type borrowArgs struct {
borrowCoins sdk.Coins
wantErr string
}
type expected struct {
expectedAccountBalance sdk.Coins
expectedModAccountBalance sdk.Coins
expectPass bool
contains string
}
type borrowTest struct {
name string
setup setupArgs
borrows []borrowArgs
expected []expected
}
testCases := []borrowTest{
{
name: "valid",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("5.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.6"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF))},
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1080*KAVA_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(200*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
expectPass: true,
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF))),
wantErr: "",
},
},
},
{
name: "invalid: loan-to-value limited",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("5.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.6"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF))}, // 20 KAVA x $5.00 price = $100
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "exceeds the allowable amount as determined by the collateralization ratio",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(61*USDX_CF))), // 61 USDX x $1 price = $61
wantErr: "",
},
},
},
{
name: "valid: multiple deposits",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.80"),
priceBTCB: sdk.MustNewDecFromStr("10000.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.10"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(0.1*BTCB_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(99.9*BTCB_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(180*USDX_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1050*KAVA_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(20*USDX_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(0.1*BTCB_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
expectPass: true,
contains: "exceeds the allowable amount as determined by the collateralization ratio",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(180*USDX_CF))),
wantErr: "",
},
},
},
{
name: "invalid: multiple deposits",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.80"),
priceBTCB: sdk.MustNewDecFromStr("10000.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.10"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(0.1*BTCB_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "exceeds the allowable amount as determined by the collateralization ratio",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(181*USDX_CF))),
wantErr: "",
},
},
},
{
name: "valid: multiple previous borrows",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("5.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.8"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(30*BNB_CF)), sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))), // (50 KAVA x $2.00 price = $100) + (30 BNB x $5.00 price = $150) = $250
initialBorrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(99*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(70*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1050*KAVA_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(30*BUSD_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF))),
expectPass: true,
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(1*USDX_CF))),
wantErr: "",
},
},
},
{
name: "invalid: over loan-to-value with multiple previous borrows",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("5.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.8"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(30*BNB_CF)), sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))), // (50 KAVA x $2.00 price = $100) + (30 BNB x $5.00 price = $150) = $250
initialBorrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "exceeds the allowable amount as determined by the collateralization ratio",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(1*USDX_CF))),
wantErr: "",
},
},
},
{
name: "invalid: no price for asset",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("5.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.6"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1080*KAVA_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(200*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
expectPass: false,
contains: "no price found for market",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("xyz", sdkmath.NewInt(1))),
wantErr: "",
},
},
},
{
name: "invalid: borrow exceed module account balance",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "exceeds borrowable module account balance",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("busd", sdkmath.NewInt(101*BUSD_CF))),
wantErr: "",
},
},
},
{
name: "invalid: over global asset borrow limit",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("20000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "fails global asset borrow limit validation",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(25*USDX_CF))),
wantErr: "",
},
},
},
{
name: "invalid: borrowing an individual coin type results in a borrow that's under the minimum USD borrow limit",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("20000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "below the minimum borrow limit",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(5*USDX_CF))),
wantErr: "",
},
},
},
{
name: "invalid: borrowing multiple coins results in a borrow that's under the minimum USD borrow limit",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("20000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "below the minimum borrow limit",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(5*USDX_CF)), sdk.NewCoin("ukava", sdkmath.NewInt(2*USDX_CF))),
wantErr: "",
},
},
},
{
name: "valid borrow multiple blocks",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("5.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.6"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("0.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.01"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: []sdk.Coin{sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF))},
initialBorrowCoins: sdk.NewCoins(),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1080*KAVA_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(200*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
expectPass: true,
},
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(40*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1060*KAVA_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(200*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
expectPass: true,
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF))),
wantErr: "",
},
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(20*KAVA_CF))),
wantErr: "",
},
},
},
{
name: "valid borrow followed by protocol reserves exceed available cash for busd when borrowing from ukava(incorrect)",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("5.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("5.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.8"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(30*BNB_CF)), sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(99*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(70*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1050*KAVA_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(30*BUSD_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF))),
expectPass: true,
},
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "insolvency - protocol reserves exceed available cash",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(1*USDX_CF))),
wantErr: "",
},
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1*KAVA_CF))),
wantErr: "",
},
},
},
{
name: "valid borrow followed by protocol reserves exceed available cash for busd when borrowing from busd",
setup: setupArgs{
usdxBorrowLimit: sdk.MustNewDecFromStr("100000000000"),
priceKAVA: sdk.MustNewDecFromStr("2.00"),
loanToValueKAVA: sdk.MustNewDecFromStr("0.8"),
priceBTCB: sdk.MustNewDecFromStr("0.00"),
loanToValueBTCB: sdk.MustNewDecFromStr("0.01"),
priceBNB: sdk.MustNewDecFromStr("5.00"),
loanToValueBNB: sdk.MustNewDecFromStr("0.8"),
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
depositCoins: sdk.NewCoins(sdk.NewCoin("bnb", sdkmath.NewInt(30*BNB_CF)), sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF))),
initialBorrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(99*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
},
expected: []expected{
{
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(50*KAVA_CF)), sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF)), sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(70*BNB_CF)), sdk.NewCoin("xyz", sdkmath.NewInt(1))),
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(1050*KAVA_CF)), sdk.NewCoin("bnb", sdkmath.NewInt(30*BUSD_CF)), sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF))),
expectPass: true,
},
{
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
expectPass: false,
contains: "insolvency - protocol reserves exceed available cash",
},
},
borrows: []borrowArgs{
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(1*USDX_CF))),
wantErr: "",
},
{
borrowCoins: sdk.NewCoins(sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF))),
wantErr: "",
},
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
// Auth module genesis state
authGS := app.NewFundedGenStateWithCoins(
tApp.AppCodec(),
[]sdk.Coins{
sdk.NewCoins(
sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF)),
sdk.NewCoin("btcb", sdkmath.NewInt(100*BTCB_CF)),
sdk.NewCoin("bnb", sdkmath.NewInt(100*BNB_CF)),
sdk.NewCoin("xyz", sdkmath.NewInt(1)),
),
},
[]sdk.AccAddress{tc.setup.borrower},
)
// hard module genesis state
hardGS := types.NewGenesisState(types.NewParams(
types.MoneyMarkets{
types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, tc.setup.usdxBorrowLimit, sdk.MustNewDecFromStr("1")), "usdx:usd", sdkmath.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("busd", types.NewBorrowLimit(false, sdk.NewDec(100000000*BUSD_CF), sdk.MustNewDecFromStr("1")), "busd:usd", sdkmath.NewInt(BUSD_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), tc.setup.loanToValueKAVA), "kava:usd", sdkmath.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.99"), sdk.ZeroDec()),
types.NewMoneyMarket("btcb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BTCB_CF), tc.setup.loanToValueBTCB), "btcb:usd", sdkmath.NewInt(BTCB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), tc.setup.loanToValueBNB), "bnb:usd", sdkmath.NewInt(BNB_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("xyz", types.NewBorrowLimit(false, sdk.NewDec(1), tc.setup.loanToValueBNB), "xyz:usd", sdkmath.NewInt(1), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)
// Pricefeed module genesis state
pricefeedGS := pricefeedtypes.GenesisState{
Params: pricefeedtypes.Params{
Markets: []pricefeedtypes.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "busd:usd", BaseAsset: "busd", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "btcb:usd", BaseAsset: "btcb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "xyz:usd", BaseAsset: "xyz", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeedtypes.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "busd:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: tc.setup.priceKAVA,
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "btcb:usd",
OracleAddress: sdk.AccAddress{},
Price: tc.setup.priceBTCB,
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "bnb:usd",
OracleAddress: sdk.AccAddress{},
Price: tc.setup.priceBNB,
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
// Initialize test application
tApp.InitializeFromGenesisStates(authGS,
app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)},
app.GenesisState{types.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)})
// Mint coins to hard module account
bankKeeper := tApp.GetBankKeeper()
hardMaccCoins := sdk.NewCoins(
sdk.NewCoin("ukava", sdkmath.NewInt(1000*KAVA_CF)),
sdk.NewCoin("usdx", sdkmath.NewInt(200*USDX_CF)),
sdk.NewCoin("busd", sdkmath.NewInt(100*BUSD_CF)),
)
err := bankKeeper.MintCoins(ctx, types.ModuleAccountName, hardMaccCoins)
suite.Require().NoError(err)
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
// Run BeginBlocker once to transition MoneyMarkets
hard.BeginBlocker(suite.ctx, suite.keeper)
suite.Require().NoError(suite.keeper.Deposit(suite.ctx, tc.setup.borrower, tc.setup.depositCoins))
// Execute user's previous borrows
if err = suite.keeper.Borrow(suite.ctx, tc.setup.borrower, tc.setup.initialBorrowCoins); tc.setup.initialBorrowCoins.IsZero() {
suite.Require().ErrorContains(err, "cannot borrow zero coins")
} else {
suite.Require().NoError(err)
}
for i, borrow := range tc.borrows {
// Now that our state is properly set up, execute the last borrow
err = suite.keeper.Borrow(suite.ctx, tc.setup.borrower, borrow.borrowCoins)
if tc.expected[i].expectPass {
suite.Require().NoError(err)
// Check borrower balance
acc := suite.getAccount(tc.setup.borrower)
suite.Require().Equal(tc.expected[i].expectedAccountBalance, suite.getAccountCoins(acc))
// Check module account balance
mAcc := suite.getModuleAccount(types.ModuleAccountName)
suite.Require().Equal(tc.expected[i].expectedModAccountBalance, suite.getAccountCoins(mAcc))
// Check that borrow struct is in store
_, f := suite.keeper.GetBorrow(suite.ctx, tc.setup.borrower)
suite.Require().True(f)
} else {
suite.Require().Error(err)
suite.Require().ErrorContains(err, tc.expected[i].contains)
}
blockDuration := time.Second * 3600 * 30 // long blocks to accumulate larger interest
runAtTime := suite.ctx.BlockTime().Add(blockDuration)
suite.ctx = suite.ctx.WithBlockTime(runAtTime)
// Run BeginBlocker once to transition MoneyMarkets
hard.BeginBlocker(suite.ctx, suite.keeper)
}
})
}
}
func (suite *KeeperTestSuite) TestValidateBorrow() {
blockDuration := time.Second * 3600 * 24 // long blocks to accumulate larger interest
_, addrs := app.GeneratePrivKeyAddressPairs(5)
borrower := addrs[0]
initialBorrowerBalance := sdk.NewCoins(
sdk.NewCoin("ukava", sdkmath.NewInt(1000*KAVA_CF)),
sdk.NewCoin("usdx", sdkmath.NewInt(1000*KAVA_CF)),
)
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("1.0"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10"))
// Initialize test app and set context
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
// Auth module genesis state
authGS := app.NewFundedGenStateWithSameCoins(
tApp.AppCodec(),
initialBorrowerBalance,
[]sdk.AccAddress{borrower},
)
// Hard module genesis state
hardGS := types.NewGenesisState(
types.NewParams(
types.MoneyMarkets{
types.NewMoneyMarket("usdx",
types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("1")), // Borrow Limit
"usdx:usd", // Market ID
sdkmath.NewInt(USDX_CF), // Conversion Factor
model, // Interest Rate Model
sdk.MustNewDecFromStr("1.0"), // Reserve Factor (high)
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
types.NewMoneyMarket("ukava",
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
"kava:usd", // Market ID
sdkmath.NewInt(KAVA_CF), // Conversion Factor
model, // Interest Rate Model
sdk.MustNewDecFromStr("1.0"), // Reserve Factor (high)
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
},
sdk.NewDec(10),
),
types.DefaultAccumulationTimes,
types.DefaultDeposits,
types.DefaultBorrows,
types.DefaultTotalSupplied,
types.DefaultTotalBorrowed,
types.DefaultTotalReserves,
)
// Pricefeed module genesis state
pricefeedGS := pricefeedtypes.GenesisState{
Params: pricefeedtypes.Params{
Markets: []pricefeedtypes.Market{
{MarketID: "usdx:usd", BaseAsset: "usdx", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
},
},
PostedPrices: []pricefeedtypes.PostedPrice{
{
MarketID: "usdx:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("1.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
{
MarketID: "kava:usd",
OracleAddress: sdk.AccAddress{},
Price: sdk.MustNewDecFromStr("2.00"),
Expiry: time.Now().Add(1 * time.Hour),
},
},
}
// Initialize test application
tApp.InitializeFromGenesisStates(
authGS,
app.GenesisState{pricefeedtypes.ModuleName: tApp.AppCodec().MustMarshalJSON(&pricefeedGS)},
app.GenesisState{types.ModuleName: tApp.AppCodec().MustMarshalJSON(&hardGS)},
)
keeper := tApp.GetHardKeeper()
suite.app = tApp
suite.ctx = ctx
suite.keeper = keeper
var err error
// Run BeginBlocker once to transition MoneyMarkets
hard.BeginBlocker(suite.ctx, suite.keeper)
// Setup borrower with some collateral to borrow against, and some reserve in the protocol.
depositCoins := sdk.NewCoins(
sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF)),
sdk.NewCoin("usdx", sdkmath.NewInt(100*USDX_CF)),
)
err = suite.keeper.Deposit(suite.ctx, borrower, depositCoins)
suite.Require().NoError(err)
initialBorrowCoins := sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(70*KAVA_CF)))
err = suite.keeper.Borrow(suite.ctx, borrower, initialBorrowCoins)
suite.Require().NoError(err)
runAtTime := suite.ctx.BlockTime().Add(blockDuration)
suite.ctx = suite.ctx.WithBlockTime(runAtTime)
hard.BeginBlocker(suite.ctx, suite.keeper)
repayCoins := sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(100*KAVA_CF))) // repay everything including accumulated interest
err = suite.keeper.Repay(suite.ctx, borrower, borrower, repayCoins)
suite.Require().NoError(err)
// Get the total borrowable amount from the protocol, taking into account the reserves.
modAccBalance := suite.getAccountCoins(suite.getModuleAccountAtCtx(types.ModuleAccountName, suite.ctx))
reserves, found := suite.keeper.GetTotalReserves(suite.ctx)
suite.Require().True(found)
availableToBorrow := modAccBalance.Sub(reserves...)
// Test borrowing one over the available amount (try to borrow from the reserves)
err = suite.keeper.Borrow(
suite.ctx,
borrower,
sdk.NewCoins(sdk.NewCoin("ukava", availableToBorrow.AmountOf("ukava").Add(sdk.OneInt()))),
)
suite.Require().Error(err)
// Test borrowing exactly the limit
err = suite.keeper.Borrow(
suite.ctx,
borrower,
sdk.NewCoins(sdk.NewCoin("ukava", availableToBorrow.AmountOf("ukava"))),
)
suite.Require().NoError(err)
// now that it's all that you can borrow, we shouldn't be able to borrow anything
err = suite.keeper.Borrow(
suite.ctx,
borrower,
sdk.NewCoins(sdk.NewCoin("ukava", sdkmath.NewInt(2*USDX_CF))),
)
suite.Require().Error(err)
suite.Require().ErrorContains(err, "available to borrow: exceeds module account balance")
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(blockDuration))
hard.BeginBlocker(suite.ctx, suite.keeper)
// Should error since ukava has become insolvent at this point
err = suite.keeper.Borrow(
suite.ctx,
borrower,
sdk.NewCoins(sdk.NewCoin("ukava", availableToBorrow.AmountOf("ukava"))),
)
suite.Require().Error(err)
suite.Require().ErrorContains(err, "protocol reserves exceed available cash")
suite.ctx = suite.ctx.WithBlockTime(suite.ctx.BlockTime().Add(blockDuration))
hard.BeginBlocker(suite.ctx, suite.keeper)
// TODO: this is wrong, since usdk is not insolvent, ukava is.
err = suite.keeper.Borrow(
suite.ctx,
borrower,
sdk.NewCoins(sdk.NewCoin("usdx", sdkmath.NewInt(25*USDX_CF))),
)
suite.Require().Error(err)
suite.Require().ErrorContains(err, "protocol reserves exceed available cash")
}