mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-11-10 10:05:18 +00:00
Hard: suppliers earn interest (#749)
* update to borrow interest factor * add supply interest factor to accrue interest * supply interest factor keeper methods * fix potential bug with user borrow indexing * sync supply interest on deposit/withdraw * separate withdraw/deposit * relocate interest sync methods * update comment * simplify supply interest statement * check truncated int not zero * add .sub(storedAmount) back * add store key suppliedcoins * increment/decrement supplied coins * update withdraw with new accounting * update withdraw test * catch repay edge case * unit tests * TestSupplyInterest scaffolding * test notes * temp: interest test * example test * changes, test checks more state * fix: calculate supply interest directly * fix: catch divide by zero * add state checks back into interest test * add snapshot test cases * test owed supplied interest paid at correct ratio * test user supply syncs user's borrow interest * remove print statements and clean up * refactor indented logic * test supply/borrow multiple coins * update decoder test Co-authored-by: karzak <kjydavis3@gmail.com>
This commit is contained in:
parent
e9f5043c84
commit
f7a73c9245
@ -53,7 +53,8 @@ var (
|
||||
NewQuerier = keeper.NewQuerier
|
||||
CalculateUtilizationRatio = keeper.CalculateUtilizationRatio
|
||||
CalculateBorrowRate = keeper.CalculateBorrowRate
|
||||
CalculateInterestFactor = keeper.CalculateInterestFactor
|
||||
CalculateBorrowInterestFactor = keeper.CalculateBorrowInterestFactor
|
||||
CalculateSupplyInterestFactor = keeper.CalculateSupplyInterestFactor
|
||||
APYToSPY = keeper.APYToSPY
|
||||
ClaimKey = types.ClaimKey
|
||||
DefaultGenesisState = types.DefaultGenesisState
|
||||
|
@ -13,11 +13,11 @@ import (
|
||||
func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins) error {
|
||||
// Set any new denoms' global borrow index to 1.0
|
||||
for _, coin := range coins {
|
||||
_, foundInterestFactor := k.GetInterestFactor(ctx, coin.Denom)
|
||||
_, foundInterestFactor := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||
if !foundInterestFactor {
|
||||
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
|
||||
if foundMm {
|
||||
k.SetInterestFactor(ctx, coin.Denom, sdk.OneDec())
|
||||
k.SetBorrowInterestFactor(ctx, coin.Denom, sdk.OneDec())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -28,11 +28,8 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
|
||||
return err
|
||||
}
|
||||
|
||||
// If the user has an existing borrow, sync its outstanding interest
|
||||
_, found := k.GetBorrow(ctx, borrower)
|
||||
if found {
|
||||
k.SyncOutstandingInterest(ctx, borrower)
|
||||
}
|
||||
// Sync any outstanding interest
|
||||
k.SyncBorrowInterest(ctx, borrower)
|
||||
|
||||
// Validate borrow amount within user and protocol limits
|
||||
err = k.ValidateBorrow(ctx, borrower, coins)
|
||||
@ -57,22 +54,39 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
|
||||
}
|
||||
}
|
||||
|
||||
// The first time a user borrows a denom we add it the user's borrow interest factor index
|
||||
var borrowInterestFactors types.BorrowInterestFactors
|
||||
currBorrow, foundBorrow := k.GetBorrow(ctx, borrower)
|
||||
// On user's first borrow, build borrow index list containing denoms and current global borrow index value
|
||||
// We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps.
|
||||
if !found {
|
||||
var interestFactors types.InterestFactors
|
||||
if foundBorrow {
|
||||
// If the coin denom to be borrowed is not in the user's existing borrow, we add it borrow index
|
||||
for _, coin := range coins {
|
||||
interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
|
||||
interestFactor := types.NewInterestFactor(coin.Denom, interestFactorValue)
|
||||
interestFactors = append(interestFactors, interestFactor)
|
||||
if !sdk.NewCoins(coin).DenomsSubsetOf(currBorrow.Amount) {
|
||||
borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||
borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
|
||||
borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
|
||||
}
|
||||
}
|
||||
// Concatenate new borrow interest factors to existing borrow interest factors
|
||||
borrowInterestFactors = append(borrowInterestFactors, currBorrow.Index...)
|
||||
} else {
|
||||
for _, coin := range coins {
|
||||
borrowInterestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||
borrowInterestFactor := types.NewBorrowInterestFactor(coin.Denom, borrowInterestFactorValue)
|
||||
borrowInterestFactors = append(borrowInterestFactors, borrowInterestFactor)
|
||||
}
|
||||
borrow := types.NewBorrow(borrower, sdk.Coins{}, interestFactors)
|
||||
k.SetBorrow(ctx, borrow)
|
||||
}
|
||||
|
||||
// Add the newly borrowed coins to the user's borrow object
|
||||
borrow, _ := k.GetBorrow(ctx, borrower)
|
||||
borrow.Amount = borrow.Amount.Add(coins...)
|
||||
// Calculate new borrow amount
|
||||
var amount sdk.Coins
|
||||
if foundBorrow {
|
||||
amount = currBorrow.Amount.Add(coins...)
|
||||
} else {
|
||||
amount = coins
|
||||
}
|
||||
|
||||
// Update the borrower's amount and borrow interest factors in the store
|
||||
borrow := types.NewBorrow(borrower, amount, borrowInterestFactors)
|
||||
k.SetBorrow(ctx, borrow)
|
||||
|
||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, borrower)
|
||||
@ -92,46 +106,6 @@ func (k Keeper) Borrow(ctx sdk.Context, borrower sdk.AccAddress, coins sdk.Coins
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncOutstandingInterest updates the user's owed interest on newly borrowed coins to the latest global state
|
||||
func (k Keeper) SyncOutstandingInterest(ctx sdk.Context, addr sdk.AccAddress) {
|
||||
totalNewInterest := sdk.Coins{}
|
||||
|
||||
// Update user's borrow index list for each asset in the 'coins' array.
|
||||
// We use a list of BorrowIndexItem here because Amino doesn't support marshaling maps.
|
||||
borrow, found := k.GetBorrow(ctx, addr)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
for _, coin := range borrow.Amount {
|
||||
// Locate the borrow index item by coin denom in the user's list of borrow indexes
|
||||
foundAtIndex := -1
|
||||
for i := range borrow.Index {
|
||||
if borrow.Index[i].Denom == coin.Denom {
|
||||
foundAtIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
interestFactorValue, _ := k.GetInterestFactor(ctx, coin.Denom)
|
||||
if foundAtIndex == -1 { // First time user has borrowed this denom
|
||||
borrow.Index = append(borrow.Index, types.NewInterestFactor(coin.Denom, interestFactorValue))
|
||||
} else { // User has an existing borrow index for this denom
|
||||
// Calculate interest owed by user since asset's last borrow index update
|
||||
storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom))
|
||||
userLastInterestFactor := borrow.Index[foundAtIndex].Value
|
||||
interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount)
|
||||
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
|
||||
// We're synced up, so update user's borrow index value to match the current global borrow index value
|
||||
borrow.Index[foundAtIndex].Value = interestFactorValue
|
||||
}
|
||||
}
|
||||
// Add all pending interest to user's borrow
|
||||
borrow.Amount = borrow.Amount.Add(totalNewInterest...)
|
||||
|
||||
// Update user's borrow in the store
|
||||
k.SetBorrow(ctx, borrow)
|
||||
}
|
||||
|
||||
// ValidateBorrow validates a borrow request against borrower and protocol requirements
|
||||
func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount sdk.Coins) error {
|
||||
if amount.IsZero() {
|
||||
@ -240,7 +214,7 @@ func (k Keeper) ValidateBorrow(ctx sdk.Context, borrower sdk.AccAddress, amount
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementBorrowedCoins increments the amount of borrowed coins by the newCoins parameter
|
||||
// IncrementBorrowedCoins increments the total amount of borrowed coins by the newCoins parameter
|
||||
func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
|
||||
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
||||
if !found {
|
||||
@ -252,7 +226,7 @@ func (k Keeper) IncrementBorrowedCoins(ctx sdk.Context, newCoins sdk.Coins) {
|
||||
}
|
||||
}
|
||||
|
||||
// DecrementBorrowedCoins decrements the amount of borrowed coins by the coins parameter
|
||||
// DecrementBorrowedCoins decrements the total amount of borrowed coins by the coins parameter
|
||||
func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
|
||||
borrowedCoins, found := k.GetBorrowedCoins(ctx)
|
||||
if !found {
|
||||
@ -272,10 +246,13 @@ func (k Keeper) DecrementBorrowedCoins(ctx sdk.Context, coins sdk.Coins) error {
|
||||
func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.Coins {
|
||||
borrowBalance := sdk.Coins{}
|
||||
borrow, found := k.GetBorrow(ctx, borrower)
|
||||
if found {
|
||||
if !found {
|
||||
return borrowBalance
|
||||
}
|
||||
|
||||
totalNewInterest := sdk.Coins{}
|
||||
for _, coin := range borrow.Amount {
|
||||
interestFactorValue, foundInterestFactorValue := k.GetInterestFactor(ctx, coin.Denom)
|
||||
interestFactorValue, foundInterestFactorValue := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||
if foundInterestFactorValue {
|
||||
// Locate the interest factor by coin denom in the user's list of interest factors
|
||||
foundAtIndex := -1
|
||||
@ -294,7 +271,6 @@ func (k Keeper) GetBorrowBalance(ctx sdk.Context, borrower sdk.AccAddress) sdk.C
|
||||
}
|
||||
}
|
||||
}
|
||||
borrowBalance = borrow.Amount.Add(totalNewInterest...)
|
||||
}
|
||||
return borrowBalance
|
||||
|
||||
return borrow.Amount.Add(totalNewInterest...)
|
||||
}
|
||||
|
@ -12,13 +12,26 @@ import (
|
||||
|
||||
// Deposit deposit
|
||||
func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
|
||||
// Set any new denoms' global supply index to 1.0
|
||||
for _, coin := range coins {
|
||||
_, foundInterestFactor := k.GetSupplyInterestFactor(ctx, coin.Denom)
|
||||
if !foundInterestFactor {
|
||||
_, foundMm := k.GetMoneyMarket(ctx, coin.Denom)
|
||||
if foundMm {
|
||||
k.SetSupplyInterestFactor(ctx, coin.Denom, sdk.OneDec())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current stored LTV based on stored borrows/deposits
|
||||
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.SyncOutstandingInterest(ctx, depositor)
|
||||
// Sync any outstanding interest
|
||||
k.SyncBorrowInterest(ctx, depositor)
|
||||
k.SyncSupplyInterest(ctx, depositor)
|
||||
|
||||
err = k.ValidateDeposit(ctx, coins)
|
||||
if err != nil {
|
||||
@ -44,17 +57,47 @@ func (k Keeper) Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coi
|
||||
return err
|
||||
}
|
||||
|
||||
deposit, found := k.GetDeposit(ctx, depositor)
|
||||
if !found {
|
||||
deposit = types.NewDeposit(depositor, coins)
|
||||
// The first time a user deposits a denom we add it the user's supply interest factor index
|
||||
var supplyInterestFactors types.SupplyInterestFactors
|
||||
currDeposit, foundDeposit := k.GetDeposit(ctx, depositor)
|
||||
// On user's first deposit, build deposit index list containing denoms and current global deposit index value
|
||||
if foundDeposit {
|
||||
// If the coin denom to be deposited is not in the user's existing deposit, we add it deposit index
|
||||
for _, coin := range coins {
|
||||
if !sdk.NewCoins(coin).DenomsSubsetOf(currDeposit.Amount) {
|
||||
supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
|
||||
supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
|
||||
supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
|
||||
}
|
||||
}
|
||||
// Concatenate new deposit interest factors to existing deposit interest factors
|
||||
supplyInterestFactors = append(supplyInterestFactors, currDeposit.Index...)
|
||||
} else {
|
||||
deposit.Amount = deposit.Amount.Add(coins...)
|
||||
for _, coin := range coins {
|
||||
supplyInterestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
|
||||
supplyInterestFactor := types.NewSupplyInterestFactor(coin.Denom, supplyInterestFactorValue)
|
||||
supplyInterestFactors = append(supplyInterestFactors, supplyInterestFactor)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate new deposit amount
|
||||
var amount sdk.Coins
|
||||
if foundDeposit {
|
||||
amount = currDeposit.Amount.Add(coins...)
|
||||
} else {
|
||||
amount = coins
|
||||
}
|
||||
|
||||
// Update the depositer's amount and supply interest factors in the store
|
||||
deposit := types.NewDeposit(depositor, amount, supplyInterestFactors)
|
||||
k.SetDeposit(ctx, deposit)
|
||||
|
||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
|
||||
|
||||
// Update total supplied amount by newly supplied coins. Don't add user's pending interest as
|
||||
// it has already been included in the total supplied coins by the BeginBlocker.
|
||||
k.IncrementSuppliedCoins(ctx, coins)
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHardDeposit,
|
||||
@ -84,76 +127,37 @@ func (k Keeper) ValidateDeposit(ctx sdk.Context, coins sdk.Coins) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Withdraw returns some or all of a deposit back to original depositor
|
||||
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
|
||||
deposit, found := k.GetDeposit(ctx, depositor)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
|
||||
}
|
||||
|
||||
// Get current stored LTV based on stored borrows/deposits
|
||||
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.SyncOutstandingInterest(ctx, depositor)
|
||||
|
||||
borrow, found := k.GetBorrow(ctx, depositor)
|
||||
if !found {
|
||||
borrow = types.Borrow{}
|
||||
}
|
||||
|
||||
proposedDepositAmount, isNegative := deposit.Amount.SafeSub(coins)
|
||||
if isNegative {
|
||||
return types.ErrNegativeBorrowedCoins
|
||||
}
|
||||
proposedDeposit := types.NewDeposit(deposit.Depositor, proposedDepositAmount)
|
||||
|
||||
valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
|
||||
}
|
||||
|
||||
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, coins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHardWithdrawal,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, coins.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
),
|
||||
)
|
||||
|
||||
if deposit.Amount.IsEqual(coins) {
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeDeleteHardDeposit,
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
),
|
||||
)
|
||||
k.DeleteDeposit(ctx, deposit)
|
||||
return nil
|
||||
}
|
||||
|
||||
deposit.Amount = deposit.Amount.Sub(coins)
|
||||
k.SetDeposit(ctx, deposit)
|
||||
|
||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTotalDeposited returns the total amount deposited for the input deposit type and deposit denom
|
||||
func (k Keeper) GetTotalDeposited(ctx sdk.Context, depositDenom string) (total sdk.Int) {
|
||||
var macc supplyExported.ModuleAccountI
|
||||
macc = k.supplyKeeper.GetModuleAccount(ctx, types.ModuleAccountName)
|
||||
return macc.GetCoins().AmountOf(depositDenom)
|
||||
}
|
||||
|
||||
// IncrementSuppliedCoins increments the total amount of supplied coins by the newCoins parameter
|
||||
func (k Keeper) IncrementSuppliedCoins(ctx sdk.Context, newCoins sdk.Coins) {
|
||||
suppliedCoins, found := k.GetSuppliedCoins(ctx)
|
||||
if !found {
|
||||
if !newCoins.Empty() {
|
||||
k.SetSuppliedCoins(ctx, newCoins)
|
||||
}
|
||||
} else {
|
||||
k.SetSuppliedCoins(ctx, suppliedCoins.Add(newCoins...))
|
||||
}
|
||||
}
|
||||
|
||||
// DecrementSuppliedCoins decrements the total amount of supplied coins by the coins parameter
|
||||
func (k Keeper) DecrementSuppliedCoins(ctx sdk.Context, coins sdk.Coins) error {
|
||||
suppliedCoins, found := k.GetSuppliedCoins(ctx)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrSuppliedCoinsNotFound, "cannot withdraw if no coins are deposited")
|
||||
}
|
||||
|
||||
updatedSuppliedCoins, isAnyNegative := suppliedCoins.SafeSub(coins)
|
||||
if isAnyNegative {
|
||||
return types.ErrNegativeSuppliedCoins
|
||||
}
|
||||
|
||||
k.SetSuppliedCoins(ctx, updatedSuppliedCoins)
|
||||
return nil
|
||||
}
|
||||
|
@ -196,360 +196,3 @@ func (suite *KeeperTestSuite) TestDeposit() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestWithdraw() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
depositAmount sdk.Coins
|
||||
withdrawAmount sdk.Coins
|
||||
createDeposit bool
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
depositExists bool
|
||||
finalDepositAmount sdk.Coins
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type withdrawTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []withdrawTest{
|
||||
{
|
||||
"valid partial withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
depositExists: true,
|
||||
finalDepositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid full withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.Coins(nil),
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"withdraw invalid, denom not found",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "subtraction results in negative borrow amount",
|
||||
},
|
||||
},
|
||||
{
|
||||
"withdraw exceeds deposit",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "subtraction results in negative borrow amount",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
authGS := app.NewAuthGenState([]sdk.AccAddress{tc.args.depositor}, []sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))})
|
||||
loanToValue := sdk.MustNewDecFromStr("0.6")
|
||||
hardGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
types.MoneyMarkets{
|
||||
types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), 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), sdk.NewInt(KAVA_CF*1000), 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), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
|
||||
},
|
||||
0, // LTV counter
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
|
||||
// Pricefeed module genesis state
|
||||
pricefeedGS := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.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},
|
||||
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
|
||||
},
|
||||
},
|
||||
PostedPrices: []pricefeed.PostedPrice{
|
||||
{
|
||||
MarketID: "usdx:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("1.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "kava:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("2.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "bnb:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("10.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tApp.InitializeFromGenesisStates(authGS,
|
||||
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
|
||||
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
|
||||
keeper := tApp.GetHardKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
if tc.args.createDeposit {
|
||||
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
|
||||
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.depositor)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
|
||||
if tc.args.depositExists {
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
|
||||
} else {
|
||||
suite.Require().False(f)
|
||||
}
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestLtvWithdraw() {
|
||||
type args struct {
|
||||
borrower sdk.AccAddress
|
||||
keeper sdk.AccAddress
|
||||
initialModuleCoins sdk.Coins
|
||||
initialBorrowerCoins sdk.Coins
|
||||
initialKeeperCoins sdk.Coins
|
||||
depositCoins []sdk.Coin
|
||||
borrowCoins sdk.Coins
|
||||
futureTime int64
|
||||
}
|
||||
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
|
||||
type liqTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
|
||||
// Set up test constants
|
||||
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
|
||||
reserveFactor := sdk.MustNewDecFromStr("0.05")
|
||||
oneMonthInSeconds := int64(2592000)
|
||||
borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
|
||||
keeper := sdk.AccAddress(crypto.AddressHash([]byte("testkeeper")))
|
||||
|
||||
testCases := []liqTest{
|
||||
{
|
||||
"invalid: withdraw is outside loan-to-value range",
|
||||
args{
|
||||
borrower: borrower,
|
||||
keeper: keeper,
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialKeeperCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF))),
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(8*KAVA_CF))),
|
||||
futureTime: oneMonthInSeconds,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "proposed withdraw outside loan-to-value range",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
|
||||
// Auth module genesis state
|
||||
authGS := app.NewAuthGenState(
|
||||
[]sdk.AccAddress{tc.args.borrower, tc.args.keeper},
|
||||
[]sdk.Coins{tc.args.initialBorrowerCoins, tc.args.initialKeeperCoins},
|
||||
)
|
||||
|
||||
// Harvest module genesis state
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
types.MoneyMarkets{
|
||||
types.NewMoneyMarket("ukava",
|
||||
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
|
||||
"kava:usd", // Market ID
|
||||
sdk.NewInt(KAVA_CF), // Conversion Factor
|
||||
sdk.NewInt(100000000*KAVA_CF), // Auction Size
|
||||
model, // Interest Rate Model
|
||||
reserveFactor, // Reserve Factor
|
||||
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
|
||||
},
|
||||
0, // LTV counter
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
|
||||
// Pricefeed module genesis state
|
||||
pricefeedGS := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.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: []pricefeed.PostedPrice{
|
||||
{
|
||||
MarketID: "usdx:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("1.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "kava:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("2.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize test application
|
||||
tApp.InitializeFromGenesisStates(authGS,
|
||||
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
|
||||
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
|
||||
// Mint coins to Harvest module account
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
|
||||
|
||||
auctionKeeper := tApp.GetAuctionKeeper()
|
||||
|
||||
keeper := tApp.GetHardKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
suite.auctionKeeper = auctionKeeper
|
||||
|
||||
var err error
|
||||
|
||||
// Run begin blocker to set up state
|
||||
hard.BeginBlocker(suite.ctx, suite.keeper)
|
||||
|
||||
// Deposit coins
|
||||
err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Borrow coins
|
||||
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Attempting to withdraw fails
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
|
||||
// Set up future chain context and run begin blocker, increasing user's owed borrow balance
|
||||
runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
|
||||
liqCtx := suite.ctx.WithBlockTime(runAtTime)
|
||||
hard.BeginBlocker(liqCtx, suite.keeper)
|
||||
|
||||
// Attempted withdraw of 1 coin still fails
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
|
||||
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.borrowCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
|
||||
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))))
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
|
||||
suite.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -69,14 +69,13 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get available hard module account cash on hand
|
||||
// Get current protocol state and hold in memory as 'prior'
|
||||
cashPrior := k.supplyKeeper.GetModuleAccount(ctx, types.ModuleName).GetCoins().AmountOf(denom)
|
||||
|
||||
// Get prior borrows
|
||||
borrowsPrior := sdk.NewCoin(denom, sdk.ZeroInt())
|
||||
borrowCoinsPrior, foundBorrowCoinsPrior := k.GetBorrowedCoins(ctx)
|
||||
if foundBorrowCoinsPrior {
|
||||
borrowsPrior = sdk.NewCoin(denom, borrowCoinsPrior.AmountOf(denom))
|
||||
borrowedPrior := sdk.NewCoin(denom, sdk.ZeroInt())
|
||||
borrowedCoinsPrior, foundBorrowedCoinsPrior := k.GetBorrowedCoins(ctx)
|
||||
if foundBorrowedCoinsPrior {
|
||||
borrowedPrior = sdk.NewCoin(denom, borrowedCoinsPrior.AmountOf(denom))
|
||||
}
|
||||
|
||||
reservesPrior, foundReservesPrior := k.GetTotalReserves(ctx, denom)
|
||||
@ -86,11 +85,18 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
||||
reservesPrior = newReservesPrior
|
||||
}
|
||||
|
||||
interestFactorPrior, foundInterestFactorPrior := k.GetInterestFactor(ctx, denom)
|
||||
if !foundInterestFactorPrior {
|
||||
newInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
|
||||
k.SetInterestFactor(ctx, denom, newInterestFactorPrior)
|
||||
interestFactorPrior = newInterestFactorPrior
|
||||
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := k.GetBorrowInterestFactor(ctx, denom)
|
||||
if !foundBorrowInterestFactorPrior {
|
||||
newBorrowInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
|
||||
k.SetBorrowInterestFactor(ctx, denom, newBorrowInterestFactorPrior)
|
||||
borrowInterestFactorPrior = newBorrowInterestFactorPrior
|
||||
}
|
||||
|
||||
supplyInterestFactorPrior, foundSupplyInterestFactorPrior := k.GetSupplyInterestFactor(ctx, denom)
|
||||
if !foundSupplyInterestFactorPrior {
|
||||
newSupplyInterestFactorPrior := sdk.MustNewDecFromStr("1.0")
|
||||
k.SetSupplyInterestFactor(ctx, denom, newSupplyInterestFactorPrior)
|
||||
supplyInterestFactorPrior = newSupplyInterestFactorPrior
|
||||
}
|
||||
|
||||
// Fetch money market from the store
|
||||
@ -100,7 +106,7 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
||||
}
|
||||
|
||||
// GetBorrowRate calculates the current interest rate based on utilization (the fraction of supply that has been borrowed)
|
||||
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowsPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
|
||||
borrowRateApy, err := CalculateBorrowRate(mm.InterestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowedPrior.Amount), sdk.NewDecFromInt(reservesPrior.Amount))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -111,16 +117,27 @@ func (k Keeper) AccrueInterest(ctx sdk.Context, denom string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
interestFactor := CalculateInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
|
||||
interestAccumulated := (interestFactor.Mul(sdk.NewDecFromInt(borrowsPrior.Amount)).TruncateInt()).Sub(borrowsPrior.Amount)
|
||||
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestAccumulated))
|
||||
totalReservesNew := reservesPrior.Add(sdk.NewCoin(denom, sdk.NewDecFromInt(interestAccumulated).Mul(mm.ReserveFactor).TruncateInt()))
|
||||
interestFactorNew := interestFactorPrior.Mul(interestFactor)
|
||||
// Calculate borrow interest factor and update
|
||||
borrowInterestFactor := CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(timeElapsed))
|
||||
interestBorrowAccumulated := (borrowInterestFactor.Mul(sdk.NewDecFromInt(borrowedPrior.Amount)).TruncateInt()).Sub(borrowedPrior.Amount)
|
||||
totalBorrowInterestAccumulated := sdk.NewCoins(sdk.NewCoin(denom, interestBorrowAccumulated))
|
||||
reservesNew := interestBorrowAccumulated.ToDec().Mul(mm.ReserveFactor).TruncateInt()
|
||||
borrowInterestFactorNew := borrowInterestFactorPrior.Mul(borrowInterestFactor)
|
||||
k.SetBorrowInterestFactor(ctx, denom, borrowInterestFactorNew)
|
||||
|
||||
k.SetInterestFactor(ctx, denom, interestFactorNew)
|
||||
// Calculate supply interest factor and update
|
||||
supplyInterestNew := interestBorrowAccumulated.Sub(reservesNew)
|
||||
supplyInterestFactor := CalculateSupplyInterestFactor(supplyInterestNew.ToDec(), cashPrior.ToDec(), borrowedPrior.Amount.ToDec(), reservesPrior.Amount.ToDec())
|
||||
supplyInterestFactorNew := supplyInterestFactorPrior.Mul(supplyInterestFactor)
|
||||
k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
|
||||
|
||||
// Update accural keys in store
|
||||
k.IncrementBorrowedCoins(ctx, totalBorrowInterestAccumulated)
|
||||
k.SetTotalReserves(ctx, denom, totalReservesNew)
|
||||
k.IncrementSuppliedCoins(ctx, sdk.NewCoins(sdk.NewCoin(denom, supplyInterestNew)))
|
||||
k.SetTotalReserves(ctx, denom, reservesPrior.Add(sdk.NewCoin(mm.Denom, reservesNew)))
|
||||
k.SetSupplyInterestFactor(ctx, denom, supplyInterestFactorNew)
|
||||
k.SetPreviousAccrualTime(ctx, denom, ctx.BlockTime())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -155,10 +172,10 @@ func CalculateUtilizationRatio(cash, borrows, reserves sdk.Dec) sdk.Dec {
|
||||
return sdk.MinDec(sdk.OneDec(), borrows.Quo(totalSupply))
|
||||
}
|
||||
|
||||
// CalculateInterestFactor calculates the simple interest scaling factor,
|
||||
// CalculateBorrowInterestFactor calculates the simple interest scaling factor,
|
||||
// which is equal to: (per-second interest rate * number of seconds elapsed)
|
||||
// Will return 1.000x, multiply by principal to get new principal with added interest
|
||||
func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
|
||||
func CalculateBorrowInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.Int) sdk.Dec {
|
||||
scalingFactorUint := sdk.NewUint(uint64(scalingFactor))
|
||||
scalingFactorInt := sdk.NewInt(int64(scalingFactor))
|
||||
|
||||
@ -173,6 +190,100 @@ func CalculateInterestFactor(perSecondInterestRate sdk.Dec, secondsElapsed sdk.I
|
||||
return sdk.NewDecFromBigInt(interestFactorMantissa.BigInt()).QuoInt(scalingFactorInt)
|
||||
}
|
||||
|
||||
// CalculateSupplyInterestFactor calculates the supply interest factor, which is the percentage of borrow interest
|
||||
// that flows to each unit of supply, i.e. at 50% utilization and 0% reserve factor, a 5% borrow interest will
|
||||
// correspond to a 2.5% supply interest.
|
||||
func CalculateSupplyInterestFactor(newInterest, cash, borrows, reserves sdk.Dec) sdk.Dec {
|
||||
totalSupply := cash.Add(borrows).Sub(reserves)
|
||||
if totalSupply.IsZero() {
|
||||
return sdk.OneDec()
|
||||
}
|
||||
return (newInterest.Quo(totalSupply)).Add(sdk.OneDec())
|
||||
}
|
||||
|
||||
// SyncBorrowInterest updates the user's owed interest on newly borrowed coins to the latest global state
|
||||
func (k Keeper) SyncBorrowInterest(ctx sdk.Context, addr sdk.AccAddress) {
|
||||
totalNewInterest := sdk.Coins{}
|
||||
|
||||
// Update user's borrow interest factor list for each asset in the 'coins' array.
|
||||
// We use a list of BorrowInterestFactors here because Amino doesn't support marshaling maps.
|
||||
borrow, found := k.GetBorrow(ctx, addr)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
for _, coin := range borrow.Amount {
|
||||
// Locate the borrow interest factor item by coin denom in the user's list of borrow indexes
|
||||
foundAtIndex := -1
|
||||
for i := range borrow.Index {
|
||||
if borrow.Index[i].Denom == coin.Denom {
|
||||
foundAtIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
interestFactorValue, _ := k.GetBorrowInterestFactor(ctx, coin.Denom)
|
||||
if foundAtIndex == -1 { // First time user has borrowed this denom
|
||||
borrow.Index = append(borrow.Index, types.NewBorrowInterestFactor(coin.Denom, interestFactorValue))
|
||||
} else { // User has an existing borrow index for this denom
|
||||
// Calculate interest owed by user since asset's last borrow index update
|
||||
storedAmount := sdk.NewDecFromInt(borrow.Amount.AmountOf(coin.Denom))
|
||||
userLastInterestFactor := borrow.Index[foundAtIndex].Value
|
||||
interest := (storedAmount.Quo(userLastInterestFactor).Mul(interestFactorValue)).Sub(storedAmount)
|
||||
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
|
||||
// We're synced up, so update user's borrow index value to match the current global borrow index value
|
||||
borrow.Index[foundAtIndex].Value = interestFactorValue
|
||||
}
|
||||
}
|
||||
// Add all pending interest to user's borrow
|
||||
borrow.Amount = borrow.Amount.Add(totalNewInterest...)
|
||||
|
||||
// Update user's borrow in the store
|
||||
k.SetBorrow(ctx, borrow)
|
||||
}
|
||||
|
||||
// SyncSupplyInterest updates the user's earned interest on supplied coins based on the latest global state
|
||||
func (k Keeper) SyncSupplyInterest(ctx sdk.Context, addr sdk.AccAddress) {
|
||||
totalNewInterest := sdk.Coins{}
|
||||
|
||||
// Update user's supply index list for each asset in the 'coins' array.
|
||||
// We use a list of SupplyInterestFactors here because Amino doesn't support marshaling maps.
|
||||
deposit, found := k.GetDeposit(ctx, addr)
|
||||
if !found {
|
||||
return
|
||||
}
|
||||
|
||||
for _, coin := range deposit.Amount {
|
||||
// Locate the deposit index item by coin denom in the user's list of deposit indexes
|
||||
foundAtIndex := -1
|
||||
for i := range deposit.Index {
|
||||
if deposit.Index[i].Denom == coin.Denom {
|
||||
foundAtIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
interestFactorValue, _ := k.GetSupplyInterestFactor(ctx, coin.Denom)
|
||||
if foundAtIndex == -1 { // First time user has supplied this denom
|
||||
deposit.Index = append(deposit.Index, types.NewSupplyInterestFactor(coin.Denom, interestFactorValue))
|
||||
} else { // User has an existing supply index for this denom
|
||||
// Calculate interest earned by user since asset's last deposit index update
|
||||
storedAmount := sdk.NewDecFromInt(deposit.Amount.AmountOf(coin.Denom))
|
||||
userLastInterestFactor := deposit.Index[foundAtIndex].Value
|
||||
interest := (storedAmount.Mul(interestFactorValue).Quo(userLastInterestFactor)).Sub(storedAmount)
|
||||
if interest.TruncateInt().GT(sdk.ZeroInt()) {
|
||||
totalNewInterest = totalNewInterest.Add(sdk.NewCoin(coin.Denom, interest.TruncateInt()))
|
||||
}
|
||||
// We're synced up, so update user's deposit index value to match the current global deposit index value
|
||||
deposit.Index[foundAtIndex].Value = interestFactorValue
|
||||
}
|
||||
}
|
||||
// Add all pending interest to user's deposit
|
||||
deposit.Amount = deposit.Amount.Add(totalNewInterest...)
|
||||
|
||||
// Update user's deposit in the store
|
||||
k.SetDeposit(ctx, deposit)
|
||||
}
|
||||
|
||||
// APYToSPY converts the input annual interest rate. For example, 10% apy would be passed as 1.10.
|
||||
// SPY = Per second compounded interest rate is how cosmos mathematically represents APY.
|
||||
func APYToSPY(apy sdk.Dec) (sdk.Dec, error) {
|
||||
|
@ -202,7 +202,7 @@ func (suite *InterestTestSuite) TestCalculateBorrowRate() {
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *InterestTestSuite) TestCalculateInterestFactor() {
|
||||
func (suite *InterestTestSuite) TestCalculateBorrowInterestFactor() {
|
||||
type args struct {
|
||||
perSecondInterestRate sdk.Dec
|
||||
timeElapsed sdk.Int
|
||||
@ -302,7 +302,65 @@ func (suite *InterestTestSuite) TestCalculateInterestFactor() {
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
interestFactor := hard.CalculateInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
|
||||
interestFactor := hard.CalculateBorrowInterestFactor(tc.args.perSecondInterestRate, tc.args.timeElapsed)
|
||||
suite.Require().Equal(tc.args.expectedValue, interestFactor)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *InterestTestSuite) TestCalculateSupplyInterestFactor() {
|
||||
type args struct {
|
||||
newInterest sdk.Dec
|
||||
cash sdk.Dec
|
||||
borrows sdk.Dec
|
||||
reserves sdk.Dec
|
||||
reserveFactor sdk.Dec
|
||||
expectedValue sdk.Dec
|
||||
}
|
||||
|
||||
type test struct {
|
||||
name string
|
||||
args args
|
||||
}
|
||||
|
||||
testCases := []test{
|
||||
{
|
||||
"low new interest",
|
||||
args{
|
||||
newInterest: sdk.MustNewDecFromStr("1"),
|
||||
cash: sdk.MustNewDecFromStr("100.0"),
|
||||
borrows: sdk.MustNewDecFromStr("1000.0"),
|
||||
reserves: sdk.MustNewDecFromStr("10.0"),
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedValue: sdk.MustNewDecFromStr("1.000917431192660550"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"medium new interest",
|
||||
args{
|
||||
newInterest: sdk.MustNewDecFromStr("5"),
|
||||
cash: sdk.MustNewDecFromStr("100.0"),
|
||||
borrows: sdk.MustNewDecFromStr("1000.0"),
|
||||
reserves: sdk.MustNewDecFromStr("10.0"),
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedValue: sdk.MustNewDecFromStr("1.004587155963302752"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"high new interest",
|
||||
args{
|
||||
newInterest: sdk.MustNewDecFromStr("10"),
|
||||
cash: sdk.MustNewDecFromStr("100.0"),
|
||||
borrows: sdk.MustNewDecFromStr("1000.0"),
|
||||
reserves: sdk.MustNewDecFromStr("10.0"),
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedValue: sdk.MustNewDecFromStr("1.009174311926605505"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
interestFactor := hard.CalculateSupplyInterestFactor(tc.args.newInterest,
|
||||
tc.args.cash, tc.args.borrows, tc.args.reserves)
|
||||
suite.Require().Equal(tc.args.expectedValue, interestFactor)
|
||||
}
|
||||
}
|
||||
@ -377,7 +435,6 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
spy, err := hard.APYToSPY(tc.args.apy)
|
||||
@ -391,13 +448,13 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
|
||||
}
|
||||
}
|
||||
|
||||
type ExpectedInterest struct {
|
||||
type ExpectedBorrowInterest struct {
|
||||
elapsedTime int64
|
||||
shouldBorrow bool
|
||||
borrowCoin sdk.Coin
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestInterest() {
|
||||
func (suite *KeeperTestSuite) TestBorrowInterest() {
|
||||
type args struct {
|
||||
user sdk.AccAddress
|
||||
initialBorrowerCoins sdk.Coins
|
||||
@ -406,7 +463,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins sdk.Coins
|
||||
interestRateModel types.InterestRateModel
|
||||
reserveFactor sdk.Dec
|
||||
expectedInterestSnaphots []ExpectedInterest
|
||||
expectedInterestSnaphots []ExpectedBorrowInterest
|
||||
}
|
||||
|
||||
type errArgs struct {
|
||||
@ -438,7 +495,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneDayInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -461,7 +518,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneWeekInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -484,7 +541,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -507,7 +564,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneYearInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -530,7 +587,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneYearInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -553,7 +610,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneYearInSeconds,
|
||||
shouldBorrow: true,
|
||||
@ -576,7 +633,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -604,7 +661,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedInterest{
|
||||
expectedInterestSnaphots: []ExpectedBorrowInterest{
|
||||
{
|
||||
elapsedTime: oneDayInSeconds,
|
||||
shouldBorrow: false,
|
||||
@ -633,6 +690,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// Initialize test app and set context
|
||||
@ -737,7 +795,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
reservesPrior = sdk.NewCoin(tc.args.borrowCoinDenom, sdk.ZeroInt())
|
||||
}
|
||||
|
||||
interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetInterestFactor(prevCtx, tc.args.borrowCoinDenom)
|
||||
interestFactorPrior, foundInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, tc.args.borrowCoinDenom)
|
||||
suite.Require().True(foundInterestFactorPrior)
|
||||
|
||||
// 2. Calculate expected interest owed
|
||||
@ -748,7 +806,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
|
||||
suite.Require().NoError(err)
|
||||
|
||||
interestFactor := hard.CalculateInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
|
||||
interestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
|
||||
expectedInterest := (interestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
|
||||
expectedReserves := reservesPrior.Add(sdk.NewCoin(tc.args.borrowCoinDenom, sdk.NewDecFromInt(expectedInterest).Mul(tc.args.reserveFactor).TruncateInt()))
|
||||
expectedInterestFactor := interestFactorPrior.Mul(interestFactor)
|
||||
@ -769,7 +827,7 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
suite.Require().Equal(expectedReserves, currTotalReserves)
|
||||
|
||||
// Check that the borrow index has increased as expected
|
||||
currIndexPrior, _ := suite.keeper.GetInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
|
||||
currIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, tc.args.borrowCoinDenom)
|
||||
suite.Require().Equal(expectedInterestFactor, currIndexPrior)
|
||||
|
||||
// After borrowing again user's borrow balance should have any outstanding interest applied
|
||||
@ -791,6 +849,495 @@ func (suite *KeeperTestSuite) TestInterest() {
|
||||
}
|
||||
}
|
||||
|
||||
type ExpectedSupplyInterest struct {
|
||||
elapsedTime int64
|
||||
shouldSupply bool
|
||||
supplyCoin sdk.Coin
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestSupplyInterest() {
|
||||
type args struct {
|
||||
user sdk.AccAddress
|
||||
initialSupplierCoins sdk.Coins
|
||||
initialBorrowerCoins sdk.Coins
|
||||
initialModuleCoins sdk.Coins
|
||||
depositCoins sdk.Coins
|
||||
coinDenoms []string
|
||||
borrowCoins sdk.Coins
|
||||
interestRateModel types.InterestRateModel
|
||||
reserveFactor sdk.Dec
|
||||
expectedInterestSnaphots []ExpectedSupplyInterest
|
||||
}
|
||||
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
|
||||
type interestTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
|
||||
normalModel := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
|
||||
|
||||
oneDayInSeconds := int64(86400)
|
||||
oneWeekInSeconds := int64(604800)
|
||||
oneMonthInSeconds := int64(2592000)
|
||||
oneYearInSeconds := int64(31536000)
|
||||
|
||||
testCases := []interestTest{
|
||||
{
|
||||
"one day",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneDayInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"one week",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneWeekInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"one month",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"one year",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneYearInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"supply/borrow multiple coins",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(100*BNB_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(20*BNB_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"supply during snapshot",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: true,
|
||||
supplyCoin: sdk.NewCoin("ukava", sdk.NewInt(20*KAVA_CF)),
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"multiple snapshots",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(80*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"varied snapshots",
|
||||
args{
|
||||
user: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
initialModuleCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(1000*KAVA_CF))),
|
||||
depositCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_CF))),
|
||||
coinDenoms: []string{"ukava"},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
|
||||
interestRateModel: normalModel,
|
||||
reserveFactor: sdk.MustNewDecFromStr("0.05"),
|
||||
expectedInterestSnaphots: []ExpectedSupplyInterest{
|
||||
{
|
||||
elapsedTime: oneMonthInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
{
|
||||
elapsedTime: oneDayInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
{
|
||||
elapsedTime: oneYearInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
{
|
||||
elapsedTime: oneWeekInSeconds,
|
||||
shouldSupply: false,
|
||||
supplyCoin: sdk.Coin{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
|
||||
// Auth module genesis state
|
||||
authGS := app.NewAuthGenState(
|
||||
[]sdk.AccAddress{tc.args.user},
|
||||
[]sdk.Coins{tc.args.initialBorrowerCoins},
|
||||
)
|
||||
|
||||
// Hard module genesis state
|
||||
hardGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
types.MoneyMarkets{
|
||||
types.NewMoneyMarket("ukava",
|
||||
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
|
||||
"kava:usd", // Market ID
|
||||
sdk.NewInt(KAVA_CF), // Conversion Factor
|
||||
sdk.NewInt(USDX_CF*1000), // Auction Size
|
||||
tc.args.interestRateModel, // Interest Rate Model
|
||||
tc.args.reserveFactor, // Reserve Factor
|
||||
sdk.ZeroDec()), // Keeper Reward Percentage
|
||||
types.NewMoneyMarket("bnb",
|
||||
types.NewBorrowLimit(false, sdk.NewDec(100000000*BNB_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
|
||||
"bnb:usd", // Market ID
|
||||
sdk.NewInt(BNB_CF), // Conversion Factor
|
||||
sdk.NewInt(USDX_CF*1000), // Auction Size
|
||||
tc.args.interestRateModel, // Interest Rate Model
|
||||
tc.args.reserveFactor, // Reserve Factor
|
||||
sdk.ZeroDec()), // Keeper Reward Percentage
|
||||
},
|
||||
0, // LTV counter
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
|
||||
// Pricefeed module genesis state
|
||||
pricefeedGS := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.Market{
|
||||
{MarketID: "kava:usd", BaseAsset: "kava", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
|
||||
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
|
||||
},
|
||||
},
|
||||
PostedPrices: []pricefeed.PostedPrice{
|
||||
{
|
||||
MarketID: "kava:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("2.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "bnb:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("20.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize test application
|
||||
tApp.InitializeFromGenesisStates(authGS,
|
||||
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
|
||||
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
|
||||
|
||||
// Mint coins to Hard module account
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
|
||||
|
||||
keeper := tApp.GetHardKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
suite.keeper.SetSuppliedCoins(ctx, tc.args.initialModuleCoins)
|
||||
|
||||
var err error
|
||||
|
||||
// Run begin blocker
|
||||
hard.BeginBlocker(suite.ctx, suite.keeper)
|
||||
|
||||
// // Deposit coins
|
||||
err = suite.keeper.Deposit(suite.ctx, tc.args.user, tc.args.depositCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Borrow coins
|
||||
err = suite.keeper.Borrow(suite.ctx, tc.args.user, tc.args.borrowCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check interest levels for each snapshot
|
||||
prevCtx := suite.ctx
|
||||
for _, snapshot := range tc.args.expectedInterestSnaphots {
|
||||
for _, coinDenom := range tc.args.coinDenoms {
|
||||
// ---------------------------- Calculate expected supply interest ----------------------------
|
||||
// 1. Get cash, borrows, reserves, and borrow index
|
||||
cashPrior := suite.getModuleAccountAtCtx(types.ModuleName, prevCtx).GetCoins().AmountOf(coinDenom)
|
||||
|
||||
var borrowCoinPriorAmount sdk.Int
|
||||
borrowCoinsPrior, borrowCoinsPriorFound := suite.keeper.GetBorrowedCoins(prevCtx)
|
||||
suite.Require().True(borrowCoinsPriorFound)
|
||||
borrowCoinPriorAmount = borrowCoinsPrior.AmountOf(coinDenom)
|
||||
|
||||
var supplyCoinPriorAmount sdk.Int
|
||||
supplyCoinsPrior, supplyCoinsPriorFound := suite.keeper.GetSuppliedCoins(prevCtx)
|
||||
suite.Require().True(supplyCoinsPriorFound)
|
||||
supplyCoinPriorAmount = supplyCoinsPrior.AmountOf(coinDenom)
|
||||
|
||||
reservesPrior, foundReservesPrior := suite.keeper.GetTotalReserves(prevCtx, coinDenom)
|
||||
if !foundReservesPrior {
|
||||
reservesPrior = sdk.NewCoin(coinDenom, sdk.ZeroInt())
|
||||
}
|
||||
|
||||
borrowInterestFactorPrior, foundBorrowInterestFactorPrior := suite.keeper.GetBorrowInterestFactor(prevCtx, coinDenom)
|
||||
suite.Require().True(foundBorrowInterestFactorPrior)
|
||||
|
||||
supplyInterestFactorPrior, foundSupplyInterestFactorPrior := suite.keeper.GetSupplyInterestFactor(prevCtx, coinDenom)
|
||||
suite.Require().True(foundSupplyInterestFactorPrior)
|
||||
|
||||
// 2. Calculate expected borrow interest owed
|
||||
borrowRateApy, err := hard.CalculateBorrowRate(tc.args.interestRateModel, sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Convert from APY to SPY, expressed as (1 + borrow rate)
|
||||
borrowRateSpy, err := hard.APYToSPY(sdk.OneDec().Add(borrowRateApy))
|
||||
suite.Require().NoError(err)
|
||||
|
||||
newBorrowInterestFactor := hard.CalculateBorrowInterestFactor(borrowRateSpy, sdk.NewInt(snapshot.elapsedTime))
|
||||
expectedBorrowInterest := (newBorrowInterestFactor.Mul(sdk.NewDecFromInt(borrowCoinPriorAmount)).TruncateInt()).Sub(borrowCoinPriorAmount)
|
||||
expectedReserves := reservesPrior.Add(sdk.NewCoin(coinDenom, sdk.NewDecFromInt(expectedBorrowInterest).Mul(tc.args.reserveFactor).TruncateInt())).Sub(reservesPrior)
|
||||
expectedTotalReserves := expectedReserves.Add(reservesPrior)
|
||||
|
||||
expectedBorrowInterestFactor := borrowInterestFactorPrior.Mul(newBorrowInterestFactor)
|
||||
expectedSupplyInterest := expectedBorrowInterest.Sub(expectedReserves.Amount)
|
||||
|
||||
newSupplyInterestFactor := hard.CalculateSupplyInterestFactor(expectedSupplyInterest.ToDec(), sdk.NewDecFromInt(cashPrior), sdk.NewDecFromInt(borrowCoinPriorAmount), sdk.NewDecFromInt(reservesPrior.Amount))
|
||||
expectedSupplyInterestFactor := supplyInterestFactorPrior.Mul(newSupplyInterestFactor)
|
||||
// -------------------------------------------------------------------------------------
|
||||
|
||||
// Set up snapshot chain context and run begin blocker
|
||||
runAtTime := time.Unix(prevCtx.BlockTime().Unix()+(snapshot.elapsedTime), 0)
|
||||
snapshotCtx := prevCtx.WithBlockTime(runAtTime)
|
||||
hard.BeginBlocker(snapshotCtx, suite.keeper)
|
||||
|
||||
borrowInterestFactor, _ := suite.keeper.GetBorrowInterestFactor(ctx, coinDenom)
|
||||
suite.Require().Equal(expectedBorrowInterestFactor, borrowInterestFactor)
|
||||
suite.Require().Equal(expectedBorrowInterest, expectedSupplyInterest.Add(expectedReserves.Amount))
|
||||
|
||||
// Check that the total amount of borrowed coins has increased by expected borrow interest amount
|
||||
borrowCoinsPost, _ := suite.keeper.GetBorrowedCoins(snapshotCtx)
|
||||
borrowCoinPostAmount := borrowCoinsPost.AmountOf(coinDenom)
|
||||
suite.Require().Equal(borrowCoinPostAmount, borrowCoinPriorAmount.Add(expectedBorrowInterest))
|
||||
|
||||
// Check that the total amount of supplied coins has increased by expected supply interest amount
|
||||
supplyCoinsPost, _ := suite.keeper.GetSuppliedCoins(prevCtx)
|
||||
supplyCoinPostAmount := supplyCoinsPost.AmountOf(coinDenom)
|
||||
suite.Require().Equal(supplyCoinPostAmount, supplyCoinPriorAmount.Add(expectedSupplyInterest))
|
||||
|
||||
// Check current total reserves
|
||||
totalReserves, _ := suite.keeper.GetTotalReserves(snapshotCtx, coinDenom)
|
||||
suite.Require().Equal(expectedTotalReserves, totalReserves)
|
||||
|
||||
// Check that the supply index has increased as expected
|
||||
currSupplyIndexPrior, _ := suite.keeper.GetSupplyInterestFactor(snapshotCtx, coinDenom)
|
||||
suite.Require().Equal(expectedSupplyInterestFactor, currSupplyIndexPrior)
|
||||
|
||||
// // Check that the borrow index has increased as expected
|
||||
currBorrowIndexPrior, _ := suite.keeper.GetBorrowInterestFactor(snapshotCtx, coinDenom)
|
||||
suite.Require().Equal(expectedBorrowInterestFactor, currBorrowIndexPrior)
|
||||
|
||||
// After supplying again user's supplied balance should have owed supply interest applied
|
||||
if snapshot.shouldSupply {
|
||||
// Calculate percentage of supply interest profits owed to user
|
||||
userSupplyBefore, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
|
||||
userSupplyCoinAmount := userSupplyBefore.Amount.AmountOf(coinDenom)
|
||||
userPercentOfTotalSupplied := userSupplyCoinAmount.ToDec().Quo(supplyCoinPriorAmount.ToDec())
|
||||
userExpectedSupplyInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalSupplied.MulInt(expectedSupplyInterest).TruncateInt())
|
||||
|
||||
// Calculate percentage of borrow interest profits owed to user
|
||||
userBorrowBefore, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
|
||||
userBorrowCoinAmount := userBorrowBefore.Amount.AmountOf(coinDenom)
|
||||
userPercentOfTotalBorrowed := userBorrowCoinAmount.ToDec().Quo(borrowCoinPriorAmount.ToDec())
|
||||
userExpectedBorrowInterestCoin := sdk.NewCoin(coinDenom, userPercentOfTotalBorrowed.MulInt(expectedBorrowInterest).TruncateInt())
|
||||
expectedBorrowCoinsAfter := userBorrowBefore.Amount.Add(userExpectedBorrowInterestCoin)
|
||||
|
||||
// Supplying syncs user's owed supply and borrow interest
|
||||
err = suite.keeper.Deposit(snapshotCtx, tc.args.user, sdk.NewCoins(snapshot.supplyCoin))
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Fetch user's new borrow and supply balance post-interaction
|
||||
userSupplyAfter, _ := suite.keeper.GetDeposit(snapshotCtx, tc.args.user)
|
||||
userBorrowAfter, _ := suite.keeper.GetBorrow(snapshotCtx, tc.args.user)
|
||||
|
||||
// Confirm that user's supply index for the denom has increased as expected
|
||||
var userSupplyAfterIndexFactor sdk.Dec
|
||||
for _, indexFactor := range userSupplyAfter.Index {
|
||||
if indexFactor.Denom == coinDenom {
|
||||
userSupplyAfterIndexFactor = indexFactor.Value
|
||||
}
|
||||
}
|
||||
suite.Require().Equal(userSupplyAfterIndexFactor, currSupplyIndexPrior)
|
||||
|
||||
// Check user's supplied amount increased by supply interest owed + the newly supplied coins
|
||||
expectedSupplyCoinsAfter := userSupplyBefore.Amount.Add(snapshot.supplyCoin).Add(userExpectedSupplyInterestCoin)
|
||||
suite.Require().Equal(expectedSupplyCoinsAfter, userSupplyAfter.Amount)
|
||||
|
||||
// Confirm that user's borrow index for the denom has increased as expected
|
||||
var userBorrowAfterIndexFactor sdk.Dec
|
||||
for _, indexFactor := range userBorrowAfter.Index {
|
||||
if indexFactor.Denom == coinDenom {
|
||||
userBorrowAfterIndexFactor = indexFactor.Value
|
||||
}
|
||||
}
|
||||
suite.Require().Equal(userBorrowAfterIndexFactor, currBorrowIndexPrior)
|
||||
|
||||
// Check user's borrowed amount increased by borrow interest owed
|
||||
suite.Require().Equal(expectedBorrowCoinsAfter, userBorrowAfter.Amount)
|
||||
}
|
||||
prevCtx = snapshotCtx
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterestTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(InterestTestSuite))
|
||||
}
|
||||
|
@ -248,6 +248,29 @@ func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
||||
return borrowedCoins, true
|
||||
}
|
||||
|
||||
// SetSuppliedCoins sets the total amount of coins currently supplied in the store
|
||||
func (k Keeper) SetSuppliedCoins(ctx sdk.Context, suppliedCoins sdk.Coins) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
|
||||
if suppliedCoins.Empty() {
|
||||
store.Set([]byte{}, []byte{})
|
||||
} else {
|
||||
bz := k.cdc.MustMarshalBinaryBare(suppliedCoins)
|
||||
store.Set([]byte{}, bz)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSuppliedCoins returns an sdk.Coins object from the store representing all currently supplied coins
|
||||
func (k Keeper) GetSuppliedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.SuppliedCoinsPrefix)
|
||||
bz := store.Get([]byte{})
|
||||
if bz == nil {
|
||||
return sdk.Coins{}, false
|
||||
}
|
||||
var suppliedCoins sdk.Coins
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &suppliedCoins)
|
||||
return suppliedCoins, true
|
||||
}
|
||||
|
||||
// GetMoneyMarket returns a money market from the store for a denom
|
||||
func (k Keeper) GetMoneyMarket(ctx sdk.Context, denom string) (types.MoneyMarket, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.MoneyMarketsPrefix)
|
||||
@ -326,22 +349,41 @@ func (k Keeper) SetTotalReserves(ctx sdk.Context, denom string, coin sdk.Coin) {
|
||||
store.Set([]byte(denom), bz)
|
||||
}
|
||||
|
||||
// GetInterestFactor returns the current interest factor for an individual market
|
||||
func (k Keeper) GetInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
|
||||
// GetBorrowInterestFactor returns the current borrow interest factor for an individual market
|
||||
func (k Keeper) GetBorrowInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
|
||||
bz := store.Get([]byte(denom))
|
||||
if bz == nil {
|
||||
return sdk.ZeroDec(), false
|
||||
}
|
||||
var interestFactor sdk.Dec
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &interestFactor)
|
||||
return interestFactor, true
|
||||
var borrowInterestFactor sdk.Dec
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &borrowInterestFactor)
|
||||
return borrowInterestFactor, true
|
||||
}
|
||||
|
||||
// SetInterestFactor sets the current interest factor for an individual market
|
||||
func (k Keeper) SetInterestFactor(ctx sdk.Context, denom string, borrowIndex sdk.Dec) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.InterestFactorPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(borrowIndex)
|
||||
// SetBorrowInterestFactor sets the current borrow interest factor for an individual market
|
||||
func (k Keeper) SetBorrowInterestFactor(ctx sdk.Context, denom string, borrowInterestFactor sdk.Dec) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowInterestFactorPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(borrowInterestFactor)
|
||||
store.Set([]byte(denom), bz)
|
||||
}
|
||||
|
||||
// GetSupplyInterestFactor returns the current supply interest factor for an individual market
|
||||
func (k Keeper) GetSupplyInterestFactor(ctx sdk.Context, denom string) (sdk.Dec, bool) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
|
||||
bz := store.Get([]byte(denom))
|
||||
if bz == nil {
|
||||
return sdk.ZeroDec(), false
|
||||
}
|
||||
var supplyInterestFactor sdk.Dec
|
||||
k.cdc.MustUnmarshalBinaryBare(bz, &supplyInterestFactor)
|
||||
return supplyInterestFactor, true
|
||||
}
|
||||
|
||||
// SetSupplyInterestFactor sets the current supply interest factor for an individual market
|
||||
func (k Keeper) SetSupplyInterestFactor(ctx sdk.Context, denom string, supplyInterestFactor sdk.Dec) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.SupplyInterestFactorPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(supplyInterestFactor)
|
||||
store.Set([]byte(denom), bz)
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,8 @@ func (suite *KeeperTestSuite) TestGetSetPreviousDelegatorDistribution() {
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
types.SupplyInterestFactors{types.NewSupplyInterestFactor("", sdk.MustNewDecFromStr("0"))})
|
||||
|
||||
_, f := suite.keeper.GetDeposit(suite.ctx, sdk.AccAddress("test"))
|
||||
suite.Require().False(f)
|
||||
@ -96,7 +97,7 @@ func (suite *KeeperTestSuite) TestGetSetDeleteDeposit() {
|
||||
|
||||
func (suite *KeeperTestSuite) TestIterateDeposits() {
|
||||
for i := 0; i < 5; i++ {
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))))
|
||||
dep := types.NewDeposit(sdk.AccAddress("test"+fmt.Sprint(i)), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))), types.SupplyInterestFactors{})
|
||||
suite.Require().NotPanics(func() { suite.keeper.SetDeposit(suite.ctx, dep) })
|
||||
}
|
||||
var deposits []types.Deposit
|
||||
|
@ -39,7 +39,7 @@ func (k Keeper) AttemptKeeperLiquidation(ctx sdk.Context, keeper sdk.AccAddress,
|
||||
return false, err
|
||||
}
|
||||
|
||||
k.SyncOutstandingInterest(ctx, borrower)
|
||||
k.SyncBorrowInterest(ctx, borrower)
|
||||
|
||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldInsertIndex, borrower)
|
||||
|
||||
|
@ -15,8 +15,8 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
|
||||
return err
|
||||
}
|
||||
|
||||
// Sync interest so loan is up-to-date
|
||||
k.SyncOutstandingInterest(ctx, sender)
|
||||
// Sync borrow interest so loan is up-to-date
|
||||
k.SyncBorrowInterest(ctx, sender)
|
||||
|
||||
// Validate requested repay
|
||||
err = k.ValidateRepay(ctx, sender, coins)
|
||||
@ -30,7 +30,10 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
|
||||
return types.ErrBorrowNotFound
|
||||
}
|
||||
|
||||
payment := k.CalculatePaymentAmount(borrow.Amount, coins)
|
||||
payment, err := k.CalculatePaymentAmount(borrow.Amount, coins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sends coins from user to Hard module account
|
||||
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment)
|
||||
@ -73,8 +76,13 @@ func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.
|
||||
}
|
||||
|
||||
// CalculatePaymentAmount prevents overpayment when repaying borrowed coins
|
||||
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Coins {
|
||||
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) (sdk.Coins, error) {
|
||||
repayment := sdk.Coins{}
|
||||
|
||||
if !payment.DenomsSubsetOf(owed) {
|
||||
return repayment, types.ErrInvalidRepaymentDenom
|
||||
}
|
||||
|
||||
for _, coin := range payment {
|
||||
if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
|
||||
repayment = append(repayment, sdk.NewCoin(coin.Denom, owed.AmountOf(coin.Denom)))
|
||||
@ -82,5 +90,5 @@ func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Co
|
||||
repayment = append(repayment, coin)
|
||||
}
|
||||
}
|
||||
return repayment
|
||||
return repayment, nil
|
||||
}
|
||||
|
@ -86,6 +86,21 @@ func (suite *KeeperTestSuite) TestRepay() {
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid: attempt to repay non-supplied coin",
|
||||
args{
|
||||
borrower: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialBorrowerCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(100*KAVA_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("ukava", sdk.NewInt(100*KAVA_CF))},
|
||||
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
|
||||
repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(10*KAVA_CF)), sdk.NewCoin("bnb", sdk.NewInt(10*KAVA_CF))),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "account can only repay up to 0bnb",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid: insufficent balance for repay",
|
||||
args{
|
||||
@ -98,7 +113,7 @@ func (suite *KeeperTestSuite) TestRepay() {
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "account can only repay up to",
|
||||
contains: "account can only repay up to 50000000ukava",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -203,7 +218,8 @@ func (suite *KeeperTestSuite) TestRepay() {
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
// If we overpaid expect an adjustment
|
||||
repaymentCoins := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
|
||||
repaymentCoins, err := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Check borrower balance
|
||||
expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins)
|
||||
|
@ -84,7 +84,7 @@ func (suite *KeeperTestSuite) TestApplyDepositRewards() {
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, cs(tc.args.totalDeposits))
|
||||
keeper := tApp.GetHardKeeper()
|
||||
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount)
|
||||
deposit := types.NewDeposit(tc.args.depositor, tc.args.depositAmount, types.SupplyInterestFactors{})
|
||||
keeper.SetDeposit(ctx, deposit)
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
|
98
x/hard/keeper/withdraw.go
Normal file
98
x/hard/keeper/withdraw.go
Normal file
@ -0,0 +1,98 @@
|
||||
package keeper
|
||||
|
||||
import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||
|
||||
"github.com/kava-labs/kava/x/hard/types"
|
||||
)
|
||||
|
||||
// Withdraw returns some or all of a deposit back to original depositor
|
||||
func (k Keeper) Withdraw(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error {
|
||||
// Get current stored LTV based on stored borrows/deposits
|
||||
prevLtv, shouldRemoveIndex, err := k.GetStoreLTV(ctx, depositor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
k.SyncBorrowInterest(ctx, depositor)
|
||||
k.SyncSupplyInterest(ctx, depositor)
|
||||
|
||||
deposit, found := k.GetDeposit(ctx, depositor)
|
||||
if !found {
|
||||
return sdkerrors.Wrapf(types.ErrDepositNotFound, "no deposit found for %s", depositor)
|
||||
}
|
||||
|
||||
amount, err := k.CalculateWithdrawAmount(deposit.Amount, coins)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
proposedDeposit := types.NewDeposit(deposit.Depositor, deposit.Amount.Sub(amount), types.SupplyInterestFactors{})
|
||||
|
||||
borrow, found := k.GetBorrow(ctx, depositor)
|
||||
if !found {
|
||||
borrow = types.Borrow{}
|
||||
}
|
||||
|
||||
valid, err := k.IsWithinValidLtvRange(ctx, proposedDeposit, borrow)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return sdkerrors.Wrapf(types.ErrInvalidWithdrawAmount, "proposed withdraw outside loan-to-value range")
|
||||
}
|
||||
|
||||
err = k.supplyKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, depositor, amount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deposit.Amount.IsEqual(amount) {
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeDeleteHardDeposit,
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
),
|
||||
)
|
||||
k.DeleteDeposit(ctx, deposit)
|
||||
return nil
|
||||
}
|
||||
|
||||
deposit.Amount = deposit.Amount.Sub(amount)
|
||||
k.SetDeposit(ctx, deposit)
|
||||
|
||||
k.UpdateItemInLtvIndex(ctx, prevLtv, shouldRemoveIndex, depositor)
|
||||
|
||||
// Update total supplied amount
|
||||
k.DecrementBorrowedCoins(ctx, amount)
|
||||
|
||||
ctx.EventManager().EmitEvent(
|
||||
sdk.NewEvent(
|
||||
types.EventTypeHardWithdrawal,
|
||||
sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()),
|
||||
sdk.NewAttribute(types.AttributeKeyDepositor, depositor.String()),
|
||||
),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalculateWithdrawAmount enables full withdraw of deposited coins by adjusting withdraw amount
|
||||
// to equal total deposit amount if the requested withdraw amount > current deposit amount
|
||||
func (k Keeper) CalculateWithdrawAmount(available sdk.Coins, request sdk.Coins) (sdk.Coins, error) {
|
||||
result := sdk.Coins{}
|
||||
|
||||
if !request.DenomsSubsetOf(available) {
|
||||
return result, types.ErrInvalidWithdrawDenom
|
||||
}
|
||||
|
||||
for _, coin := range request {
|
||||
if coin.Amount.GT(available.AmountOf(coin.Denom)) {
|
||||
result = append(result, sdk.NewCoin(coin.Denom, available.AmountOf(coin.Denom)))
|
||||
} else {
|
||||
result = append(result, coin)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
390
x/hard/keeper/withdraw_test.go
Normal file
390
x/hard/keeper/withdraw_test.go
Normal file
@ -0,0 +1,390 @@
|
||||
package keeper_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
tmtime "github.com/tendermint/tendermint/types/time"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
"github.com/kava-labs/kava/x/hard"
|
||||
"github.com/kava-labs/kava/x/hard/types"
|
||||
"github.com/kava-labs/kava/x/pricefeed"
|
||||
)
|
||||
|
||||
func (suite *KeeperTestSuite) TestWithdraw() {
|
||||
type args struct {
|
||||
depositor sdk.AccAddress
|
||||
initialModAccountBalance sdk.Coins
|
||||
depositAmount sdk.Coins
|
||||
withdrawAmount sdk.Coins
|
||||
createDeposit bool
|
||||
expectedAccountBalance sdk.Coins
|
||||
expectedModAccountBalance sdk.Coins
|
||||
depositExists bool
|
||||
finalDepositAmount sdk.Coins
|
||||
}
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
type withdrawTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
testCases := []withdrawTest{
|
||||
{
|
||||
"valid: partial withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialModAccountBalance: sdk.Coins(nil),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(900)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
depositExists: true,
|
||||
finalDepositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(100))),
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid: full withdraw",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialModAccountBalance: sdk.Coins(nil),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.Coins(nil),
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid: withdraw exceeds deposit but is adjusted to match max deposit",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(300))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000))),
|
||||
expectedModAccountBalance: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000))),
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: true,
|
||||
contains: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid: withdraw non-supplied coin type",
|
||||
args{
|
||||
depositor: sdk.AccAddress(crypto.AddressHash([]byte("test"))),
|
||||
initialModAccountBalance: sdk.Coins(nil),
|
||||
depositAmount: sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(200))),
|
||||
withdrawAmount: sdk.NewCoins(sdk.NewCoin("btcb", sdk.NewInt(200))),
|
||||
createDeposit: true,
|
||||
expectedAccountBalance: sdk.Coins{},
|
||||
expectedModAccountBalance: sdk.Coins{},
|
||||
depositExists: false,
|
||||
finalDepositAmount: sdk.Coins{},
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "no coins of this type deposited",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// create new app with one funded account
|
||||
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
authGS := app.NewAuthGenState(
|
||||
[]sdk.AccAddress{tc.args.depositor},
|
||||
[]sdk.Coins{sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1000)), sdk.NewCoin("btcb", sdk.NewInt(1000)))},
|
||||
)
|
||||
|
||||
loanToValue := sdk.MustNewDecFromStr("0.6")
|
||||
hardGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "bnb", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
types.MoneyMarkets{
|
||||
types.NewMoneyMarket("usdx", types.NewBorrowLimit(false, sdk.NewDec(1000000000000000), loanToValue), "usdx:usd", sdk.NewInt(1000000), sdk.NewInt(USDX_CF*1000), 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), sdk.NewInt(KAVA_CF*1000), 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), sdk.NewInt(BNB_CF*1000), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05"), sdk.ZeroDec()),
|
||||
},
|
||||
0, // LTV counter
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
|
||||
// Pricefeed module genesis state
|
||||
pricefeedGS := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.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},
|
||||
{MarketID: "bnb:usd", BaseAsset: "bnb", QuoteAsset: "usd", Oracles: []sdk.AccAddress{}, Active: true},
|
||||
},
|
||||
},
|
||||
PostedPrices: []pricefeed.PostedPrice{
|
||||
{
|
||||
MarketID: "usdx:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("1.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "kava:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("2.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "bnb:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("10.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tApp.InitializeFromGenesisStates(authGS,
|
||||
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
|
||||
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(hardGS)})
|
||||
keeper := tApp.GetHardKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
|
||||
// Mint coins to Hard module account
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModAccountBalance)
|
||||
|
||||
if tc.args.createDeposit {
|
||||
err := suite.keeper.Deposit(suite.ctx, tc.args.depositor, tc.args.depositAmount)
|
||||
suite.Require().NoError(err)
|
||||
}
|
||||
|
||||
err := suite.keeper.Withdraw(suite.ctx, tc.args.depositor, tc.args.withdrawAmount)
|
||||
|
||||
if tc.errArgs.expectPass {
|
||||
suite.Require().NoError(err)
|
||||
acc := suite.getAccount(tc.args.depositor)
|
||||
suite.Require().Equal(tc.args.expectedAccountBalance, acc.GetCoins())
|
||||
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||
suite.Require().Equal(tc.args.expectedModAccountBalance, mAcc.GetCoins())
|
||||
testDeposit, f := suite.keeper.GetDeposit(suite.ctx, tc.args.depositor)
|
||||
if tc.args.depositExists {
|
||||
suite.Require().True(f)
|
||||
suite.Require().Equal(tc.args.finalDepositAmount, testDeposit.Amount)
|
||||
} else {
|
||||
suite.Require().False(f)
|
||||
}
|
||||
} else {
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *KeeperTestSuite) TestLtvWithdraw() {
|
||||
type args struct {
|
||||
borrower sdk.AccAddress
|
||||
initialModuleCoins sdk.Coins
|
||||
initialBorrowerCoins sdk.Coins
|
||||
depositCoins sdk.Coins
|
||||
borrowCoins sdk.Coins
|
||||
futureTime int64
|
||||
}
|
||||
|
||||
type errArgs struct {
|
||||
expectPass bool
|
||||
contains string
|
||||
}
|
||||
|
||||
type liqTest struct {
|
||||
name string
|
||||
args args
|
||||
errArgs errArgs
|
||||
}
|
||||
|
||||
// Set up test constants
|
||||
model := types.NewInterestRateModel(sdk.MustNewDecFromStr("0"), sdk.MustNewDecFromStr("0.1"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("0.5"))
|
||||
reserveFactor := sdk.MustNewDecFromStr("0.05")
|
||||
oneMonthInSeconds := int64(2592000)
|
||||
borrower := sdk.AccAddress(crypto.AddressHash([]byte("testborrower")))
|
||||
|
||||
testCases := []liqTest{
|
||||
{
|
||||
"invalid: withdraw is outside loan-to-value range",
|
||||
args{
|
||||
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
|
||||
futureTime: oneMonthInSeconds,
|
||||
},
|
||||
errArgs{
|
||||
expectPass: false,
|
||||
contains: "proposed withdraw outside loan-to-value range",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
suite.Run(tc.name, func() {
|
||||
// Initialize test app and set context
|
||||
tApp := app.NewTestApp()
|
||||
ctx := tApp.NewContext(true, abci.Header{Height: 1, Time: tmtime.Now()})
|
||||
|
||||
// Auth module genesis state
|
||||
authGS := app.NewAuthGenState(
|
||||
[]sdk.AccAddress{tc.args.borrower},
|
||||
[]sdk.Coins{tc.args.initialBorrowerCoins},
|
||||
)
|
||||
|
||||
// Harvest module genesis state
|
||||
harvestGS := types.NewGenesisState(types.NewParams(
|
||||
true,
|
||||
types.DistributionSchedules{
|
||||
types.NewDistributionSchedule(true, "ukava", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2020, 11, 22, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(5000)), time.Date(2021, 11, 22, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
},
|
||||
types.DelegatorDistributionSchedules{types.NewDelegatorDistributionSchedule(
|
||||
types.NewDistributionSchedule(true, "usdx", time.Date(2020, 10, 8, 14, 0, 0, 0, time.UTC), time.Date(2025, 10, 8, 14, 0, 0, 0, time.UTC), sdk.NewCoin("hard", sdk.NewInt(500)), time.Date(2026, 10, 8, 14, 0, 0, 0, time.UTC), types.Multipliers{types.NewMultiplier(types.Small, 0, sdk.MustNewDecFromStr("0.33")), types.NewMultiplier(types.Medium, 6, sdk.MustNewDecFromStr("0.5")), types.NewMultiplier(types.Medium, 24, sdk.OneDec())}),
|
||||
time.Hour*24,
|
||||
),
|
||||
},
|
||||
types.MoneyMarkets{
|
||||
types.NewMoneyMarket("ukava",
|
||||
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
|
||||
"kava:usd", // Market ID
|
||||
sdk.NewInt(KAVA_CF), // Conversion Factor
|
||||
sdk.NewInt(100000000*KAVA_CF), // Auction Size
|
||||
model, // Interest Rate Model
|
||||
reserveFactor, // Reserve Factor
|
||||
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
|
||||
types.NewMoneyMarket("usdx",
|
||||
types.NewBorrowLimit(false, sdk.NewDec(100000000*KAVA_CF), sdk.MustNewDecFromStr("0.8")), // Borrow Limit
|
||||
"usdx:usd", // Market ID
|
||||
sdk.NewInt(KAVA_CF), // Conversion Factor
|
||||
sdk.NewInt(100000000*KAVA_CF), // Auction Size
|
||||
model, // Interest Rate Model
|
||||
reserveFactor, // Reserve Factor
|
||||
sdk.MustNewDecFromStr("0.05")), // Keeper Reward Percent
|
||||
},
|
||||
0, // LTV counter
|
||||
), types.DefaultPreviousBlockTime, types.DefaultDistributionTimes)
|
||||
|
||||
// Pricefeed module genesis state
|
||||
pricefeedGS := pricefeed.GenesisState{
|
||||
Params: pricefeed.Params{
|
||||
Markets: []pricefeed.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: []pricefeed.PostedPrice{
|
||||
{
|
||||
MarketID: "usdx:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("1.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
{
|
||||
MarketID: "kava:usd",
|
||||
OracleAddress: sdk.AccAddress{},
|
||||
Price: sdk.MustNewDecFromStr("2.00"),
|
||||
Expiry: time.Now().Add(100 * time.Hour),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize test application
|
||||
tApp.InitializeFromGenesisStates(authGS,
|
||||
app.GenesisState{pricefeed.ModuleName: pricefeed.ModuleCdc.MustMarshalJSON(pricefeedGS)},
|
||||
app.GenesisState{types.ModuleName: types.ModuleCdc.MustMarshalJSON(harvestGS)})
|
||||
|
||||
// Mint coins to Harvest module account
|
||||
supplyKeeper := tApp.GetSupplyKeeper()
|
||||
supplyKeeper.MintCoins(ctx, types.ModuleAccountName, tc.args.initialModuleCoins)
|
||||
|
||||
auctionKeeper := tApp.GetAuctionKeeper()
|
||||
|
||||
keeper := tApp.GetHardKeeper()
|
||||
suite.app = tApp
|
||||
suite.ctx = ctx
|
||||
suite.keeper = keeper
|
||||
suite.auctionKeeper = auctionKeeper
|
||||
|
||||
var err error
|
||||
|
||||
// Run begin blocker to set up state
|
||||
hard.BeginBlocker(suite.ctx, suite.keeper)
|
||||
|
||||
// Borrower deposits coins
|
||||
err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, tc.args.depositCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Borrower borrows coins
|
||||
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Attempting to withdraw fails
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
|
||||
suite.Require().Error(err)
|
||||
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||
|
||||
// Set up future chain context and run begin blocker, increasing user's owed borrow balance
|
||||
runAtTime := time.Unix(suite.ctx.BlockTime().Unix()+(tc.args.futureTime), 0)
|
||||
liqCtx := suite.ctx.WithBlockTime(runAtTime)
|
||||
hard.BeginBlocker(liqCtx, suite.keeper)
|
||||
|
||||
// Attempted withdraw of 1 coin still fails
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, sdk.NewCoins(sdk.NewCoin("ukava", sdk.OneInt())))
|
||||
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.borrowCoins)
|
||||
suite.Require().NoError(err)
|
||||
|
||||
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, tc.args.depositCoins)
|
||||
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))))
|
||||
err = suite.keeper.Withdraw(suite.ctx, tc.args.borrower, withdrawCoins)
|
||||
suite.Require().NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
@ -27,7 +27,9 @@ func TestDecodeDistributionStore(t *testing.T) {
|
||||
cdc := makeTestCodec()
|
||||
|
||||
prevBlockTime := time.Now().UTC()
|
||||
deposit := types.NewDeposit(sdk.AccAddress("test"), sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))))
|
||||
deposit := types.NewDeposit(sdk.AccAddress("test"),
|
||||
sdk.NewCoins(sdk.NewCoin("bnb", sdk.NewInt(1))),
|
||||
types.SupplyInterestFactors{types.NewSupplyInterestFactor("bnb", sdk.OneDec())})
|
||||
claim := types.NewClaim(sdk.AccAddress("test"), "bnb", sdk.NewCoin("hard", sdk.NewInt(100)), "stake")
|
||||
|
||||
kvPairs := kv.Pairs{
|
||||
|
@ -4,32 +4,15 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// BorrowIndexItem defines an individual borrow index
|
||||
type BorrowIndexItem struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
Value sdk.Dec `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// NewBorrowIndexItem returns a new BorrowIndexItem instance
|
||||
func NewBorrowIndexItem(denom string, value sdk.Dec) BorrowIndexItem {
|
||||
return BorrowIndexItem{
|
||||
Denom: denom,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// BorrowIndexes is a slice of BorrowIndexItem, because Amino won't marshal maps
|
||||
type BorrowIndexes []BorrowIndexItem
|
||||
|
||||
// Borrow defines an amount of coins borrowed from a hard module account
|
||||
type Borrow struct {
|
||||
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
||||
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
||||
Index InterestFactors `json:"index" yaml:"index"`
|
||||
Index BorrowInterestFactors `json:"index" yaml:"index"`
|
||||
}
|
||||
|
||||
// NewBorrow returns a new Borrow instance
|
||||
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors) Borrow {
|
||||
func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index BorrowInterestFactors) Borrow {
|
||||
return Borrow{
|
||||
Borrower: borrower,
|
||||
Amount: amount,
|
||||
@ -37,19 +20,19 @@ func NewBorrow(borrower sdk.AccAddress, amount sdk.Coins, index InterestFactors)
|
||||
}
|
||||
}
|
||||
|
||||
// InterestFactor defines an individual interest factor
|
||||
type InterestFactor struct {
|
||||
// BorrowInterestFactor defines an individual borrow interest factor
|
||||
type BorrowInterestFactor struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
Value sdk.Dec `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// NewInterestFactor returns a new InterestFactor instance
|
||||
func NewInterestFactor(denom string, value sdk.Dec) InterestFactor {
|
||||
return InterestFactor{
|
||||
// NewBorrowInterestFactor returns a new BorrowInterestFactor instance
|
||||
func NewBorrowInterestFactor(denom string, value sdk.Dec) BorrowInterestFactor {
|
||||
return BorrowInterestFactor{
|
||||
Denom: denom,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// InterestFactors is a slice of InterestFactor, because Amino won't marshal maps
|
||||
type InterestFactors []InterestFactor
|
||||
// BorrowInterestFactors is a slice of BorrowInterestFactor, because Amino won't marshal maps
|
||||
type BorrowInterestFactors []BorrowInterestFactor
|
||||
|
@ -8,12 +8,31 @@ import (
|
||||
type Deposit struct {
|
||||
Depositor sdk.AccAddress `json:"depositor" yaml:"depositor"`
|
||||
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
||||
Index SupplyInterestFactors `json:"index" yaml:"index"`
|
||||
}
|
||||
|
||||
// NewDeposit returns a new deposit
|
||||
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins) Deposit {
|
||||
func NewDeposit(depositor sdk.AccAddress, amount sdk.Coins, indexes SupplyInterestFactors) Deposit {
|
||||
return Deposit{
|
||||
Depositor: depositor,
|
||||
Amount: amount,
|
||||
Index: indexes,
|
||||
}
|
||||
}
|
||||
|
||||
// SupplyInterestFactor defines an individual borrow interest factor
|
||||
type SupplyInterestFactor struct {
|
||||
Denom string `json:"denom" yaml:"denom"`
|
||||
Value sdk.Dec `json:"value" yaml:"value"`
|
||||
}
|
||||
|
||||
// NewSupplyInterestFactor returns a new SupplyInterestFactor instance
|
||||
func NewSupplyInterestFactor(denom string, value sdk.Dec) SupplyInterestFactor {
|
||||
return SupplyInterestFactor{
|
||||
Denom: denom,
|
||||
Value: value,
|
||||
}
|
||||
}
|
||||
|
||||
// SupplyInterestFactors is a slice of SupplyInterestFactor, because Amino won't marshal maps
|
||||
type SupplyInterestFactors []SupplyInterestFactor
|
||||
|
@ -67,4 +67,12 @@ var (
|
||||
ErrInsufficientCoins = sdkerrors.Register(ModuleName, 30, "unrecoverable state - insufficient coins")
|
||||
// ErrInsufficientBalanceForBorrow error for when the requested borrow exceeds user's balance
|
||||
ErrInsufficientBalanceForBorrow = sdkerrors.Register(ModuleName, 31, "insufficient balance")
|
||||
// ErrSuppliedCoinsNotFound error for when the total amount of supplied coins cannot be found
|
||||
ErrSuppliedCoinsNotFound = sdkerrors.Register(ModuleName, 32, "no supplied coins found")
|
||||
// ErrNegativeSuppliedCoins error for when substracting coins from the total supplied balance results in a negative amount
|
||||
ErrNegativeSuppliedCoins = sdkerrors.Register(ModuleName, 33, "subtraction results in negative supplied amount")
|
||||
// ErrInvalidWithdrawDenom error for when user attempts to withdraw a non-supplied coin type
|
||||
ErrInvalidWithdrawDenom = sdkerrors.Register(ModuleName, 34, "no coins of this type deposited")
|
||||
// ErrInvalidRepaymentDenom error for when user attempts to repay a non-borrowed coin type
|
||||
ErrInvalidRepaymentDenom = sdkerrors.Register(ModuleName, 35, "no coins of this type borrowed")
|
||||
)
|
||||
|
@ -40,11 +40,13 @@ var (
|
||||
ClaimsKeyPrefix = []byte{0x04}
|
||||
BorrowsKeyPrefix = []byte{0x05}
|
||||
BorrowedCoinsPrefix = []byte{0x06}
|
||||
MoneyMarketsPrefix = []byte{0x07}
|
||||
PreviousAccrualTimePrefix = []byte{0x08} // denom -> time
|
||||
TotalReservesPrefix = []byte{0x09} // denom -> sdk.Coin
|
||||
InterestFactorPrefix = []byte{0x10} // denom -> sdk.Dec
|
||||
LtvIndexPrefix = []byte{0x11}
|
||||
SuppliedCoinsPrefix = []byte{0x07}
|
||||
MoneyMarketsPrefix = []byte{0x08}
|
||||
PreviousAccrualTimePrefix = []byte{0x09} // denom -> time
|
||||
TotalReservesPrefix = []byte{0x10} // denom -> sdk.Coin
|
||||
BorrowInterestFactorPrefix = []byte{0x11} // denom -> sdk.Dec
|
||||
SupplyInterestFactorPrefix = []byte{0x12} // denom -> sdk.Dec
|
||||
LtvIndexPrefix = []byte{0x13}
|
||||
sep = []byte(":")
|
||||
)
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user