Hard Audit: add minimum borrow USD value (#822)

* add module param MinimumBorrowUSDValue

* borrow/repay min limit restrictions

* add borrow/repay test cases

* update tests with new module params

* update timelock test with param

* update withdraw LTV test

* remove unused GetCurrentBorrowUSDValue method

* commit to prompt CircleCI run
This commit is contained in:
Denali Marsh 2021-02-12 16:28:05 +01:00 committed by GitHub
parent cd7a227030
commit fe2a131b31
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 225 additions and 30 deletions

View File

@ -208,6 +208,12 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
}
}
// Borrow's updated total USD value must be greater than the minimum global USD borrow limit
totalBorrowUSDValue := proprosedBorrowUSDValue.Add(existingBorrowUSDValue)
if totalBorrowUSDValue.LT(k.GetMinimumBorrowUSDValue(ctx)) {
return sdkerrors.Wrapf(types.ErrBelowMinimumBorrowValue, "the proposed borrow's USD value $%s is below the minimum borrow limit $%s", totalBorrowUSDValue, k.GetMinimumBorrowUSDValue(ctx))
}
// Validate that the proposed borrow's USD value is within user's borrowable limit
if proprosedBorrowUSDValue.GT(totalBorrowableAmount.Sub(existingBorrowUSDValue)) {
return sdkerrors.Wrapf(types.ErrInsufficientLoanToValue, "requested borrow %s exceeds the allowable amount as determined by the collateralization ratio", amount)

View File

@ -245,6 +245,50 @@ func (suite *KeeperTestSuite) TestBorrow() {
contains: "fails global asset borrow limit validation",
},
},
{
"invalid: borrowing an individual coin type results in a borrow that's under the minimum USD borrow limit",
args{
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", sdk.NewInt(50*KAVA_CF))),
previousBorrowCoins: sdk.NewCoins(),
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(5*USDX_CF))),
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
},
errArgs{
expectPass: false,
contains: "below the minimum borrow limit",
},
},
{
"invalid: borrowing multiple coins results in a borrow that's under the minimum USD borrow limit",
args{
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", sdk.NewInt(50*KAVA_CF))),
previousBorrowCoins: sdk.NewCoins(),
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(5*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(2*USDX_CF))),
expectedAccountBalance: sdk.NewCoins(),
expectedModAccountBalance: sdk.NewCoins(),
},
errArgs{
expectPass: false,
contains: "below the minimum borrow limit",
},
},
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
@ -269,6 +313,7 @@ func (suite *KeeperTestSuite) TestBorrow() {
types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), tc.args.loanToValueBNB), "bnb:usd", sdk.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.args.loanToValueBNB), "xyz:usd", sdk.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,
)

View File

@ -111,6 +111,7 @@ func (suite *KeeperTestSuite) TestDeposit() {
types.NewMoneyMarket("bnb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(1000000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
types.NewMoneyMarket("btcb", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "btcb:usd", sdk.NewInt(1000000), 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,
)

View File

@ -796,6 +796,7 @@ func (suite *KeeperTestSuite) TestBorrowInterest() {
tc.args.reserveFactor, // Reserve Factor
sdk.ZeroDec()), // Keeper Reward Percentage
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)
@ -1209,6 +1210,7 @@ func (suite *KeeperTestSuite) TestSupplyInterest() {
tc.args.reserveFactor, // Reserve Factor
sdk.ZeroDec()), // Keeper Reward Percentage
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)

View File

@ -511,6 +511,7 @@ func (suite *KeeperTestSuite) TestKeeperLiquidation() {
reserveFactor, // Reserve Factor
tc.args.keeperRewardPercent), // Keeper Reward Percent
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)

View File

@ -28,3 +28,9 @@ func (k Keeper) GetMoneyMarketParam(ctx sdk.Context, denom string) (types.MoneyM
}
return types.MoneyMarket{}, false
}
// GetMinimumBorrowUSDValue returns the minimum borrow USD value
func (k Keeper) GetMinimumBorrowUSDValue(ctx sdk.Context) sdk.Dec {
params := k.GetParams(ctx)
return params.MinimumBorrowUSDValue
}

View File

@ -21,7 +21,7 @@ func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.C
k.SyncBorrowInterest(ctx, owner)
// Validate that sender holds coins for repayment
err := k.ValidateRepay(ctx, sender, coins)
err := k.ValidateRepay(ctx, sender, owner, coins)
if err != nil {
return err
}
@ -78,14 +78,80 @@ func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.C
}
// ValidateRepay validates a requested loan repay
func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error {
func (k Keeper) ValidateRepay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.Coins) error {
moneyMarketCache := map[string]types.MoneyMarket{}
assetPriceCache := map[string]sdk.Dec{}
// Get the total USD value of user's existing borrows
existingBorrowUSDValue := sdk.ZeroDec()
existingBorrow, found := k.GetBorrow(ctx, owner)
if found {
for _, borrowedCoin := range existingBorrow.Amount {
moneyMarket, ok := moneyMarketCache[borrowedCoin.Denom]
if !ok { // Fetch money market and store in local cache
newMoneyMarket, found := k.GetMoneyMarketParam(ctx, borrowedCoin.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", borrowedCoin.Denom)
}
moneyMarketCache[borrowedCoin.Denom] = newMoneyMarket
moneyMarket = newMoneyMarket
}
assetPrice, ok := assetPriceCache[borrowedCoin.Denom]
if !ok { // Fetch current asset price and store in local cache
assetPriceInfo, err := k.pricefeedKeeper.GetCurrentPrice(ctx, moneyMarket.SpotMarketID)
if err != nil {
return sdkerrors.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID)
}
assetPriceCache[borrowedCoin.Denom] = assetPriceInfo.Price
assetPrice = assetPriceInfo.Price
}
// Calculate this borrow coin's USD value and add it to the total previous borrowed USD value
coinUSDValue := sdk.NewDecFromInt(borrowedCoin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPrice)
existingBorrowUSDValue = existingBorrowUSDValue.Add(coinUSDValue)
}
}
senderAcc := k.accountKeeper.GetAccount(ctx, sender)
senderCoins := senderAcc.SpendableCoins(ctx.BlockTime())
for _, coin := range coins {
if senderCoins.AmountOf(coin.Denom).LT(coin.Amount) {
return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(coin.Denom), coin.Denom)
repayTotalUSDValue := sdk.ZeroDec()
for _, repayCoin := range coins {
// Check that sender holds enough tokens to make the proposed payment
if senderCoins.AmountOf(repayCoin.Denom).LT(repayCoin.Amount) {
return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(repayCoin.Denom), repayCoin.Denom)
}
moneyMarket, ok := moneyMarketCache[repayCoin.Denom]
if !ok { // Fetch money market and store in local cache
newMoneyMarket, found := k.GetMoneyMarketParam(ctx, repayCoin.Denom)
if !found {
return sdkerrors.Wrapf(types.ErrMarketNotFound, "no market found for denom %s", repayCoin.Denom)
}
moneyMarketCache[repayCoin.Denom] = newMoneyMarket
moneyMarket = newMoneyMarket
}
// Calculate this coin's USD value and add it to the repay's total USD value
assetPrice, ok := assetPriceCache[repayCoin.Denom]
if !ok { // Fetch current asset price and store in local cache
assetPriceInfo, err := k.pricefeedKeeper.GetCurrentPrice(ctx, moneyMarket.SpotMarketID)
if err != nil {
return sdkerrors.Wrapf(types.ErrPriceNotFound, "no price found for market %s", moneyMarket.SpotMarketID)
}
assetPriceCache[repayCoin.Denom] = assetPriceInfo.Price
assetPrice = assetPriceInfo.Price
}
coinUSDValue := sdk.NewDecFromInt(repayCoin.Amount).Quo(sdk.NewDecFromInt(moneyMarket.ConversionFactor)).Mul(assetPrice)
repayTotalUSDValue = repayTotalUSDValue.Add(coinUSDValue)
}
// If the proposed repayment would results in a borrowed USD value below the minimum borrow USD value, reject it.
// User can overpay their loan to close it out, but underpaying by such a margin that the USD value is in an
// invalid range is not allowed
proposedBorrowNewUSDValue := existingBorrowUSDValue.Sub(repayTotalUSDValue)
if proposedBorrowNewUSDValue.IsPositive() && proposedBorrowNewUSDValue.LT(k.GetMinimumBorrowUSDValue(ctx)) {
return sdkerrors.Wrapf(types.ErrBelowMinimumBorrowValue, "the proposed borrow's USD value $%s is below the minimum borrow limit $%s", proposedBorrowNewUSDValue, k.GetMinimumBorrowUSDValue(ctx))
}
return nil

View File

@ -122,6 +122,38 @@ func (suite *KeeperTestSuite) TestRepay() {
contains: "account can only repay up to 50000000ukava",
},
},
{
"invalid: repaying a single coin type results in borrow position below the minimum USD value",
args{
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))),
depositCoins: []sdk.Coin{sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))},
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(50*USDX_CF))),
repayCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(45*USDX_CF))),
},
errArgs{
expectPass: false,
expectDelete: false,
contains: "proposed borrow's USD value $5.000000000000000000 is below the minimum borrow limit",
},
},
{
"invalid: repaying multiple coin types results in borrow position below the minimum USD value",
args{
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))),
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(1000*USDX_CF))),
depositCoins: []sdk.Coin{sdk.NewCoin("usdx", sdk.NewInt(100*USDX_CF))},
borrowCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(50*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // (50*$1)+(10*$2) = $70
repayCoins: sdk.NewCoins(sdk.NewCoin("usdx", sdk.NewInt(45*USDX_CF)), sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), // (45*$1)+(8*2) = $61
},
errArgs{
expectPass: false,
expectDelete: false,
contains: "proposed borrow's USD value $9.000000000000000000 is below the minimum borrow limit",
},
},
}
for _, tc := range testCases {
@ -153,6 +185,7 @@ func (suite *KeeperTestSuite) TestRepay() {
sdk.MustNewDecFromStr("0.05"), // Reserve Factor
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)

View File

@ -285,6 +285,7 @@ func (suite *KeeperTestSuite) TestSendTimeLockedCoinsToAccount() {
types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), 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(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), 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,
)

View File

@ -129,6 +129,7 @@ func (suite *KeeperTestSuite) TestWithdraw() {
types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "kava:usd", sdk.NewInt(1000000), 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(1000000000000000), loanToValue), "bnb:usd", sdk.NewInt(100000000), 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,
)
@ -212,6 +213,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
initialBorrowerCoins sdk.Coins
depositCoins sdk.Coins
borrowCoins sdk.Coins
repayCoins sdk.Coins
futureTime int64
}
@ -239,8 +241,9 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
borrower: borrower,
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("usdx", sdk.NewInt(100*KAVA_CF))),
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))), // 10 * 2 = $20
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))), // 8 * 2 = $16
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))), // 100 * 2 = $200
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))), // 80 * 2 = $160
repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(60*KAVA_CF))), // 60 * 2 = $120
futureTime: oneMonthInSeconds,
},
errArgs{
@ -280,6 +283,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
reserveFactor, // Reserve Factor
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
},
sdk.NewDec(10),
), types.DefaultAccumulationTimes, types.DefaultDeposits, types.DefaultBorrows,
types.DefaultTotalSupplied, types.DefaultTotalBorrowed, types.DefaultTotalReserves,
)
@ -353,8 +357,8 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Repay the initial principal
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.borrowCoins)
// Repay the initial principal. Over pay the position so the borrow is closed.
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.repayCoins)
suite.Require().NoError(err)
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
@ -362,8 +366,8 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
suite.Require().Error(err)
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
// Withdrawing half the coins should succeed
withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(2))))
// Withdrawing 10% of the coins should succeed
withdrawCoins := sdk.NewCoins(sdk.NewCoin("ukava", tc.args.depositCoins[0].Amount.Quo(sdk.NewInt(10))))
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
suite.Require().NoError(err)
})

View File

@ -63,4 +63,6 @@ var (
ErrInvalidRepaymentDenom = sdkerrors.Register(ModuleName, 28, "no coins of this type borrowed")
// ErrInvalidIndexFactorDenom error for when index factor denom cannot be found
ErrInvalidIndexFactorDenom = sdkerrors.Register(ModuleName, 29, "no index factor found for denom")
// ErrBelowMinimumBorrowValue error for when a proposed borrow position is less than the minimum USD value
ErrBelowMinimumBorrowValue = sdkerrors.Register(ModuleName, 30, "invalid proposed borrow value")
)

View File

@ -61,6 +61,7 @@ func (suite *GenesisTestSuite) TestGenesisValidation() {
types.MoneyMarkets{
types.NewMoneyMarket("usdx", types.NewBorrowLimit(true, sdk.MustNewDecFromStr("100000000000"), sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.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()),
},
sdk.MustNewDecFromStr("10"),
),
gats: types.GenesisAccumulationTimes{
types.NewGenesisAccumulationTime("usdx", time.Date(2020, 12, 15, 14, 0, 0, 0, time.UTC), sdk.OneDec(), sdk.OneDec()),

View File

@ -11,20 +11,23 @@ import (
// Parameter keys and default values
var (
KeyMoneyMarkets = []byte("MoneyMarkets")
DefaultMoneyMarkets = MoneyMarkets{}
GovDenom = cdptypes.DefaultGovDenom
DefaultAccumulationTimes = GenesisAccumulationTimes{}
DefaultTotalSupplied = sdk.Coins{}
DefaultTotalBorrowed = sdk.Coins{}
DefaultTotalReserves = sdk.Coins{}
DefaultDeposits = Deposits{}
DefaultBorrows = Borrows{}
KeyMoneyMarkets = []byte("MoneyMarkets")
KeyMinimumBorrowUSDValue = []byte("MinimumBorrowUSDValue")
DefaultMoneyMarkets = MoneyMarkets{}
DefaultMinimumBorrowUSDValue = sdk.NewDec(10) // $10 USD minimum borrow value
GovDenom = cdptypes.DefaultGovDenom
DefaultAccumulationTimes = GenesisAccumulationTimes{}
DefaultTotalSupplied = sdk.Coins{}
DefaultTotalBorrowed = sdk.Coins{}
DefaultTotalReserves = sdk.Coins{}
DefaultDeposits = Deposits{}
DefaultBorrows = Borrows{}
)
// Params governance parameters for hard module
type Params struct {
MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"`
MoneyMarkets MoneyMarkets `json:"money_markets" yaml:"money_markets"`
MinimumBorrowUSDValue sdk.Dec `json:"minimum_borrow_usd_value" yaml:"minimum_borrow_usd_value"`
}
// BorrowLimit enforces restrictions on a money market
@ -220,22 +223,24 @@ func (irm InterestRateModel) Equal(irmCompareTo InterestRateModel) bool {
type InterestRateModels []InterestRateModel
// NewParams returns a new params object
func NewParams(moneyMarkets MoneyMarkets) Params {
func NewParams(moneyMarkets MoneyMarkets, minimumBorrowUSDValue sdk.Dec) Params {
return Params{
MoneyMarkets: moneyMarkets,
MoneyMarkets: moneyMarkets,
MinimumBorrowUSDValue: minimumBorrowUSDValue,
}
}
// DefaultParams returns default params for hard module
func DefaultParams() Params {
return NewParams(DefaultMoneyMarkets)
return NewParams(DefaultMoneyMarkets, DefaultMinimumBorrowUSDValue)
}
// String implements fmt.Stringer
func (p Params) String() string {
return fmt.Sprintf(`Params:
Money Markets %v`,
p.MoneyMarkets)
Minimum Borrow USD Value: %v
Money Markets: %v`,
p.MinimumBorrowUSDValue, p.MoneyMarkets)
}
// ParamKeyTable Key declaration for parameters
@ -247,14 +252,32 @@ func ParamKeyTable() params.KeyTable {
func (p *Params) ParamSetPairs() params.ParamSetPairs {
return params.ParamSetPairs{
params.NewParamSetPair(KeyMoneyMarkets, &p.MoneyMarkets, validateMoneyMarketParams),
params.NewParamSetPair(KeyMinimumBorrowUSDValue, &p.MinimumBorrowUSDValue, validateMinimumBorrowUSDValue),
}
}
// Validate checks that the parameters have valid values.
func (p Params) Validate() error {
if err := validateMinimumBorrowUSDValue(p.MinimumBorrowUSDValue); err != nil {
return err
}
return validateMoneyMarketParams(p.MoneyMarkets)
}
func validateMinimumBorrowUSDValue(i interface{}) error {
minBorrowVal, ok := i.(sdk.Dec)
if !ok {
return fmt.Errorf("invalid parameter type: %T", i)
}
if minBorrowVal.IsNegative() {
return fmt.Errorf("Minimum borrow USD value cannot be negative")
}
return nil
}
func validateMoneyMarketParams(i interface{}) error {
mm, ok := i.(MoneyMarkets)
if !ok {

View File

@ -4,6 +4,7 @@ import (
"strings"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/x/hard/types"
@ -15,7 +16,8 @@ type ParamTestSuite struct {
func (suite *ParamTestSuite) TestParamValidation() {
type args struct {
mms types.MoneyMarkets
minBorrowVal sdk.Dec
mms types.MoneyMarkets
}
testCases := []struct {
name string
@ -26,7 +28,8 @@ func (suite *ParamTestSuite) TestParamValidation() {
{
name: "default",
args: args{
mms: types.DefaultMoneyMarkets,
minBorrowVal: types.DefaultMinimumBorrowUSDValue,
mms: types.DefaultMoneyMarkets,
},
expectPass: true,
expectedErr: "",
@ -34,7 +37,7 @@ func (suite *ParamTestSuite) TestParamValidation() {
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
params := types.NewParams(tc.args.mms)
params := types.NewParams(tc.args.mms, tc.args.minBorrowVal)
err := params.Validate()
if tc.expectPass {
suite.NoError(err)

View File

@ -159,6 +159,7 @@ func NewHardGenStateMulti() app.GenesisState {
hard.NewMoneyMarket("btcb", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "btc:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
hard.NewMoneyMarket("xrp", hard.NewBorrowLimit(false, borrowLimit, loanToValue), "xrp:usd", sdk.NewInt(1000000), hard.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
},
sdk.NewDec(10),
), hard.DefaultAccumulationTimes, hard.DefaultDeposits, hard.DefaultBorrows,
hard.DefaultTotalSupplied, hard.DefaultTotalBorrowed, hard.DefaultTotalReserves,
)