mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-26 00:05:18 +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),
|
||||
getCmdClaimReward(cdc),
|
||||
getCmdBorrow(cdc),
|
||||
getCmdRepay(cdc),
|
||||
)...)
|
||||
|
||||
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)
|
||||
case types.MsgBorrow:
|
||||
return handleMsgBorrow(ctx, k, msg)
|
||||
case types.MsgRepay:
|
||||
return handleMsgRepay(ctx, k, msg)
|
||||
default:
|
||||
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(),
|
||||
}, 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,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
|
||||
func (k Keeper) SetBorrowedCoins(ctx sdk.Context, borrowedCoins sdk.Coins) {
|
||||
store := prefix.NewStore(ctx.KVStore(k.key), types.BorrowedCoinsPrefix)
|
||||
bz := k.cdc.MustMarshalBinaryBare(borrowedCoins)
|
||||
store.Set([]byte{}, bz)
|
||||
if borrowedCoins.Empty() {
|
||||
store.Set([]byte{}, []byte{})
|
||||
} else {
|
||||
bz := k.cdc.MustMarshalBinaryBare(borrowedCoins)
|
||||
store.Set([]byte{}, bz)
|
||||
}
|
||||
}
|
||||
|
||||
// GetBorrowedCoins returns an sdk.Coins object from the store representing all currently borrowed coins
|
||||
|
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(MsgWithdraw{}, "harvest/MsgWithdraw", nil)
|
||||
cdc.RegisterConcrete(MsgBorrow{}, "harvest/MsgBorrow", nil)
|
||||
cdc.RegisterConcrete(MsgRepay{}, "harvest/MsgRepay", nil)
|
||||
cdc.RegisterConcrete(DistributionSchedule{}, "harvest/DistributionSchedule", nil)
|
||||
}
|
||||
|
@ -55,8 +55,10 @@ var (
|
||||
ErrGreaterThanAssetBorrowLimit = sdkerrors.Register(ModuleName, 24, "fails global asset borrow limit validation")
|
||||
// ErrBorrowEmptyCoins error for when you cannot borrow empty 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 = sdkerrors.Register(ModuleName, 26, "no previous accrual time found")
|
||||
// ErrBorrowNotFound error for when borrow not found in store
|
||||
ErrBorrowNotFound = sdkerrors.Register(ModuleName, 27, "no borrow found")
|
||||
ErrPreviousAccrualTimeNotFound = sdkerrors.Register(ModuleName, 27, "no previous accrual time found")
|
||||
// ErrInsufficientBalanceForRepay error for when requested repay exceeds user's balance
|
||||
ErrInsufficientBalanceForRepay = sdkerrors.Register(ModuleName, 29, "insufficient balance")
|
||||
)
|
||||
|
@ -9,6 +9,7 @@ const (
|
||||
EventTypeHarvestWithdrawal = "harvest_withdrawal"
|
||||
EventTypeClaimHarvestReward = "claim_harvest_reward"
|
||||
EventTypeHarvestBorrow = "harvest_borrow"
|
||||
EventTypeHarvestRepay = "harvest_repay"
|
||||
AttributeValueCategory = ModuleName
|
||||
AttributeKeyBlockHeight = "block_height"
|
||||
AttributeKeyRewardsDistribution = "rewards_distributed"
|
||||
@ -22,4 +23,6 @@ const (
|
||||
AttributeKeyBorrow = "borrow"
|
||||
AttributeKeyBorrower = "borrower"
|
||||
AttributeKeyBorrowCoins = "borrow_coins"
|
||||
AttributeKeySender = "sender"
|
||||
AttributeKeyRepayCoins = "repay_coins"
|
||||
)
|
||||
|
@ -209,8 +209,6 @@ func (msg MsgClaimReward) GetSigners() []sdk.AccAddress {
|
||||
return []sdk.AccAddress{msg.Sender}
|
||||
}
|
||||
|
||||
// ---------------------------------------
|
||||
|
||||
// MsgBorrow borrows funds from the harvest module.
|
||||
type MsgBorrow struct {
|
||||
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 }
|
||||
|
||||
// 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.
|
||||
func (msg MsgBorrow) ValidateBasic() error {
|
||||
@ -260,3 +258,53 @@ func (msg MsgBorrow) String() string {
|
||||
Amount: %s
|
||||
`, 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) {
|
||||
suite.Run(t, new(MsgTestSuite))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user