mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 08:15:19 +00:00
Hard: repay borrowed coins (#725)
* add msg borrow test * repay types * register msg repay on codec * repay keeper functionality * repay cli * repay keeper test * Hard: repay insufficient balance error (#726) * repay error: insufficient balance * isolate coin type in error msg * add multi-coin repay example * CalculatePaymentAmount, repay > SyncBorrowInterest * remove todo: index updated by sync * update tests * add back in test
This commit is contained in:
parent
4dd174ea85
commit
4e641c5212
@ -34,6 +34,7 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
|
|||||||
getCmdWithdraw(cdc),
|
getCmdWithdraw(cdc),
|
||||||
getCmdClaimReward(cdc),
|
getCmdClaimReward(cdc),
|
||||||
getCmdBorrow(cdc),
|
getCmdBorrow(cdc),
|
||||||
|
getCmdRepay(cdc),
|
||||||
)...)
|
)...)
|
||||||
|
|
||||||
return harvestTxCmd
|
return harvestTxCmd
|
||||||
@ -144,3 +145,31 @@ func getCmdBorrow(cdc *codec.Codec) *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getCmdRepay(cdc *codec.Codec) *cobra.Command {
|
||||||
|
return &cobra.Command{
|
||||||
|
Use: "repay [1000000000ukava]",
|
||||||
|
Short: "repay tokens to the harvest protocol",
|
||||||
|
Long: strings.TrimSpace(`repay tokens to the harvest protocol`),
|
||||||
|
Args: cobra.ExactArgs(1),
|
||||||
|
Example: fmt.Sprintf(
|
||||||
|
`%s tx %s repay 1000000000ukava,25000000000bnb --from <key>`, version.ClientName, types.ModuleName,
|
||||||
|
),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
inBuf := bufio.NewReader(cmd.InOrStdin())
|
||||||
|
cliCtx := context.NewCLIContext().WithCodec(cdc)
|
||||||
|
txBldr := auth.NewTxBuilderFromCLI(inBuf).WithTxEncoder(utils.GetTxEncoder(cdc))
|
||||||
|
|
||||||
|
coins, err := sdk.ParseCoins(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := types.NewMsgRepay(cliCtx.GetFromAddress(), coins)
|
||||||
|
if err := msg.ValidateBasic(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return utils.GenerateOrBroadcastMsgs(cliCtx, txBldr, []sdk.Msg{msg})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -23,6 +23,8 @@ func NewHandler(k Keeper) sdk.Handler {
|
|||||||
return handleMsgWithdraw(ctx, k, msg)
|
return handleMsgWithdraw(ctx, k, msg)
|
||||||
case types.MsgBorrow:
|
case types.MsgBorrow:
|
||||||
return handleMsgBorrow(ctx, k, msg)
|
return handleMsgBorrow(ctx, k, msg)
|
||||||
|
case types.MsgRepay:
|
||||||
|
return handleMsgRepay(ctx, k, msg)
|
||||||
default:
|
default:
|
||||||
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
return nil, sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized %s message type: %T", ModuleName, msg)
|
||||||
}
|
}
|
||||||
@ -100,3 +102,21 @@ func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sd
|
|||||||
Events: ctx.EventManager().Events(),
|
Events: ctx.EventManager().Events(),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleMsgRepay(ctx sdk.Context, k keeper.Keeper, msg types.MsgRepay) (*sdk.Result, error) {
|
||||||
|
err := k.Repay(ctx, msg.Sender, msg.Amount)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
sdk.EventTypeMessage,
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory),
|
||||||
|
sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return &sdk.Result{
|
||||||
|
Events: ctx.EventManager().Events(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
@ -240,9 +240,13 @@ func (k Keeper) IterateBorrows(ctx sdk.Context, cb func(borrow types.Borrow) (st
|
|||||||
// SetBorrowedCoins sets the total amount of coins currently borrowed in the store
|
// SetBorrowedCoins sets the total amount of coins currently borrowed in the store
|
||||||
func (k Keeper) SetBorrowedCoins(ctx sdk.Context, borrowedCoins sdk.Coins) {
|
func (k Keeper) SetBorrowedCoins(ctx sdk.Context, borrowedCoins sdk.Coins) {
|
||||||
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowedCoinsPrefix)
|
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowedCoinsPrefix)
|
||||||
|
if borrowedCoins.Empty() {
|
||||||
|
store.Set([]byte{}, []byte{})
|
||||||
|
} else {
|
||||||
bz := k.cdc.MustMarshalBinaryBare(borrowedCoins)
|
bz := k.cdc.MustMarshalBinaryBare(borrowedCoins)
|
||||||
store.Set([]byte{}, bz)
|
store.Set([]byte{}, bz)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetBorrowedCoins returns an sdk.Coins object from the store representing all currently borrowed coins
|
// GetBorrowedCoins returns an sdk.Coins object from the store representing all currently borrowed coins
|
||||||
func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
func (k Keeper) GetBorrowedCoins(ctx sdk.Context) (sdk.Coins, bool) {
|
||||||
|
77
x/harvest/keeper/repay.go
Normal file
77
x/harvest/keeper/repay.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package keeper
|
||||||
|
|
||||||
|
import (
|
||||||
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||||
|
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
|
||||||
|
|
||||||
|
"github.com/kava-labs/kava/x/harvest/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Repay borrowed funds
|
||||||
|
func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error {
|
||||||
|
// Sync interest so loan is up-to-date
|
||||||
|
k.SyncBorrowInterest(ctx, sender, coins)
|
||||||
|
|
||||||
|
// Validate requested repay
|
||||||
|
err := k.ValidateRepay(ctx, sender, coins)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check borrow exists here to avoid duplicating store read in ValidateRepay
|
||||||
|
borrow, found := k.GetBorrow(ctx, sender)
|
||||||
|
if !found {
|
||||||
|
return types.ErrBorrowNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
payment := k.CalculatePaymentAmount(borrow.Amount, coins)
|
||||||
|
|
||||||
|
// Sends coins from user to Harvest module account
|
||||||
|
err = k.supplyKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, payment)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update user's borrow in store
|
||||||
|
borrow.Amount = borrow.Amount.Sub(payment)
|
||||||
|
k.SetBorrow(ctx, borrow)
|
||||||
|
|
||||||
|
// Update total borrowed amount
|
||||||
|
k.DecrementBorrowedCoins(ctx, payment)
|
||||||
|
|
||||||
|
ctx.EventManager().EmitEvent(
|
||||||
|
sdk.NewEvent(
|
||||||
|
types.EventTypeHarvestRepay,
|
||||||
|
sdk.NewAttribute(types.AttributeKeySender, sender.String()),
|
||||||
|
sdk.NewAttribute(types.AttributeKeyRepayCoins, payment.String()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRepay validates a requested loan repay
|
||||||
|
func (k Keeper) ValidateRepay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error {
|
||||||
|
senderAcc := k.accountKeeper.GetAccount(ctx, sender)
|
||||||
|
senderCoins := senderAcc.GetCoins()
|
||||||
|
for _, coin := range coins {
|
||||||
|
if senderCoins.AmountOf(coin.Denom).LT(coin.Amount) {
|
||||||
|
return sdkerrors.Wrapf(types.ErrInsufficientBalanceForRepay, "account can only repay up to %s%s", senderCoins.AmountOf(coin.Denom), coin.Denom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CalculatePaymentAmount prevents overpayment when repaying borrowed coins
|
||||||
|
func (k Keeper) CalculatePaymentAmount(owed sdk.Coins, payment sdk.Coins) sdk.Coins {
|
||||||
|
repayment := sdk.Coins{}
|
||||||
|
for _, coin := range payment {
|
||||||
|
if coin.Amount.GT(owed.AmountOf(coin.Denom)) {
|
||||||
|
repayment = append(repayment, sdk.NewCoin(coin.Denom, owed.AmountOf(coin.Denom)))
|
||||||
|
} else {
|
||||||
|
repayment = append(repayment, coin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return repayment
|
||||||
|
}
|
225
x/harvest/keeper/repay_test.go
Normal file
225
x/harvest/keeper/repay_test.go
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
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/harvest/types"
|
||||||
|
"github.com/kava-labs/kava/x/pricefeed"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (suite *KeeperTestSuite) TestRepay() {
|
||||||
|
type args struct {
|
||||||
|
borrower sdk.AccAddress
|
||||||
|
initialBorrowerCoins sdk.Coins
|
||||||
|
initialModuleCoins sdk.Coins
|
||||||
|
depositCoins []sdk.Coin
|
||||||
|
borrowCoins sdk.Coins
|
||||||
|
repayCoins sdk.Coins
|
||||||
|
expectedAccountBalance sdk.Coins
|
||||||
|
expectedModAccountBalance sdk.Coins
|
||||||
|
}
|
||||||
|
|
||||||
|
type errArgs struct {
|
||||||
|
expectPass bool
|
||||||
|
contains string
|
||||||
|
}
|
||||||
|
|
||||||
|
type borrowTest struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
errArgs errArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []borrowTest{
|
||||||
|
{
|
||||||
|
"valid: partial repay",
|
||||||
|
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))),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid: repay in full",
|
||||||
|
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(50*KAVA_CF))),
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"valid: overpayment is adjusted",
|
||||||
|
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(80*KAVA_CF))}, // Deposit less so user still has some KAVA
|
||||||
|
borrowCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(50*KAVA_CF))),
|
||||||
|
repayCoins: sdk.NewCoins(sdk.NewCoin("ukava", sdk.NewInt(60*KAVA_CF))), // Exceeds borrowed coins but not user's balance
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: true,
|
||||||
|
contains: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"invalid: insufficent balance for repay",
|
||||||
|
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(51*KAVA_CF))), // Exceeds user's KAVA balance
|
||||||
|
},
|
||||||
|
errArgs{
|
||||||
|
expectPass: false,
|
||||||
|
contains: "account can only repay up to",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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, "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.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("usdx", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("1")), "usdx:usd", sdk.NewInt(USDX_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
|
||||||
|
types.NewMoneyMarket("ukava", types.NewBorrowLimit(false, sdk.NewDec(100000000*USDX_CF), sdk.MustNewDecFromStr("0.8")), "kava:usd", sdk.NewInt(KAVA_CF), types.NewInterestRateModel(sdk.MustNewDecFromStr("0.05"), sdk.MustNewDecFromStr("2"), sdk.MustNewDecFromStr("0.8"), sdk.MustNewDecFromStr("10")), sdk.MustNewDecFromStr("0.05")),
|
||||||
|
},
|
||||||
|
), 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(1 * time.Hour),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MarketID: "kava:usd",
|
||||||
|
OracleAddress: sdk.AccAddress{},
|
||||||
|
Price: sdk.MustNewDecFromStr("2.00"),
|
||||||
|
Expiry: time.Now().Add(1 * time.Hour),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize test application
|
||||||
|
tApp.InitializeFromGenesisStates(authGS,
|
||||||
|
app.GenesisState{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)
|
||||||
|
|
||||||
|
keeper := tApp.GetHarvestKeeper()
|
||||||
|
suite.app = tApp
|
||||||
|
suite.ctx = ctx
|
||||||
|
suite.keeper = keeper
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Deposit coins to harvest
|
||||||
|
depositedCoins := sdk.NewCoins()
|
||||||
|
for _, depositCoin := range tc.args.depositCoins {
|
||||||
|
err = suite.keeper.Deposit(suite.ctx, tc.args.borrower, depositCoin)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
depositedCoins.Add(depositCoin)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Borrow coins from harvest
|
||||||
|
err = suite.keeper.Borrow(suite.ctx, tc.args.borrower, tc.args.borrowCoins)
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
|
||||||
|
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.repayCoins)
|
||||||
|
if tc.errArgs.expectPass {
|
||||||
|
suite.Require().NoError(err)
|
||||||
|
// If we overpaid expect an adjustment
|
||||||
|
repaymentCoins := suite.keeper.CalculatePaymentAmount(tc.args.borrowCoins, tc.args.repayCoins)
|
||||||
|
|
||||||
|
// Check borrower balance
|
||||||
|
expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...).Sub(repaymentCoins)
|
||||||
|
acc := suite.getAccount(tc.args.borrower)
|
||||||
|
suite.Require().Equal(expectedBorrowerCoins, acc.GetCoins())
|
||||||
|
|
||||||
|
// Check module account balance
|
||||||
|
expectedModuleCoins := tc.args.initialModuleCoins.Add(tc.args.depositCoins...).Sub(tc.args.borrowCoins).Add(repaymentCoins...)
|
||||||
|
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||||
|
suite.Require().Equal(expectedModuleCoins, mAcc.GetCoins())
|
||||||
|
|
||||||
|
// Check user's borrow object
|
||||||
|
borrow, _ := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower)
|
||||||
|
expectedBorrowCoins := tc.args.borrowCoins.Sub(repaymentCoins)
|
||||||
|
suite.Require().Equal(expectedBorrowCoins, borrow.Amount)
|
||||||
|
} else {
|
||||||
|
suite.Require().Error(err)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.errArgs.contains))
|
||||||
|
|
||||||
|
// Check borrower balance (no repay coins)
|
||||||
|
expectedBorrowerCoins := tc.args.initialBorrowerCoins.Sub(tc.args.depositCoins).Add(tc.args.borrowCoins...)
|
||||||
|
acc := suite.getAccount(tc.args.borrower)
|
||||||
|
suite.Require().Equal(expectedBorrowerCoins, acc.GetCoins())
|
||||||
|
|
||||||
|
// Check module account balance (no repay coins)
|
||||||
|
expectedModuleCoins := tc.args.initialModuleCoins.Add(tc.args.depositCoins...).Sub(tc.args.borrowCoins)
|
||||||
|
mAcc := suite.getModuleAccount(types.ModuleAccountName)
|
||||||
|
suite.Require().Equal(expectedModuleCoins, mAcc.GetCoins())
|
||||||
|
|
||||||
|
// Check user's borrow object (no repay coins)
|
||||||
|
borrow, _ := suite.keeper.GetBorrow(suite.ctx, tc.args.borrower)
|
||||||
|
suite.Require().Equal(tc.args.borrowCoins, borrow.Amount)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -18,5 +18,6 @@ func RegisterCodec(cdc *codec.Codec) {
|
|||||||
cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil)
|
cdc.RegisterConcrete(MsgDeposit{}, "harvest/MsgDeposit", nil)
|
||||||
cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil)
|
cdc.RegisterConcrete(MsgWithdraw{}, "harvest/MsgWithdraw", nil)
|
||||||
cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil)
|
cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil)
|
||||||
|
cdc.RegisterConcrete(MsgRepay{}, "harvest/MsgRepay", nil)
|
||||||
cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil)
|
cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil)
|
||||||
}
|
}
|
||||||
|
@ -55,8 +55,10 @@ var (
|
|||||||
ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation")
|
ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation")
|
||||||
// ErrBorrowEmptyCoins error for when you cannot borrow empty coins
|
// ErrBorrowEmptyCoins error for when you cannot borrow empty coins
|
||||||
ErrBorrowEmptyCoins = sdkerrors.Register(ModuleName, 25, "cannot borrow zero coins")
|
ErrBorrowEmptyCoins = sdkerrors.Register(ModuleName, 25, "cannot borrow zero coins")
|
||||||
|
// ErrBorrowNotFound error for when a user's borrow is not found in the store
|
||||||
|
ErrBorrowNotFound = sdkerrors.Register(ModuleName, 26, "borrow not found")
|
||||||
// ErrPreviousAccrualTimeNotFound error for no previous accrual time found in store
|
// ErrPreviousAccrualTimeNotFound error for no previous accrual time found in store
|
||||||
ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 26, "no previous accrual time found")
|
ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 27, "no previous accrual time found")
|
||||||
// ErrBorrowNotFound error for when borrow not found in store
|
// ErrInsufficientBalanceForRepay error for when requested repay exceeds user's balance
|
||||||
ErrBorrowNotFound = sdkerrors.Register(ModuleName, 27, "no borrow found")
|
ErrInsufficientBalanceForRepay = sdkerrors.Register(ModuleName, 29, "insufficient balance")
|
||||||
)
|
)
|
||||||
|
@ -9,6 +9,7 @@ const (
|
|||||||
EventTypeHarvestWithdrawal = "harvest_withdrawal"
|
EventTypeHarvestWithdrawal = "harvest_withdrawal"
|
||||||
EventTypeClaimHarvestReward = "claim_harvest_reward"
|
EventTypeClaimHarvestReward = "claim_harvest_reward"
|
||||||
EventTypeHarvestBorrow = "harvest_borrow"
|
EventTypeHarvestBorrow = "harvest_borrow"
|
||||||
|
EventTypeHarvestRepay = "harvest_repay"
|
||||||
AttributeValueCategory = ModuleName
|
AttributeValueCategory = ModuleName
|
||||||
AttributeKeyBlockHeight = "block_height"
|
AttributeKeyBlockHeight = "block_height"
|
||||||
AttributeKeyRewardsDistribution = "rewards_distributed"
|
AttributeKeyRewardsDistribution = "rewards_distributed"
|
||||||
@ -22,4 +23,6 @@ const (
|
|||||||
AttributeKeyBorrow = "borrow"
|
AttributeKeyBorrow = "borrow"
|
||||||
AttributeKeyBorrower = "borrower"
|
AttributeKeyBorrower = "borrower"
|
||||||
AttributeKeyBorrowCoins = "borrow_coins"
|
AttributeKeyBorrowCoins = "borrow_coins"
|
||||||
|
AttributeKeySender = "sender"
|
||||||
|
AttributeKeyRepayCoins = "repay_coins"
|
||||||
)
|
)
|
||||||
|
@ -209,8 +209,6 @@ func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
|
|||||||
return []sdk.AccAddress{msg.Sender}
|
return []sdk.AccAddress{msg.Sender}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------
|
|
||||||
|
|
||||||
// MsgBorrow borrows funds from the harvest module.
|
// MsgBorrow borrows funds from the harvest module.
|
||||||
type MsgBorrow struct {
|
type MsgBorrow struct {
|
||||||
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
Borrower sdk.AccAddress `json:"borrower" yaml:"borrower"`
|
||||||
@ -229,7 +227,7 @@ func NewMsgBorrow(borrower sdk.AccAddress, amount sdk.Coins) MsgBorrow {
|
|||||||
func (msg MsgBorrow) Route() string { return RouterKey }
|
func (msg MsgBorrow) Route() string { return RouterKey }
|
||||||
|
|
||||||
// Type returns a human-readable string for the message, intended for utilization within tags.
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
func (msg MsgBorrow) Type() string { return "harvest_borrow" } // TODO: or just 'borrow'
|
func (msg MsgBorrow) Type() string { return "harvest_borrow" }
|
||||||
|
|
||||||
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
func (msg MsgBorrow) ValidateBasic() error {
|
func (msg MsgBorrow) ValidateBasic() error {
|
||||||
@ -260,3 +258,53 @@ func (msg MsgBorrow) String() string {
|
|||||||
Amount: %s
|
Amount: %s
|
||||||
`, msg.Borrower, msg.Amount)
|
`, msg.Borrower, msg.Amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MsgRepay repays funds to the harvest module.
|
||||||
|
type MsgRepay struct {
|
||||||
|
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
|
||||||
|
Amount sdk.Coins `json:"amount" yaml:"amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMsgRepay returns a new MsgRepay
|
||||||
|
func NewMsgRepay(sender sdk.AccAddress, amount sdk.Coins) MsgRepay {
|
||||||
|
return MsgRepay{
|
||||||
|
Sender: sender,
|
||||||
|
Amount: amount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route return the message type used for routing the message.
|
||||||
|
func (msg MsgRepay) Route() string { return RouterKey }
|
||||||
|
|
||||||
|
// Type returns a human-readable string for the message, intended for utilization within tags.
|
||||||
|
func (msg MsgRepay) Type() string { return "harvest_repay" }
|
||||||
|
|
||||||
|
// ValidateBasic does a simple validation check that doesn't require access to any other information.
|
||||||
|
func (msg MsgRepay) ValidateBasic() error {
|
||||||
|
if msg.Sender.Empty() {
|
||||||
|
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
|
||||||
|
}
|
||||||
|
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
|
||||||
|
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "repay amount %s", msg.Amount)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignBytes gets the canonical byte representation of the Msg.
|
||||||
|
func (msg MsgRepay) GetSignBytes() []byte {
|
||||||
|
bz := ModuleCdc.MustMarshalJSON(msg)
|
||||||
|
return sdk.MustSortJSON(bz)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSigners returns the addresses of signers that must sign.
|
||||||
|
func (msg MsgRepay) GetSigners() []sdk.AccAddress {
|
||||||
|
return []sdk.AccAddress{msg.Sender}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String implements the Stringer interface
|
||||||
|
func (msg MsgRepay) String() string {
|
||||||
|
return fmt.Sprintf(`Repay Message:
|
||||||
|
Sender: %s
|
||||||
|
Amount: %s
|
||||||
|
`, msg.Sender, msg.Amount)
|
||||||
|
}
|
||||||
|
@ -192,6 +192,82 @@ func (suite *MsgTestSuite) TestMsgClaim() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *MsgTestSuite) TestMsgBorrow() {
|
||||||
|
type args struct {
|
||||||
|
borrower sdk.AccAddress
|
||||||
|
amount sdk.Coins
|
||||||
|
}
|
||||||
|
addrs := []sdk.AccAddress{
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectPass bool
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
args: args{
|
||||||
|
borrower: addrs[0],
|
||||||
|
amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))),
|
||||||
|
},
|
||||||
|
expectPass: true,
|
||||||
|
expectedErr: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
msg := types.NewMsgBorrow(tc.args.borrower, tc.args.amount)
|
||||||
|
err := msg.ValidateBasic()
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.NoError(err)
|
||||||
|
} else {
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *MsgTestSuite) TestMsgRepay() {
|
||||||
|
type args struct {
|
||||||
|
sender sdk.AccAddress
|
||||||
|
amount sdk.Coins
|
||||||
|
}
|
||||||
|
addrs := []sdk.AccAddress{
|
||||||
|
sdk.AccAddress("test1"),
|
||||||
|
}
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expectPass bool
|
||||||
|
expectedErr string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid",
|
||||||
|
args: args{
|
||||||
|
sender: addrs[0],
|
||||||
|
amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))),
|
||||||
|
},
|
||||||
|
expectPass: true,
|
||||||
|
expectedErr: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testCases {
|
||||||
|
suite.Run(tc.name, func() {
|
||||||
|
msg := types.NewMsgRepay(tc.args.sender, tc.args.amount)
|
||||||
|
err := msg.ValidateBasic()
|
||||||
|
if tc.expectPass {
|
||||||
|
suite.NoError(err)
|
||||||
|
} else {
|
||||||
|
suite.Error(err)
|
||||||
|
suite.Require().True(strings.Contains(err.Error(), tc.expectedErr))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsgTestSuite(t *testing.T) {
|
func TestMsgTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(MsgTestSuite))
|
suite.Run(t, new(MsgTestSuite))
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user