Hard Audit: enable users to repay another account's borrows (#801)

* add owner to repay msg

* pass owner and sender to repay function

* make owner arg an optional flag

* make owner optional for REST
This commit is contained in:
Denali Marsh 2021-02-08 13:51:42 +01:00 committed by GitHub
parent 1b2cfa6d1a
commit 5af50e1a2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 60 additions and 20 deletions

View File

@ -6,6 +6,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/context"
@ -33,13 +34,20 @@ func GetTxCmd(cdc *codec.Codec) *cobra.Command {
getCmdDeposit(cdc),
getCmdWithdraw(cdc),
getCmdBorrow(cdc),
addOptionalFlag(getCmdRepay(cdc), flagOwner, "", "original borrower's address whose loan will be repaid"),
getCmdLiquidate(cdc),
getCmdRepay(cdc),
)...)
return hardTxCmd
}
// addFlag adds a cobra flag and binds it using viper
func addOptionalFlag(cmd *cobra.Command, flagName, flagValue, flagUsage string) *cobra.Command {
cmd.Flags().String(flagName, flagValue, flagUsage)
viper.BindPFlag(flagName, cmd.Flags().Lookup(flagName))
return cmd
}
func getCmdDeposit(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "deposit [amount]",
@ -91,7 +99,7 @@ func getCmdWithdraw(cdc *codec.Codec) *cobra.Command {
func getCmdBorrow(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "borrow [1000000000ukava]",
Use: "borrow [amount]",
Short: "borrow tokens from the hard protocol",
Long: strings.TrimSpace(`borrows tokens from the hard protocol`),
Args: cobra.ExactArgs(1),
@ -119,24 +127,40 @@ func getCmdBorrow(cdc *codec.Codec) *cobra.Command {
func getCmdRepay(cdc *codec.Codec) *cobra.Command {
return &cobra.Command{
Use: "repay [1000000000ukava]",
Use: "repay [amount]",
Short: "repay tokens to the hard protocol",
Long: strings.TrimSpace(`repay tokens to the hard protocol`),
Long: strings.TrimSpace(`repay tokens to the hard protocol with optional --owner param to repay another account's loan`),
Args: cobra.ExactArgs(1),
Example: fmt.Sprintf(
`%s tx %s repay 1000000000ukava,25000000000bnb --from <key>`, version.ClientName, types.ModuleName,
),
Example: strings.TrimSpace(`
kvcli tx hard repay 1000000000ukava --from <key>
kvcli tx hard repay 1000000000ukava,25000000000bnb --from <key>
kvcli tx hard repay 1000000000ukava,25000000000bnb --owner <owner-address> --from <key>
`),
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))
var owner sdk.AccAddress
ownerStr := viper.GetString(flagOwner)
// Parse optional owner argument or default to sender
if len(ownerStr) > 0 {
ownerAddr, err := sdk.AccAddressFromBech32(ownerStr)
if err != nil {
return err
}
owner = ownerAddr
} else {
owner = cliCtx.GetFromAddress()
}
coins, err := sdk.ParseCoins(args[0])
if err != nil {
return err
}
msg := types.NewMsgRepay(cliCtx.GetFromAddress(), coins)
msg := types.NewMsgRepay(cliCtx.GetFromAddress(), owner, coins)
if err := msg.ValidateBasic(); err != nil {
return err
}

View File

@ -47,6 +47,7 @@ type PostBorrowReq struct {
type PostRepayReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
From sdk.AccAddress `json:"from" yaml:"from"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
}

View File

@ -97,7 +97,12 @@ func postRepayHandlerFn(cliCtx context.CLIContext) http.HandlerFunc {
return
}
msg := types.NewMsgRepay(req.From, req.Amount)
owner := req.Owner
if owner.Empty() {
owner = req.From
}
msg := types.NewMsgRepay(req.From, owner, req.Amount)
if err := msg.ValidateBasic(); err != nil {
rest.WriteErrorResponse(w, http.StatusBadRequest, err.Error())
return

View File

@ -84,7 +84,7 @@ func handleMsgBorrow(ctx sdk.Context, k keeper.Keeper, msg types.MsgBorrow) (*sd
}
func handleMsgRepay(ctx sdk.Context, k keeper.Keeper, msg types.MsgRepay) (*sdk.Result, error) {
err := k.Repay(ctx, msg.Sender, msg.Amount)
err := k.Repay(ctx, msg.Sender, msg.Owner, msg.Amount)
if err != nil {
return nil, err
}

View File

@ -8,9 +8,9 @@ import (
)
// Repay borrowed funds
func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) error {
func (k Keeper) Repay(ctx sdk.Context, sender, owner sdk.AccAddress, coins sdk.Coins) error {
// Check borrow exists here to avoid duplicating store read in ValidateRepay
borrow, found := k.GetBorrow(ctx, sender)
borrow, found := k.GetBorrow(ctx, owner)
if !found {
return types.ErrBorrowNotFound
}
@ -18,10 +18,10 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
k.BeforeBorrowModified(ctx, borrow)
// Sync borrow interest so loan is up-to-date
k.SyncBorrowInterest(ctx, sender)
k.SyncBorrowInterest(ctx, owner)
// Validate requested repay
err := k.ValidateRepay(ctx, sender, coins)
// Validate that sender holds coins for repayment
err = k.ValidateRepay(ctx, sender, coins)
if err != nil {
return err
}
@ -69,6 +69,7 @@ func (k Keeper) Repay(ctx sdk.Context, sender sdk.AccAddress, coins sdk.Coins) e
sdk.NewEvent(
types.EventTypeHardRepay,
sdk.NewAttribute(types.AttributeKeySender, sender.String()),
sdk.NewAttribute(types.AttributeKeyOwner, owner.String()),
sdk.NewAttribute(types.AttributeKeyRepayCoins, payment.String()),
),
)

View File

@ -211,7 +211,7 @@ func (suite *KeeperTestSuite) TestRepay() {
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)
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.repayCoins)
if tc.errArgs.expectPass {
suite.Require().NoError(err)
// If we overpaid expect an adjustment

View File

@ -356,7 +356,7 @@ func (suite *KeeperTestSuite) TestLtvWithdraw() {
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)
err = suite.keeper.Repay(suite.ctx, tc.args.borrower, tc.args.borrower, tc.args.borrowCoins)
suite.Require().NoError(err)
// Attempted withdraw of all deposited coins fails as user hasn't repaid interest debt

View File

@ -26,4 +26,5 @@ const (
AttributeKeyLiquidatedCoins = "liquidated_coins"
AttributeKeyKeeper = "keeper"
AttributeKeyKeeperRewardCoins = "keeper_reward_coins"
AttributeKeyOwner = "owner"
)

View File

@ -169,13 +169,15 @@ func (msg MsgBorrow) String() string {
// MsgRepay repays funds to the hard module.
type MsgRepay struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
Owner sdk.AccAddress `json:"owner" yaml:"owner"`
Amount sdk.Coins `json:"amount" yaml:"amount"`
}
// NewMsgRepay returns a new MsgRepay
func NewMsgRepay(sender sdk.AccAddress, amount sdk.Coins) MsgRepay {
func NewMsgRepay(sender, owner sdk.AccAddress, amount sdk.Coins) MsgRepay {
return MsgRepay{
Sender: sender,
Owner: owner,
Amount: amount,
}
}
@ -191,6 +193,9 @@ func (msg MsgRepay) ValidateBasic() error {
if msg.Sender.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "sender address cannot be empty")
}
if msg.Owner.Empty() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, "owner address cannot be empty")
}
if !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "repay amount %s", msg.Amount)
}
@ -212,8 +217,9 @@ func (msg MsgRepay) GetSigners() []sdk.AccAddress {
func (msg MsgRepay) String() string {
return fmt.Sprintf(`Repay Message:
Sender: %s
Owner: %s
Amount: %s
`, msg.Sender, msg.Amount)
`, msg.Sender, msg.Owner, msg.Amount)
}
// MsgLiquidate attempts to liquidate a borrower's borrow

View File

@ -152,6 +152,7 @@ func (suite *MsgTestSuite) TestMsgBorrow() {
func (suite *MsgTestSuite) TestMsgRepay() {
type args struct {
sender sdk.AccAddress
owner sdk.AccAddress
amount sdk.Coins
}
addrs := []sdk.AccAddress{
@ -167,6 +168,7 @@ func (suite *MsgTestSuite) TestMsgRepay() {
name: "valid",
args: args{
sender: addrs[0],
owner: addrs[0],
amount: sdk.NewCoins(sdk.NewCoin("test", sdk.NewInt(1000000))),
},
expectPass: true,
@ -175,7 +177,7 @@ func (suite *MsgTestSuite) TestMsgRepay() {
}
for _, tc := range testCases {
suite.Run(tc.name, func() {
msg := types.NewMsgRepay(tc.args.sender, tc.args.amount)
msg := types.NewMsgRepay(tc.args.sender, tc.args.owner, tc.args.amount)
err := msg.ValidateBasic()
if tc.expectPass {
suite.NoError(err)