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:
Denali Marsh 2020-12-07 22:51:35 +01:00 committed by GitHub
parent 4dd174ea85
commit 4e641c5212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 493 additions and 8 deletions

View File

@ -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})
},
}
}

View File

@ -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
}

View File

@ -240,8 +240,12 @@ 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

77
x/harvest/keeper/repay.go Normal file
View 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
}

View 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)
}
})
}
}

View File

@ -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)
} }

View File

@ -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")
) )

View File

@ -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"
) )

View File

@ -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)
}

View File

@ -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))
} }