feat: add proposals for community pool deposits/withdrawals (#1304)

* feat: community pool deposit/withdraw proposals

* fix: check community pool balance in tests

* add new msg type definitions

* add msg methods and tests

* add module and keeper skeleton

* add deposit and withdraw methods (no delegation)

* untested depsit/withdraw with delegation methods

* add cli cmds

* fix cli argument parsing

* add tests for delegate/undelegate msgs

* emit un/delegate events

* add godoc comments

* tally handler with liquid staking support

* clean up

* update for liquid keeper changes

* Exclude non-bkava denoms from aggregate underlying ukava calculation

* wip Add claim

* Add distr keeper and claiming

* Add claim test

* Update claim test with failures

* wip Add staking rewards

* -S

Fix savings to earn incentive methods

* Use a single accural time for all earn incentives

* Add additional required liquid methods

* Update genesis to only include 1 accrual time for earn

* Revert "Update genesis to only include 1 accrual time for earn"

This reverts commit cc7e35347298681c0c8a4a0b9bf9b9b296c25531.

* Revert "Use a single accural time for all earn incentives"

This reverts commit aeb49c4622d4e3d99dc6421c8830932b1b546be9.

* Update tests with incentive distribution

* Add earn to incentive rewards query

* add earn cli tx

* Update claim example to use ukava large

* add proposal to gov router

* fix example tx formating

* add proposal handlers to gov app module

* fix: define gov router after earn keeper

* fix: correct proposal type

* remove outdated comment

* refactor withdraw so that fee pool is allows adjusted by the actual withdraw amount

* fix: lint proto file

* use non blocked module account instead of dist acc

* add fund mod account to app, enable receiving

* update to new withdraw interface

* add human readable apy test cases

* remove duplicate changes from previous merge

* remove deprecated io/ioutil package

* standardize proposal type as a pointer
(also matches sdk)

* minior comments and formatting

* use withdraw amount in router msgs

Co-authored-by: rhuairahrighairigh <ruaridh.odonnell@gmail.com>
Co-authored-by: Draco <draco@dracoli.com>
Co-authored-by: drklee3 <derrick@dlee.dev>
Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com>
This commit is contained in:
Kevin Davis 2022-09-29 11:01:06 -06:00 committed by GitHub
parent 6ef9bab67d
commit ef874f9913
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 2126 additions and 82 deletions

View File

@ -107,6 +107,7 @@ import (
committeekeeper "github.com/kava-labs/kava/x/committee/keeper" committeekeeper "github.com/kava-labs/kava/x/committee/keeper"
committeetypes "github.com/kava-labs/kava/x/committee/types" committeetypes "github.com/kava-labs/kava/x/committee/types"
earn "github.com/kava-labs/kava/x/earn" earn "github.com/kava-labs/kava/x/earn"
earnclient "github.com/kava-labs/kava/x/earn/client"
earnkeeper "github.com/kava-labs/kava/x/earn/keeper" earnkeeper "github.com/kava-labs/kava/x/earn/keeper"
earntypes "github.com/kava-labs/kava/x/earn/types" earntypes "github.com/kava-labs/kava/x/earn/types"
evmutil "github.com/kava-labs/kava/x/evmutil" evmutil "github.com/kava-labs/kava/x/evmutil"
@ -171,6 +172,8 @@ var (
ibcclientclient.UpgradeProposalHandler, ibcclientclient.UpgradeProposalHandler,
kavadistclient.ProposalHandler, kavadistclient.ProposalHandler,
committeeclient.ProposalHandler, committeeclient.ProposalHandler,
earnclient.DepositProposalHandler,
earnclient.WithdrawProposalHandler,
), ),
params.AppModuleBasic{}, params.AppModuleBasic{},
crisis.AppModuleBasic{}, crisis.AppModuleBasic{},
@ -225,6 +228,7 @@ var (
savingstypes.ModuleAccountName: nil, savingstypes.ModuleAccountName: nil,
liquidtypes.ModuleAccountName: {authtypes.Minter, authtypes.Burner}, liquidtypes.ModuleAccountName: {authtypes.Minter, authtypes.Burner},
earntypes.ModuleAccountName: nil, earntypes.ModuleAccountName: nil,
kavadisttypes.FundModuleAccount: nil,
} }
) )
@ -613,6 +617,7 @@ func NewApp(
&app.liquidKeeper, &app.liquidKeeper,
&hardKeeper, &hardKeeper,
&savingsKeeper, &savingsKeeper,
app.distrKeeper,
) )
app.incentiveKeeper = incentivekeeper.NewKeeper( app.incentiveKeeper = incentivekeeper.NewKeeper(
@ -652,26 +657,6 @@ func NewApp(
app.accountKeeper, app.accountKeeper,
app.bankKeeper, app.bankKeeper,
) )
// create gov keeper with router
// NOTE this must be done after any keepers referenced in the gov router (ie committee) are defined
govRouter := govtypes.NewRouter()
govRouter.
AddRoute(govtypes.RouterKey, govtypes.ProposalHandler).
AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper)).
AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.ibcKeeper.ClientKeeper)).
AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)).
AddRoute(kavadisttypes.RouterKey, kavadist.NewCommunityPoolMultiSpendProposalHandler(app.kavadistKeeper)).
AddRoute(committeetypes.RouterKey, committee.NewProposalHandler(app.committeeKeeper))
app.govKeeper = govkeeper.NewKeeper(
appCodec,
keys[govtypes.StoreKey],
govSubspace,
app.accountKeeper,
app.bankKeeper,
&app.stakingKeeper,
govRouter,
)
// register the staking hooks // register the staking hooks
// NOTE: These keepers are passed by reference above, so they will contain these hooks. // NOTE: These keepers are passed by reference above, so they will contain these hooks.
@ -684,6 +669,28 @@ func NewApp(
app.savingsKeeper = *savingsKeeper.SetHooks(savingstypes.NewMultiSavingsHooks(app.incentiveKeeper.Hooks())) app.savingsKeeper = *savingsKeeper.SetHooks(savingstypes.NewMultiSavingsHooks(app.incentiveKeeper.Hooks()))
app.earnKeeper = *earnKeeper.SetHooks(app.incentiveKeeper.Hooks()) app.earnKeeper = *earnKeeper.SetHooks(app.incentiveKeeper.Hooks())
// create gov keeper with router
// NOTE this must be done after any keepers referenced in the gov router (ie committee) are defined
govRouter := govtypes.NewRouter()
govRouter.
AddRoute(govtypes.RouterKey, govtypes.ProposalHandler).
AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.paramsKeeper)).
AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.upgradeKeeper)).
AddRoute(ibcclienttypes.RouterKey, ibcclient.NewClientProposalHandler(app.ibcKeeper.ClientKeeper)).
AddRoute(distrtypes.RouterKey, distr.NewCommunityPoolSpendProposalHandler(app.distrKeeper)).
AddRoute(kavadisttypes.RouterKey, kavadist.NewCommunityPoolMultiSpendProposalHandler(app.kavadistKeeper)).
AddRoute(earntypes.RouterKey, earn.NewCommunityPoolProposalHandler(app.earnKeeper)).
AddRoute(committeetypes.RouterKey, committee.NewProposalHandler(app.committeeKeeper))
app.govKeeper = govkeeper.NewKeeper(
appCodec,
keys[govtypes.StoreKey],
govSubspace,
app.accountKeeper,
app.bankKeeper,
&app.stakingKeeper,
govRouter,
)
// override x/gov tally handler with custom implementation // override x/gov tally handler with custom implementation
tallyHandler := NewTallyHandler( tallyHandler := NewTallyHandler(
app.govKeeper, app.stakingKeeper, app.savingsKeeper, app.earnKeeper, app.govKeeper, app.stakingKeeper, app.savingsKeeper, app.earnKeeper,
@ -1023,10 +1030,11 @@ func (app *App) loadBlockedMaccAddrs() map[string]bool {
kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName) kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName)
earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName) earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName)
liquidMaccAddr := app.accountKeeper.GetModuleAddress(liquidtypes.ModuleName) liquidMaccAddr := app.accountKeeper.GetModuleAddress(liquidtypes.ModuleName)
kavadistFundMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.FundModuleAccount)
for addr := range modAccAddrs { for addr := range modAccAddrs {
// Set the kavadist and earn module account address as unblocked // Set the kavadist and earn module account address as unblocked
if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() || addr == liquidMaccAddr.String() { if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() || addr == liquidMaccAddr.String() || addr == kavadistFundMaccAddr.String() {
modAccAddrs[addr] = false modAccAddrs[addr] = false
} }
} }

View File

@ -193,6 +193,12 @@
- [kava/earn/v1beta1/genesis.proto](#kava/earn/v1beta1/genesis.proto) - [kava/earn/v1beta1/genesis.proto](#kava/earn/v1beta1/genesis.proto)
- [GenesisState](#kava.earn.v1beta1.GenesisState) - [GenesisState](#kava.earn.v1beta1.GenesisState)
- [kava/earn/v1beta1/proposal.proto](#kava/earn/v1beta1/proposal.proto)
- [CommunityPoolDepositProposal](#kava.earn.v1beta1.CommunityPoolDepositProposal)
- [CommunityPoolDepositProposalJSON](#kava.earn.v1beta1.CommunityPoolDepositProposalJSON)
- [CommunityPoolWithdrawProposal](#kava.earn.v1beta1.CommunityPoolWithdrawProposal)
- [CommunityPoolWithdrawProposalJSON](#kava.earn.v1beta1.CommunityPoolWithdrawProposalJSON)
- [kava/earn/v1beta1/query.proto](#kava/earn/v1beta1/query.proto) - [kava/earn/v1beta1/query.proto](#kava/earn/v1beta1/query.proto)
- [DepositResponse](#kava.earn.v1beta1.DepositResponse) - [DepositResponse](#kava.earn.v1beta1.DepositResponse)
- [QueryDepositsRequest](#kava.earn.v1beta1.QueryDepositsRequest) - [QueryDepositsRequest](#kava.earn.v1beta1.QueryDepositsRequest)
@ -2950,6 +2956,92 @@ GenesisState defines the earn module's genesis state.
<!-- end messages -->
<!-- end enums -->
<!-- end HasExtensions -->
<!-- end services -->
<a name="kava/earn/v1beta1/proposal.proto"></a>
<p align="right"><a href="#top">Top</a></p>
## kava/earn/v1beta1/proposal.proto
<a name="kava.earn.v1beta1.CommunityPoolDepositProposal"></a>
### CommunityPoolDepositProposal
CommunityPoolDepositProposal deposits from the community pool into an earn vault
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `title` | [string](#string) | | |
| `description` | [string](#string) | | |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | |
<a name="kava.earn.v1beta1.CommunityPoolDepositProposalJSON"></a>
### CommunityPoolDepositProposalJSON
CommunityPoolDepositProposalJSON defines a CommunityPoolDepositProposal with a deposit
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `title` | [string](#string) | | |
| `description` | [string](#string) | | |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | |
| `deposit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | |
<a name="kava.earn.v1beta1.CommunityPoolWithdrawProposal"></a>
### CommunityPoolWithdrawProposal
CommunityPoolWithdrawProposal withdraws from an earn vault back to community pool
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `title` | [string](#string) | | |
| `description` | [string](#string) | | |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | |
<a name="kava.earn.v1beta1.CommunityPoolWithdrawProposalJSON"></a>
### CommunityPoolWithdrawProposalJSON
CommunityPoolWithdrawProposalJSON defines a CommunityPoolWithdrawProposal with a deposit
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `title` | [string](#string) | | |
| `description` | [string](#string) | | |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | |
| `deposit` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | repeated | |
<!-- end messages --> <!-- end messages -->
<!-- end enums --> <!-- end enums -->

View File

@ -0,0 +1,51 @@
syntax = "proto3";
package kava.earn.v1beta1;
import "gogoproto/gogo.proto";
import "cosmos/base/v1beta1/coin.proto";
option go_package = "github.com/kava-labs/kava/x/earn/types";
// CommunityPoolDepositProposal deposits from the community pool into an earn vault
message CommunityPoolDepositProposal {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.goproto_getters) = false;
string title = 1;
string description = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// CommunityPoolDepositProposalJSON defines a CommunityPoolDepositProposal with a deposit
message CommunityPoolDepositProposalJSON {
option (gogoproto.goproto_stringer) = true;
option (gogoproto.goproto_getters) = false;
string title = 1;
string description = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
repeated cosmos.base.v1beta1.Coin deposit = 4
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
}
// CommunityPoolWithdrawProposal withdraws from an earn vault back to community pool
message CommunityPoolWithdrawProposal {
option (gogoproto.goproto_stringer) = false;
option (gogoproto.goproto_getters) = false;
string title = 1;
string description = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// CommunityPoolWithdrawProposalJSON defines a CommunityPoolWithdrawProposal with a deposit
message CommunityPoolWithdrawProposalJSON {
option (gogoproto.goproto_stringer) = true;
option (gogoproto.goproto_getters) = false;
string title = 1;
string description = 2;
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
repeated cosmos.base.v1beta1.Coin deposit = 4
[(gogoproto.nullable) = false, (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coins"];
}

View File

@ -2,6 +2,7 @@ package cli
import ( import (
"fmt" "fmt"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -10,6 +11,7 @@ import (
"github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/version" "github.com/cosmos/cosmos-sdk/version"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/earn/types" "github.com/kava-labs/kava/x/earn/types"
) )
@ -109,3 +111,119 @@ func getCmdWithdraw() *cobra.Command {
}, },
} }
} }
// GetCmdSubmitCommunityPoolDepositProposal implements the command to submit a community-pool deposit proposal
func GetCmdSubmitCommunityPoolDepositProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "community-pool-deposit [proposal-file]",
Args: cobra.ExactArgs(1),
Short: "Submit a community pool deposit proposal",
Long: strings.TrimSpace(
fmt.Sprintf(`Submit a community pool deposit proposal along with an initial deposit.
The proposal details must be supplied via a JSON file.
Example:
$ %s tx gov submit-proposal community-pool-deposit <path/to/proposal.json> --from=<key_or_address>
Where proposal.json contains:
{
"title": "Community Pool Deposit",
"description": "Deposit some KAVA from community pool!",
"amount":
{
"denom": "ukava",
"amount": "100000000000"
},
"deposit": [
{
"denom": "ukava",
"amount": "1000000000"
}
]
}
`,
version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
proposal, err := ParseCommunityPoolDepositProposalJSON(clientCtx.Codec, args[0])
if err != nil {
return err
}
from := clientCtx.GetFromAddress()
content := types.NewCommunityPoolDepositProposal(proposal.Title, proposal.Description, proposal.Amount)
msg, err := govtypes.NewMsgSubmitProposal(content, proposal.Deposit, from)
if err != nil {
return err
}
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
return cmd
}
// GetCmdSubmitCommunityPoolWithdrawProposal implements the command to submit a community-pool withdraw proposal
func GetCmdSubmitCommunityPoolWithdrawProposal() *cobra.Command {
cmd := &cobra.Command{
Use: "community-pool-withdraw [proposal-file]",
Args: cobra.ExactArgs(1),
Short: "Submit a community pool withdraw proposal",
Long: strings.TrimSpace(
fmt.Sprintf(`Submit a community pool withdraw proposal along with an initial deposit.
The proposal details must be supplied via a JSON file.
Example:
$ %s tx gov submit-proposal community-pool-withdraw <path/to/proposal.json> --from=<key_or_address>
Where proposal.json contains:
{
"title": "Community Pool Withdraw",
"description": "Withdraw some KAVA from community pool!",
"amount":
{
"denom": "ukava",
"amount": "100000000000"
},
"deposit": [
{
"denom": "ukava",
"amount": "1000000000"
}
]
}
`,
version.AppName,
),
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
proposal, err := ParseCommunityPoolWithdrawProposalJSON(clientCtx.Codec, args[0])
if err != nil {
return err
}
from := clientCtx.GetFromAddress()
content := types.NewCommunityPoolWithdrawProposal(proposal.Title, proposal.Description, proposal.Amount)
msg, err := govtypes.NewMsgSubmitProposal(content, proposal.Deposit, from)
if err != nil {
return err
}
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
return cmd
}

View File

@ -0,0 +1,39 @@
package cli
import (
"os"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/kava-labs/kava/x/earn/types"
)
// ParseCommunityPoolDepositProposalJSON reads and parses a CommunityPoolDepositProposalJSON from a file.
func ParseCommunityPoolDepositProposalJSON(cdc codec.JSONCodec, proposalFile string) (types.CommunityPoolDepositProposalJSON, error) {
proposal := types.CommunityPoolDepositProposalJSON{}
contents, err := os.ReadFile(proposalFile)
if err != nil {
return proposal, err
}
if err := cdc.UnmarshalJSON(contents, &proposal); err != nil {
return proposal, err
}
return proposal, nil
}
// ParseCommunityPoolWithdrawProposalJSON reads and parses a CommunityPoolWithdrawProposalJSON from a file.
func ParseCommunityPoolWithdrawProposalJSON(cdc codec.JSONCodec, proposalFile string) (types.CommunityPoolWithdrawProposalJSON, error) {
proposal := types.CommunityPoolWithdrawProposalJSON{}
contents, err := os.ReadFile(proposalFile)
if err != nil {
return proposal, err
}
if err := cdc.UnmarshalJSON(contents, &proposal); err != nil {
return proposal, err
}
return proposal, nil
}

View File

@ -0,0 +1,14 @@
package client
import (
govclient "github.com/cosmos/cosmos-sdk/x/gov/client"
"github.com/kava-labs/kava/x/earn/client/cli"
"github.com/kava-labs/kava/x/earn/client/rest"
)
// community-pool deposit/withdraw proposal handlers
var (
DepositProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitCommunityPoolDepositProposal, rest.DepositProposalRESTHandler)
WithdrawProposalHandler = govclient.NewProposalHandler(cli.GetCmdSubmitCommunityPoolWithdrawProposal, rest.WithdrawProposalRESTHandler)
)

View File

@ -0,0 +1,97 @@
package rest
import (
"net/http"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/rest"
govrest "github.com/cosmos/cosmos-sdk/x/gov/client/rest"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/earn/types"
)
type (
// CommunityPoolDepositProposalReq defines a community pool deposit proposal request body.
CommunityPoolDepositProposalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
}
// CommunityPoolWithdrawProposalReq defines a community pool deposit proposal request body.
CommunityPoolWithdrawProposalReq struct {
BaseReq rest.BaseReq `json:"base_req" yaml:"base_req"`
Title string `json:"title" yaml:"title"`
Description string `json:"description" yaml:"description"`
Amount sdk.Coin `json:"amount" yaml:"amount"`
Deposit sdk.Coins `json:"deposit" yaml:"deposit"`
Proposer sdk.AccAddress `json:"proposer" yaml:"proposer"`
}
)
// DepositProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool deposit REST handler with a given sub-route.
func DepositProposalRESTHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
return govrest.ProposalRESTHandler{
SubRoute: types.ProposalTypeCommunityPoolDeposit,
Handler: postDepositProposalHandlerFn(cliCtx),
}
}
func postDepositProposalHandlerFn(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req CommunityPoolDepositProposalReq
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
content := types.NewCommunityPoolDepositProposal(req.Title, req.Description, req.Amount)
msg, err := govtypes.NewMsgSubmitProposal(content, req.Deposit, req.Proposer)
if rest.CheckBadRequestError(w, err) {
return
}
if rest.CheckBadRequestError(w, msg.ValidateBasic()) {
return
}
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg)
}
}
// WithdrawProposalRESTHandler returns a ProposalRESTHandler that exposes the community pool deposit REST handler with a given sub-route.
func WithdrawProposalRESTHandler(cliCtx client.Context) govrest.ProposalRESTHandler {
return govrest.ProposalRESTHandler{
SubRoute: types.ProposalTypeCommunityPoolWithdraw,
Handler: postWithdrawProposalHandlerFn(cliCtx),
}
}
func postWithdrawProposalHandlerFn(cliCtx client.Context) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req CommunityPoolWithdrawProposalReq
if !rest.ReadRESTReq(w, r, cliCtx.LegacyAmino, &req) {
return
}
req.BaseReq = req.BaseReq.Sanitize()
if !req.BaseReq.ValidateBasic(w) {
return
}
content := types.NewCommunityPoolWithdrawProposal(req.Title, req.Description, req.Amount)
msg, err := govtypes.NewMsgSubmitProposal(content, req.Deposit, req.Proposer)
if rest.CheckBadRequestError(w, err) {
return
}
if rest.CheckBadRequestError(w, msg.ValidateBasic()) {
return
}
tx.WriteGeneratedTxResponse(cliCtx, w, req.BaseReq, msg)
}
}

24
x/earn/handler.go Normal file
View File

@ -0,0 +1,24 @@
package earn
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
"github.com/kava-labs/kava/x/earn/keeper"
"github.com/kava-labs/kava/x/earn/types"
)
// NewCommunityPoolProposalHandler
func NewCommunityPoolProposalHandler(k keeper.Keeper) govtypes.Handler {
return func(ctx sdk.Context, content govtypes.Content) error {
switch c := content.(type) {
case *types.CommunityPoolDepositProposal:
return keeper.HandleCommunityPoolDepositProposal(ctx, k, c)
case *types.CommunityPoolWithdrawProposal:
return keeper.HandleCommunityPoolWithdrawProposal(ctx, k, c)
default:
return sdkerrors.Wrapf(sdkerrors.ErrUnknownRequest, "unrecognized earn proposal content type: %T", c)
}
}
}

View File

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/earn/types" "github.com/kava-labs/kava/x/earn/types"
) )
@ -112,3 +113,15 @@ func (k *Keeper) Deposit(
return nil return nil
} }
// DepositFromModuleAccount adds the provided amount from a depositor module
// account to a vault. The vault is specified by the denom in the amount.
func (k *Keeper) DepositFromModuleAccount(
ctx sdk.Context,
from string,
wantAmount sdk.Coin,
withdrawStrategy types.StrategyType,
) error {
addr := k.accountKeeper.GetModuleAddress(from)
return k.Deposit(ctx, addr, wantAmount, withdrawStrategy)
}

View File

@ -172,7 +172,7 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() {
acc.GetAddress(), acc.GetAddress(),
shareRecord.AmountOf(deposit1Amount.Denom), shareRecord.AmountOf(deposit1Amount.Denom),
).Once() ).Once()
err = suite.Keeper.Withdraw( _, err = suite.Keeper.Withdraw(
suite.Ctx, suite.Ctx,
acc.GetAddress(), acc.GetAddress(),
// 3 deposits, multiply original deposit amount by 3 // 3 deposits, multiply original deposit amount by 3
@ -196,7 +196,7 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() {
acc.GetAddress(), acc.GetAddress(),
shareRecord.AmountOf(deposit2Amount.Denom), shareRecord.AmountOf(deposit2Amount.Denom),
).Once() ).Once()
err = suite.Keeper.Withdraw( _, err = suite.Keeper.Withdraw(
suite.Ctx, suite.Ctx,
acc.GetAddress(), acc.GetAddress(),
deposit2Amount, deposit2Amount,
@ -219,7 +219,7 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() {
acc.GetAddress(), acc.GetAddress(),
shareRecord.AmountOf(deposit2Amount.Denom), shareRecord.AmountOf(deposit2Amount.Denom),
).Once() ).Once()
err = suite.Keeper.Withdraw( _, err = suite.Keeper.Withdraw(
suite.Ctx, suite.Ctx,
acc.GetAddress(), acc.GetAddress(),
deposit2Amount, deposit2Amount,
@ -242,7 +242,7 @@ func (suite *hookTestSuite) TestHooks_DepositAndWithdraw() {
acc.GetAddress(), acc.GetAddress(),
shareRecord.AmountOf(deposit2Amount.Denom), shareRecord.AmountOf(deposit2Amount.Denom),
).Once() ).Once()
err = suite.Keeper.Withdraw( _, err = suite.Keeper.Withdraw(
suite.Ctx, suite.Ctx,
acc.GetAddress(), acc.GetAddress(),
deposit2Amount, deposit2Amount,
@ -274,7 +274,7 @@ func (suite *hookTestSuite) TestHooks_NoPanicsOnNilHooks() {
suite.Require().NoError(err) suite.Require().NoError(err)
// BeforeVaultDepositModified should not panic if no hooks are registered // BeforeVaultDepositModified should not panic if no hooks are registered
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
} }
@ -317,6 +317,6 @@ func (suite *hookTestSuite) TestHooks_HookOrdering() {
suite.Require().True(found, "expected after hook to be called after shares are updated") suite.Require().True(found, "expected after hook to be called after shares are updated")
suite.Require().Equal(depositAmount.Amount.MulRaw(2).ToDec(), shares.AmountOf(depositAmount.Denom)) suite.Require().Equal(depositAmount.Amount.MulRaw(2).ToDec(), shares.AmountOf(depositAmount.Denom))
}) })
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
} }

View File

@ -21,6 +21,9 @@ type Keeper struct {
// Keepers used for strategies // Keepers used for strategies
hardKeeper types.HardKeeper hardKeeper types.HardKeeper
savingsKeeper types.SavingsKeeper savingsKeeper types.SavingsKeeper
// Keeper for community pool transfers
distKeeper types.DistributionKeeper
} }
// NewKeeper creates a new keeper // NewKeeper creates a new keeper
@ -33,6 +36,7 @@ func NewKeeper(
liquidKeeper types.LiquidKeeper, liquidKeeper types.LiquidKeeper,
hardKeeper types.HardKeeper, hardKeeper types.HardKeeper,
savingsKeeper types.SavingsKeeper, savingsKeeper types.SavingsKeeper,
distKeeper types.DistributionKeeper,
) Keeper { ) Keeper {
if !paramstore.HasKeyTable() { if !paramstore.HasKeyTable() {
paramstore = paramstore.WithKeyTable(types.ParamKeyTable()) paramstore = paramstore.WithKeyTable(types.ParamKeyTable())
@ -47,6 +51,7 @@ func NewKeeper(
liquidKeeper: liquidKeeper, liquidKeeper: liquidKeeper,
hardKeeper: hardKeeper, hardKeeper: hardKeeper,
savingsKeeper: savingsKeeper, savingsKeeper: savingsKeeper,
distKeeper: distKeeper,
} }
} }

View File

@ -53,7 +53,8 @@ func (m msgServer) Withdraw(goCtx context.Context, msg *types.MsgWithdraw) (*typ
return nil, err return nil, err
} }
if err := m.keeper.Withdraw(ctx, from, msg.Amount, msg.Strategy); err != nil { _, err = m.keeper.Withdraw(ctx, from, msg.Amount, msg.Strategy)
if err != nil {
return nil, err return nil, err
} }

View File

@ -0,0 +1,49 @@
package keeper
import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/earn/types"
kavadisttypes "github.com/kava-labs/kava/x/kavadist/types"
)
// HandleCommunityPoolDepositProposal is a handler for executing a passed community pool deposit proposal
func HandleCommunityPoolDepositProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolDepositProposal) error {
fundAcc := k.accountKeeper.GetModuleAccount(ctx, kavadisttypes.FundModuleAccount)
if err := k.distKeeper.DistributeFromFeePool(ctx, sdk.NewCoins(p.Amount), fundAcc.GetAddress()); err != nil {
return err
}
err := k.DepositFromModuleAccount(ctx, kavadisttypes.FundModuleAccount, p.Amount, types.STRATEGY_TYPE_SAVINGS)
if err != nil {
return err
}
return nil
}
// HandleCommunityPoolWithdrawProposal is a handler for executing a passed community pool withdraw proposal.
func HandleCommunityPoolWithdrawProposal(ctx sdk.Context, k Keeper, p *types.CommunityPoolWithdrawProposal) error {
// Withdraw to fund module account
withdrawAmount, err := k.WithdrawFromModuleAccount(ctx, kavadisttypes.FundModuleAccount, p.Amount, types.STRATEGY_TYPE_SAVINGS)
if err != nil {
return err
}
// Move funds to the community pool manually
err = k.bankKeeper.SendCoinsFromModuleToModule(
ctx,
kavadisttypes.FundModuleAccount,
k.distKeeper.GetDistributionAccount(ctx).GetName(),
sdk.NewCoins(withdrawAmount),
)
if err != nil {
return err
}
feePool := k.distKeeper.GetFeePool(ctx)
newCommunityPool := feePool.CommunityPool.Add(sdk.NewDecCoinFromCoin(withdrawAmount))
feePool.CommunityPool = newCommunityPool
k.distKeeper.SetFeePool(ctx, feePool)
return nil
}

View File

@ -0,0 +1,80 @@
package keeper_test
import (
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/kava-labs/kava/x/earn/keeper"
"github.com/kava-labs/kava/x/earn/testutil"
"github.com/kava-labs/kava/x/earn/types"
"github.com/stretchr/testify/suite"
)
type proposalTestSuite struct {
testutil.Suite
}
func (suite *proposalTestSuite) SetupTest() {
suite.Suite.SetupTest()
suite.Keeper.SetParams(suite.Ctx, types.DefaultParams())
}
func TestProposalTestSuite(t *testing.T) {
suite.Run(t, new(proposalTestSuite))
}
func (suite *proposalTestSuite) TestCommunityDepositProposal() {
distKeeper := suite.App.GetDistrKeeper()
ctx := suite.Ctx
macc := distKeeper.GetDistributionAccount(ctx)
fundAmount := sdk.NewCoins(sdk.NewInt64Coin("ukava", 100000000))
depositAmount := sdk.NewCoin("ukava", sdk.NewInt(10000000))
suite.Require().NoError(suite.App.FundModuleAccount(ctx, macc.GetName(), fundAmount))
feePool := distKeeper.GetFeePool(ctx)
feePool.CommunityPool = sdk.NewDecCoinsFromCoins(fundAmount...)
distKeeper.SetFeePool(ctx, feePool)
suite.CreateVault("ukava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil)
prop := types.NewCommunityPoolDepositProposal("test title",
"desc", depositAmount)
err := keeper.HandleCommunityPoolDepositProposal(ctx, suite.Keeper, prop)
suite.Require().NoError(err)
balance := suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress())
suite.Require().Equal(fundAmount.Sub(sdk.NewCoins(depositAmount)), balance)
feePool = distKeeper.GetFeePool(ctx)
communityPoolBalance, change := feePool.CommunityPool.TruncateDecimal()
suite.Require().Equal(fundAmount.Sub(sdk.NewCoins(depositAmount)), communityPoolBalance)
suite.Require().True(change.Empty())
}
func (suite *proposalTestSuite) TestCommunityWithdrawProposal() {
distKeeper := suite.App.GetDistrKeeper()
ctx := suite.Ctx
macc := distKeeper.GetDistributionAccount(ctx)
fundAmount := sdk.NewCoins(sdk.NewInt64Coin("ukava", 100000000))
depositAmount := sdk.NewCoin("ukava", sdk.NewInt(10000000))
suite.Require().NoError(suite.App.FundModuleAccount(ctx, macc.GetName(), fundAmount))
feePool := distKeeper.GetFeePool(ctx)
feePool.CommunityPool = sdk.NewDecCoinsFromCoins(fundAmount...)
distKeeper.SetFeePool(ctx, feePool)
// TODO update to STRATEGY_TYPE_SAVINGS once implemented
suite.CreateVault("ukava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil)
deposit := types.NewCommunityPoolDepositProposal("test title",
"desc", depositAmount)
err := keeper.HandleCommunityPoolDepositProposal(ctx, suite.Keeper, deposit)
suite.Require().NoError(err)
balance := suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress())
suite.Require().Equal(fundAmount.Sub(sdk.NewCoins(depositAmount)), balance)
withdraw := types.NewCommunityPoolWithdrawProposal("test title",
"desc", depositAmount)
err = keeper.HandleCommunityPoolWithdrawProposal(ctx, suite.Keeper, withdraw)
suite.Require().NoError(err)
balance = suite.BankKeeper.GetAllBalances(ctx, macc.GetAddress())
suite.Require().Equal(fundAmount, balance)
feePool = distKeeper.GetFeePool(ctx)
communityPoolBalance, change := feePool.CommunityPool.TruncateDecimal()
suite.Require().Equal(fundAmount, communityPoolBalance)
suite.Require().True(change.Empty())
}

View File

@ -184,7 +184,7 @@ func (suite *strategyHardTestSuite) TestWithdraw() {
suite.Equal(depositAmount, totalValue) suite.Equal(depositAmount, totalValue)
// Withdraw // Withdraw
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
suite.HardDepositAmountEqual(sdk.NewCoins()) suite.HardDepositAmountEqual(sdk.NewCoins())
@ -196,7 +196,7 @@ func (suite *strategyHardTestSuite) TestWithdraw() {
suite.Equal(sdk.NewInt64Coin(vaultDenom, 0), totalValue) suite.Equal(sdk.NewInt64Coin(vaultDenom, 0), totalValue)
// Withdraw again // Withdraw again
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault should be deleted when no more supply") suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault should be deleted when no more supply")
} }
@ -218,11 +218,11 @@ func (suite *strategyHardTestSuite) TestWithdraw_OnlyWithdrawOwnSupply() {
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw // Withdraw
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw again // Withdraw again
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs( suite.Require().ErrorIs(
err, err,
@ -263,11 +263,11 @@ func (suite *strategyHardTestSuite) TestWithdraw_WithAccumulatedHard() {
suite.Equal(depositAmount.AddAmount(sdk.NewInt(10)), accValue) suite.Equal(depositAmount.AddAmount(sdk.NewInt(10)), accValue)
// Withdraw 100, 10 remaining // Withdraw 100, 10 remaining
err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw 100 again -- too much // Withdraw 100 again -- too much
err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs( suite.Require().ErrorIs(
err, err,
@ -276,11 +276,11 @@ func (suite *strategyHardTestSuite) TestWithdraw_WithAccumulatedHard() {
) )
// Half of remaining 10, 5 remaining // Half of remaining 10, 5 remaining
err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(vaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(vaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw all // Withdraw all
err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(vaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(vaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
accValue, err = suite.Keeper.GetVaultAccountValue(suite.Ctx, vaultDenom, acc) accValue, err = suite.Keeper.GetVaultAccountValue(suite.Ctx, vaultDenom, acc)
@ -401,7 +401,7 @@ func (suite *strategyHardTestSuite) TestWithdraw_AccumulatedAmount() {
suite.Require().NoError(err) suite.Require().NoError(err)
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(10)), types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(10)), types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
_, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) _, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)
@ -444,7 +444,7 @@ func (suite *strategyHardTestSuite) TestWithdraw_AccumulatedTruncated() {
suite.Equal(depositAmount.AddAmount(sdk.NewInt(5)), accBal, "acc1 should have 105 usdx") suite.Equal(depositAmount.AddAmount(sdk.NewInt(5)), accBal, "acc1 should have 105 usdx")
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(5)), types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(5)), types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)
@ -485,7 +485,7 @@ func (suite *strategyHardTestSuite) TestWithdraw_ExpensiveShares() {
suite.Equal(sdk.NewInt(2000), accBal.Amount, "acc1 should have 2000 usdx") suite.Equal(sdk.NewInt(2000), accBal.Amount, "acc1 should have 2000 usdx")
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, sdk.NewInt64Coin(vaultDenom, 2000), types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, sdk.NewInt64Coin(vaultDenom, 2000), types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)

View File

@ -180,7 +180,7 @@ func (suite *strategySavingsTestSuite) TestWithdraw() {
suite.Equal(depositAmount, totalValue) suite.Equal(depositAmount, totalValue)
// Withdraw // Withdraw
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
suite.SavingsDepositAmountEqual(sdk.NewCoins()) suite.SavingsDepositAmountEqual(sdk.NewCoins())
@ -192,7 +192,7 @@ func (suite *strategySavingsTestSuite) TestWithdraw() {
suite.Equal(sdk.NewInt64Coin(savingsVaultDenom, 0), totalValue) suite.Equal(sdk.NewInt64Coin(savingsVaultDenom, 0), totalValue)
// Withdraw again // Withdraw again
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault should be deleted when no more supply") suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault should be deleted when no more supply")
} }
@ -213,11 +213,11 @@ func (suite *strategySavingsTestSuite) TestWithdraw_OnlyWithdrawOwnSupply() {
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw // Withdraw
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw again // Withdraw again
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs( suite.Require().ErrorIs(
err, err,
@ -257,11 +257,11 @@ func (suite *strategySavingsTestSuite) TestWithdraw_WithAccumulatedSavings() {
suite.Equal(depositAmount.AddAmount(sdk.NewInt(10)), accValue) suite.Equal(depositAmount.AddAmount(sdk.NewInt(10)), accValue)
// Withdraw 100, 10 remaining // Withdraw 100, 10 remaining
err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw 100 again -- too much // Withdraw 100 again -- too much
err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs( suite.Require().ErrorIs(
err, err,
@ -270,11 +270,11 @@ func (suite *strategySavingsTestSuite) TestWithdraw_WithAccumulatedSavings() {
) )
// Half of remaining 10, 5 remaining // Half of remaining 10, 5 remaining
err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(savingsVaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(savingsVaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
// Withdraw all // Withdraw all
err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(savingsVaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc, sdk.NewCoin(savingsVaultDenom, sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
accValue, err = suite.Keeper.GetVaultAccountValue(suite.Ctx, savingsVaultDenom, acc) accValue, err = suite.Keeper.GetVaultAccountValue(suite.Ctx, savingsVaultDenom, acc)
@ -393,7 +393,7 @@ func (suite *strategySavingsTestSuite) TestWithdraw_AccumulatedAmount() {
suite.Require().NoError(err) suite.Require().NoError(err)
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(10)), types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(10)), types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
_, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) _, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)
@ -435,7 +435,7 @@ func (suite *strategySavingsTestSuite) TestWithdraw_AccumulatedTruncated() {
suite.Equal(depositAmount.AddAmount(sdk.NewInt(5)), accBal, "acc1 should have 105 usdx") suite.Equal(depositAmount.AddAmount(sdk.NewInt(5)), accBal, "acc1 should have 105 usdx")
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, depositAmount.AddAmount(sdk.NewInt(5)), types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)
@ -475,7 +475,7 @@ func (suite *strategySavingsTestSuite) TestWithdraw_ExpensiveShares() {
suite.Equal(sdk.NewInt(2000), accBal.Amount, "acc1 should have 2000 usdx") suite.Equal(sdk.NewInt(2000), accBal.Amount, "acc1 should have 2000 usdx")
// 3. Withdraw all from acc1 - including accumulated amount // 3. Withdraw all from acc1 - including accumulated amount
err = suite.Keeper.Withdraw(suite.Ctx, acc1, sdk.NewInt64Coin(savingsVaultDenom, 2000), types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1, sdk.NewInt64Coin(savingsVaultDenom, 2000), types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err) suite.Require().NoError(err)
acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1) acc1Shares, found = suite.Keeper.GetVaultAccountShares(suite.Ctx, acc1)

View File

@ -16,43 +16,43 @@ func (k *Keeper) Withdraw(
from sdk.AccAddress, from sdk.AccAddress,
wantAmount sdk.Coin, wantAmount sdk.Coin,
withdrawStrategy types.StrategyType, withdrawStrategy types.StrategyType,
) error { ) (sdk.Coin, error) {
// Get AllowedVault, if not found (not a valid vault), return error // Get AllowedVault, if not found (not a valid vault), return error
allowedVault, found := k.GetAllowedVault(ctx, wantAmount.Denom) allowedVault, found := k.GetAllowedVault(ctx, wantAmount.Denom)
if !found { if !found {
return types.ErrInvalidVaultDenom return sdk.Coin{}, types.ErrInvalidVaultDenom
} }
if wantAmount.IsZero() { if wantAmount.IsZero() {
return types.ErrInsufficientAmount return sdk.Coin{}, types.ErrInsufficientAmount
} }
// Check if withdraw strategy is supported by vault // Check if withdraw strategy is supported by vault
if !allowedVault.IsStrategyAllowed(withdrawStrategy) { if !allowedVault.IsStrategyAllowed(withdrawStrategy) {
return types.ErrInvalidVaultStrategy return sdk.Coin{}, types.ErrInvalidVaultStrategy
} }
// Check if VaultRecord exists // Check if VaultRecord exists
vaultRecord, found := k.GetVaultRecord(ctx, wantAmount.Denom) vaultRecord, found := k.GetVaultRecord(ctx, wantAmount.Denom)
if !found { if !found {
return types.ErrVaultRecordNotFound return sdk.Coin{}, types.ErrVaultRecordNotFound
} }
// Get account share record for the vault // Get account share record for the vault
vaultShareRecord, found := k.GetVaultShareRecord(ctx, from) vaultShareRecord, found := k.GetVaultShareRecord(ctx, from)
if !found { if !found {
return types.ErrVaultShareRecordNotFound return sdk.Coin{}, types.ErrVaultShareRecordNotFound
} }
withdrawShares, err := k.ConvertToShares(ctx, wantAmount) withdrawShares, err := k.ConvertToShares(ctx, wantAmount)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert assets to shares: %w", err) return sdk.Coin{}, fmt.Errorf("failed to convert assets to shares: %w", err)
} }
accCurrentShares := vaultShareRecord.Shares.AmountOf(wantAmount.Denom) accCurrentShares := vaultShareRecord.Shares.AmountOf(wantAmount.Denom)
// Check if account is not withdrawing more shares than they have // Check if account is not withdrawing more shares than they have
if accCurrentShares.LT(withdrawShares.Amount) { if accCurrentShares.LT(withdrawShares.Amount) {
return sdkerrors.Wrapf( return sdk.Coin{}, sdkerrors.Wrapf(
types.ErrInsufficientValue, types.ErrInsufficientValue,
"account has less %s vault shares than withdraw shares, %s < %s", "account has less %s vault shares than withdraw shares, %s < %s",
wantAmount.Denom, wantAmount.Denom,
@ -64,17 +64,17 @@ func (k *Keeper) Withdraw(
// Convert shares to amount to get truncated true share value // Convert shares to amount to get truncated true share value
withdrawAmount, err := k.ConvertToAssets(ctx, withdrawShares) withdrawAmount, err := k.ConvertToAssets(ctx, withdrawShares)
if err != nil { if err != nil {
return fmt.Errorf("failed to convert shares to assets: %w", err) return sdk.Coin{}, fmt.Errorf("failed to convert shares to assets: %w", err)
} }
accountValue, err := k.GetVaultAccountValue(ctx, wantAmount.Denom, from) accountValue, err := k.GetVaultAccountValue(ctx, wantAmount.Denom, from)
if err != nil { if err != nil {
return fmt.Errorf("failed to get account value: %w", err) return sdk.Coin{}, fmt.Errorf("failed to get account value: %w", err)
} }
// Check if withdrawAmount > account value // Check if withdrawAmount > account value
if withdrawAmount.Amount.GT(accountValue.Amount) { if withdrawAmount.Amount.GT(accountValue.Amount) {
return sdkerrors.Wrapf( return sdk.Coin{}, sdkerrors.Wrapf(
types.ErrInsufficientValue, types.ErrInsufficientValue,
"account has less %s vault value than withdraw amount, %s < %s", "account has less %s vault value than withdraw amount, %s < %s",
withdrawAmount.Denom, withdrawAmount.Denom,
@ -86,7 +86,7 @@ func (k *Keeper) Withdraw(
// Get the strategy for the vault // Get the strategy for the vault
strategy, err := k.GetStrategy(allowedVault.Strategies[0]) strategy, err := k.GetStrategy(allowedVault.Strategies[0])
if err != nil { if err != nil {
return err return sdk.Coin{}, err
} }
// Not necessary to check if amount denom is allowed for the strategy, as // Not necessary to check if amount denom is allowed for the strategy, as
@ -94,7 +94,7 @@ func (k *Keeper) Withdraw(
// Withdraw the withdrawAmount from the strategy // Withdraw the withdrawAmount from the strategy
if err := strategy.Withdraw(ctx, withdrawAmount); err != nil { if err := strategy.Withdraw(ctx, withdrawAmount); err != nil {
return fmt.Errorf("failed to withdraw from strategy: %w", err) return sdk.Coin{}, fmt.Errorf("failed to withdraw from strategy: %w", err)
} }
// Send coins back to account, must withdraw from strategy first or the // Send coins back to account, must withdraw from strategy first or the
@ -105,7 +105,7 @@ func (k *Keeper) Withdraw(
from, from,
sdk.NewCoins(withdrawAmount), sdk.NewCoins(withdrawAmount),
); err != nil { ); err != nil {
return err return sdk.Coin{}, err
} }
// Check if new account balance of shares results in account share value // Check if new account balance of shares results in account share value
@ -116,7 +116,7 @@ func (k *Keeper) Withdraw(
vaultShareRecord.Shares.GetShare(withdrawAmount.Denom).Sub(withdrawShares), vaultShareRecord.Shares.GetShare(withdrawAmount.Denom).Sub(withdrawShares),
) )
if err != nil { if err != nil {
return err return sdk.Coin{}, err
} }
if isDust { if isDust {
@ -148,5 +148,21 @@ func (k *Keeper) Withdraw(
), ),
) )
return nil return withdrawAmount, nil
}
// WithdrawFromModuleAccount removes the amount of supplied tokens from a vault and transfers it
// back to the module account. The module account must be unblocked from receiving transfers.
func (k *Keeper) WithdrawFromModuleAccount(
ctx sdk.Context,
from string,
wantAmount sdk.Coin,
withdrawStrategy types.StrategyType,
) (sdk.Coin, error) {
// Ensure the module account exists to prevent SendCoins from creating a new non-module account.
acc := k.accountKeeper.GetModuleAccount(ctx, from)
if acc == nil {
return sdk.Coin{}, fmt.Errorf("module account not found: %s", from)
}
return k.Withdraw(ctx, acc.GetAddress(), wantAmount, withdrawStrategy)
} }

View File

@ -33,7 +33,7 @@ func (suite *withdrawTestSuite) TestWithdraw_NoVaultRecord() {
acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0)
// Withdraw without having any prior deposits // Withdraw without having any prior deposits
err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound) suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound)
@ -65,7 +65,7 @@ func (suite *withdrawTestSuite) TestWithdraw_NoVaultShareRecord() {
acc2 := suite.CreateAccount(sdk.NewCoins(startBalance), 1) acc2 := suite.CreateAccount(sdk.NewCoins(startBalance), 1)
// Withdraw from acc2 without having any prior deposits // Withdraw from acc2 without having any prior deposits
err = suite.Keeper.Withdraw(suite.Ctx, acc2.GetAddress(), acc2WithdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc2.GetAddress(), acc2WithdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrVaultShareRecordNotFound) suite.Require().ErrorIs(err, types.ErrVaultShareRecordNotFound)
@ -94,7 +94,7 @@ func (suite *withdrawTestSuite) TestWithdraw_ExceedBalance() {
err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrInsufficientValue) suite.Require().ErrorIs(err, types.ErrInsufficientValue)
@ -119,7 +119,7 @@ func (suite *withdrawTestSuite) TestWithdraw_Zero() {
acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0)
err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrInsufficientAmount) suite.Require().ErrorIs(err, types.ErrInsufficientAmount)
@ -144,7 +144,7 @@ func (suite *withdrawTestSuite) TestWithdraw_InvalidVault() {
acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0)
err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrInvalidVaultDenom) suite.Require().ErrorIs(err, types.ErrInvalidVaultDenom)
@ -169,7 +169,7 @@ func (suite *withdrawTestSuite) TestWithdraw_InvalidStrategy() {
acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0) acc := suite.CreateAccount(sdk.NewCoins(startBalance), 0)
err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_SAVINGS) _, err := suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrInvalidVaultStrategy) suite.Require().ErrorIs(err, types.ErrInvalidVaultStrategy)
} }
@ -187,7 +187,7 @@ func (suite *withdrawTestSuite) TestWithdraw_FullBalance() {
err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), withdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
// No net changes in balances // No net changes in balances
@ -214,7 +214,7 @@ func (suite *withdrawTestSuite) TestWithdraw_Partial() {
err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD) err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
suite.AccountBalanceEqual( suite.AccountBalanceEqual(
@ -223,11 +223,11 @@ func (suite *withdrawTestSuite) TestWithdraw_Partial() {
) )
// Second withdraw for remaining 50 // Second withdraw for remaining 50
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err) suite.Require().NoError(err)
// No more balance to withdraw // No more balance to withdraw
err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD) _, err = suite.Keeper.Withdraw(suite.Ctx, acc.GetAddress(), partialWithdrawAmount, types.STRATEGY_TYPE_HARD)
suite.Require().Error(err) suite.Require().Error(err)
suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault record should be deleted after no more supplied") suite.Require().ErrorIs(err, types.ErrVaultRecordNotFound, "vault record should be deleted after no more supplied")
@ -265,7 +265,7 @@ func (suite *withdrawTestSuite) TestWithdraw_bKava() {
"should be able to deposit bkava derivative denom in bkava vault", "should be able to deposit bkava derivative denom in bkava vault",
) )
err = suite.Keeper.Withdraw(suite.Ctx, acc1.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS) _, err = suite.Keeper.Withdraw(suite.Ctx, acc1.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError( suite.Require().NoError(
err, err,
"should be able to withdraw bkava derivative denom from bkava vault", "should be able to withdraw bkava derivative denom from bkava vault",

View File

@ -7,6 +7,7 @@ import (
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
) )
// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the // RegisterLegacyAminoCodec registers all the necessary types and interfaces for the
@ -14,6 +15,8 @@ import (
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgDeposit{}, "earn/MsgDeposit", nil) cdc.RegisterConcrete(&MsgDeposit{}, "earn/MsgDeposit", nil)
cdc.RegisterConcrete(&MsgWithdraw{}, "earn/MsgWithdraw", nil) cdc.RegisterConcrete(&MsgWithdraw{}, "earn/MsgWithdraw", nil)
cdc.RegisterConcrete(&CommunityPoolDepositProposal{}, "kava/CommunityPoolDepositProposal", nil)
cdc.RegisterConcrete(&CommunityPoolWithdrawProposal{}, "kava/CommunityPoolWithdrawProposal", nil)
} }
// RegisterInterfaces registers proto messages under their interfaces for unmarshalling, // RegisterInterfaces registers proto messages under their interfaces for unmarshalling,
@ -23,6 +26,10 @@ func RegisterInterfaces(registry types.InterfaceRegistry) {
&MsgDeposit{}, &MsgDeposit{},
&MsgWithdraw{}, &MsgWithdraw{},
) )
registry.RegisterImplementations((*govtypes.Content)(nil),
&CommunityPoolDepositProposal{},
&CommunityPoolWithdrawProposal{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
} }

View File

@ -3,6 +3,7 @@ package types
import ( import (
sdk "github.com/cosmos/cosmos-sdk/types" sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/cosmos/cosmos-sdk/x/auth/types"
disttypes "github.com/cosmos/cosmos-sdk/x/distribution/types"
hardtypes "github.com/kava-labs/kava/x/hard/types" hardtypes "github.com/kava-labs/kava/x/hard/types"
savingstypes "github.com/kava-labs/kava/x/savings/types" savingstypes "github.com/kava-labs/kava/x/savings/types"
@ -25,6 +26,14 @@ type BankKeeper interface {
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
} }
// DistributionKeeper defines the expected interface needed for community-pool deposits to earn vaults
type DistributionKeeper interface {
GetFeePool(ctx sdk.Context) (feePool disttypes.FeePool)
SetFeePool(ctx sdk.Context, feePool disttypes.FeePool)
GetDistributionAccount(ctx sdk.Context) types.ModuleAccountI
DistributeFromFeePool(ctx sdk.Context, amount sdk.Coins, receiveAddr sdk.AccAddress) error
}
// LiquidKeeper defines the expected interface needed for derivative to staked token conversions. // LiquidKeeper defines the expected interface needed for derivative to staked token conversions.
type LiquidKeeper interface { type LiquidKeeper interface {
GetStakedTokensForDerivatives(ctx sdk.Context, derivatives sdk.Coins) (sdk.Coin, error) GetStakedTokensForDerivatives(ctx sdk.Context, derivatives sdk.Coins) (sdk.Coin, error)

117
x/earn/types/proposal.go Normal file
View File

@ -0,0 +1,117 @@
package types
import (
fmt "fmt"
"strings"
sdk "github.com/cosmos/cosmos-sdk/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
)
const (
// ProposalTypeCommunityPoolDeposit defines the type for a CommunityPoolDepositProposal
ProposalTypeCommunityPoolDeposit = "CommunityPoolDeposit"
// ProposalTypeCommunityPoolWithdraw defines the type for a CommunityPoolDepositProposal
ProposalTypeCommunityPoolWithdraw = "CommunityPoolWithdraw"
)
// Assert CommunityPoolDepositProposal implements govtypes.Content at compile-time
var (
_ govtypes.Content = &CommunityPoolDepositProposal{}
_ govtypes.Content = &CommunityPoolWithdrawProposal{}
)
func init() {
govtypes.RegisterProposalType(ProposalTypeCommunityPoolDeposit)
govtypes.RegisterProposalTypeCodec(&CommunityPoolDepositProposal{}, "kava/CommunityPoolDepositProposal")
govtypes.RegisterProposalType(ProposalTypeCommunityPoolWithdraw)
govtypes.RegisterProposalTypeCodec(&CommunityPoolWithdrawProposal{}, "kava/CommunityPoolWithdrawProposal")
}
// NewCommunityPoolDepositProposal creates a new community pool deposit proposal.
func NewCommunityPoolDepositProposal(title, description string, amount sdk.Coin) *CommunityPoolDepositProposal {
return &CommunityPoolDepositProposal{
Title: title,
Description: description,
Amount: amount,
}
}
// GetTitle returns the title of a community pool deposit proposal.
func (cdp *CommunityPoolDepositProposal) GetTitle() string { return cdp.Title }
// GetDescription returns the description of a community pool deposit proposal.
func (cdp *CommunityPoolDepositProposal) GetDescription() string { return cdp.Description }
// GetDescription returns the routing key of a community pool deposit proposal.
func (cdp *CommunityPoolDepositProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of a community pool deposit proposal.
func (cdp *CommunityPoolDepositProposal) ProposalType() string {
return ProposalTypeCommunityPoolDeposit
}
// String implements fmt.Stringer
func (cdp *CommunityPoolDepositProposal) String() string {
var b strings.Builder
b.WriteString(fmt.Sprintf(`Community Pool Deposit Proposal:
Title: %s
Description: %s
Amount: %s
`, cdp.Title, cdp.Description, cdp.Amount))
return b.String()
}
// ValidateBasic stateless validation of a community pool multi-spend proposal.
func (cdp *CommunityPoolDepositProposal) ValidateBasic() error {
err := govtypes.ValidateAbstract(cdp)
if err != nil {
return err
}
return cdp.Amount.Validate()
}
// NewCommunityPoolWithdrawProposal creates a new community pool deposit proposal.
func NewCommunityPoolWithdrawProposal(title, description string, amount sdk.Coin) *CommunityPoolWithdrawProposal {
return &CommunityPoolWithdrawProposal{
Title: title,
Description: description,
Amount: amount,
}
}
// GetTitle returns the title of a community pool withdraw proposal.
func (cdp *CommunityPoolWithdrawProposal) GetTitle() string { return cdp.Title }
// GetDescription returns the description of a community pool withdraw proposal.
func (cdp *CommunityPoolWithdrawProposal) GetDescription() string { return cdp.Description }
// GetDescription returns the routing key of a community pool withdraw proposal.
func (cdp *CommunityPoolWithdrawProposal) ProposalRoute() string { return RouterKey }
// ProposalType returns the type of a community pool withdraw proposal.
func (cdp *CommunityPoolWithdrawProposal) ProposalType() string {
return ProposalTypeCommunityPoolWithdraw
}
// String implements fmt.Stringer
func (cdp *CommunityPoolWithdrawProposal) String() string {
var b strings.Builder
b.WriteString(fmt.Sprintf(`Community Pool Withdraw Proposal:
Title: %s
Description: %s
Amount: %s
`, cdp.Title, cdp.Description, cdp.Amount))
return b.String()
}
// ValidateBasic stateless validation of a community pool multi-spend proposal.
func (cdp *CommunityPoolWithdrawProposal) ValidateBasic() error {
err := govtypes.ValidateAbstract(cdp)
if err != nil {
return err
}
return cdp.Amount.Validate()
}

1285
x/earn/types/proposal.pb.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -445,6 +445,22 @@ func (suite *InterestTestSuite) TestAPYToSPY() {
}, },
false, false,
}, },
{
"5% apy",
args{
apy: sdk.MustNewDecFromStr("1.05"),
expectedValue: sdk.MustNewDecFromStr("1.000000001547125958"),
},
false,
},
{
"25% apy",
args{
apy: sdk.MustNewDecFromStr("1.25"),
expectedValue: sdk.MustNewDecFromStr("1.000000007075835620"),
},
false,
},
{ {
"medium-high apy", "medium-high apy",
args{ args{

View File

@ -18,6 +18,9 @@ const (
// KavaDistMacc module account for kavadist // KavaDistMacc module account for kavadist
KavaDistMacc = ModuleName KavaDistMacc = ModuleName
// Treasury
FundModuleAccount = "kava-fund"
) )
var ( var (

View File

@ -129,12 +129,12 @@ func (m msgServer) WithdrawBurn(goCtx context.Context, msg *types.MsgWithdrawBur
return nil, err return nil, err
} }
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS) withdrawnAmount, err := m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount) _, err = m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, withdrawnAmount)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -169,12 +169,12 @@ func (m msgServer) WithdrawBurnUndelegate(goCtx context.Context, msg *types.MsgW
return nil, err return nil, err
} }
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS) withdrawnAmount, err := m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil { if err != nil {
return nil, err return nil, err
} }
sharesReturned, err := m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount) sharesReturned, err := m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, withdrawnAmount)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -30,5 +30,5 @@ type LiquidKeeper interface {
type EarnKeeper interface { type EarnKeeper interface {
Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositStrategy earntypes.StrategyType) error Deposit(ctx sdk.Context, depositor sdk.AccAddress, amount sdk.Coin, depositStrategy earntypes.StrategyType) error
Withdraw(ctx sdk.Context, from sdk.AccAddress, wantAmount sdk.Coin, withdrawStrategy earntypes.StrategyType) error Withdraw(ctx sdk.Context, from sdk.AccAddress, wantAmount sdk.Coin, withdrawStrategy earntypes.StrategyType) (sdk.Coin, error)
} }