Add Combined Earn and Liquid msgs (#1305)

* 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
This commit is contained in:
Ruaridh 2022-09-28 03:28:57 +01:00 committed by GitHub
parent 314f733cb8
commit 9519690324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 3906 additions and 2 deletions

View File

@ -131,6 +131,9 @@ import (
pricefeed "github.com/kava-labs/kava/x/pricefeed" pricefeed "github.com/kava-labs/kava/x/pricefeed"
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
"github.com/kava-labs/kava/x/router"
routerkeeper "github.com/kava-labs/kava/x/router/keeper"
routertypes "github.com/kava-labs/kava/x/router/types"
savings "github.com/kava-labs/kava/x/savings" savings "github.com/kava-labs/kava/x/savings"
savingskeeper "github.com/kava-labs/kava/x/savings/keeper" savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
savingstypes "github.com/kava-labs/kava/x/savings/types" savingstypes "github.com/kava-labs/kava/x/savings/types"
@ -195,6 +198,7 @@ var (
evmutil.AppModuleBasic{}, evmutil.AppModuleBasic{},
liquid.AppModuleBasic{}, liquid.AppModuleBasic{},
earn.AppModuleBasic{}, earn.AppModuleBasic{},
router.AppModuleBasic{},
) )
// module account permissions // module account permissions
@ -291,6 +295,7 @@ type App struct {
savingsKeeper savingskeeper.Keeper savingsKeeper savingskeeper.Keeper
liquidKeeper liquidkeeper.Keeper liquidKeeper liquidkeeper.Keeper
earnKeeper earnkeeper.Keeper earnKeeper earnkeeper.Keeper
routerKeeper routerkeeper.Keeper
// make scoped keepers public for test purposes // make scoped keepers public for test purposes
ScopedIBCKeeper capabilitykeeper.ScopedKeeper ScopedIBCKeeper capabilitykeeper.ScopedKeeper
@ -624,6 +629,11 @@ func NewApp(
nil, nil,
&earnKeeper, &earnKeeper,
) )
app.routerKeeper = routerkeeper.NewKeeper(
&app.earnKeeper,
app.liquidKeeper,
&app.stakingKeeper,
)
// create committee keeper with router // create committee keeper with router
committeeGovRouter := govtypes.NewRouter() committeeGovRouter := govtypes.NewRouter()
@ -718,6 +728,7 @@ func NewApp(
savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper), savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
liquid.NewAppModule(app.liquidKeeper), liquid.NewAppModule(app.liquidKeeper),
earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper), earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
router.NewAppModule(app.routerKeeper),
) )
// Warning: Some begin blockers must run before others. Ensure the dependencies are understood before modifying this list. // Warning: Some begin blockers must run before others. Ensure the dependencies are understood before modifying this list.
@ -766,6 +777,7 @@ func NewApp(
savingstypes.ModuleName, savingstypes.ModuleName,
liquidtypes.ModuleName, liquidtypes.ModuleName,
earntypes.ModuleName, earntypes.ModuleName,
routertypes.ModuleName,
) )
// Warning: Some end blockers must run before others. Ensure the dependencies are understood before modifying this list. // Warning: Some end blockers must run before others. Ensure the dependencies are understood before modifying this list.
@ -806,6 +818,7 @@ func NewApp(
savingstypes.ModuleName, savingstypes.ModuleName,
liquidtypes.ModuleName, liquidtypes.ModuleName,
earntypes.ModuleName, earntypes.ModuleName,
routertypes.ModuleName,
) )
// Warning: Some init genesis methods must run before others. Ensure the dependencies are understood before modifying this list // Warning: Some init genesis methods must run before others. Ensure the dependencies are understood before modifying this list
@ -845,6 +858,7 @@ func NewApp(
upgradetypes.ModuleName, upgradetypes.ModuleName,
validatorvestingtypes.ModuleName, validatorvestingtypes.ModuleName,
liquidtypes.ModuleName, liquidtypes.ModuleName,
routertypes.ModuleName,
) )
app.mm.RegisterInvariants(&app.crisisKeeper) app.mm.RegisterInvariants(&app.crisisKeeper)

View File

@ -42,6 +42,7 @@ import (
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper" kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper" liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
routerkeeper "github.com/kava-labs/kava/x/router/keeper"
savingskeeper "github.com/kava-labs/kava/x/savings/keeper" savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
swapkeeper "github.com/kava-labs/kava/x/swap/keeper" swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
) )
@ -112,6 +113,7 @@ func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper { return tApp.sa
func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper } func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.liquidKeeper } func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.liquidKeeper }
func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper } func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper }
func (tApp TestApp) GetRouterKeeper() routerkeeper.Keeper { return tApp.routerKeeper }
func (tApp TestApp) GetStoreKey(s string) sdk.StoreKey { return tApp.keys[s] } func (tApp TestApp) GetStoreKey(s string) sdk.StoreKey { return tApp.keys[s] }

View File

@ -3,6 +3,7 @@ set -e
validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover" validatorMnemonic="equip town gesture square tomorrow volume nephew minute witness beef rich gadget actress egg sing secret pole winter alarm law today check violin uncover"
# kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c # kava1ffv7nhd3z6sych2qpqkk03ec6hzkmufy0r2s4c
# kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0
faucetMnemonic="crash sort dwarf disease change advice attract clump avoid mobile clump right junior axis book fresh mask tube front require until face effort vault" faucetMnemonic="crash sort dwarf disease change advice attract clump avoid mobile clump right junior axis book fresh mask tube front require until face effort vault"
# kava1adkm6svtzjsxxvg7g6rshg6kj9qwej8gwqadqd # kava1adkm6svtzjsxxvg7g6rshg6kj9qwej8gwqadqd
@ -92,5 +93,14 @@ jq '.app_state.evm.params.chain_config.merge_fork_block = null' $DATA/config/gen
jq '.app_state.earn.params.allowed_vaults = [ jq '.app_state.earn.params.allowed_vaults = [
{ {
denom: "usdx", denom: "usdx",
vault_strategy: 1, strategies: ["STRATEGY_TYPE_HARD"],
},
{
denom: "bkava",
strategies: ["STRATEGY_TYPE_SAVINGS"],
}]' $DATA/config/genesis.json | sponge $DATA/config/genesis.json }]' $DATA/config/genesis.json | sponge $DATA/config/genesis.json
jq '.app_state.savings.params.supported_denoms = ["bkava-kavavaloper1ffv7nhd3z6sych2qpqkk03ec6hzkmufyz4scd0"]' $DATA/config/genesis.json | sponge $DATA/config/genesis.json
$BINARY config broadcast-mode block

View File

@ -430,6 +430,18 @@
- [Msg](#kava.pricefeed.v1beta1.Msg) - [Msg](#kava.pricefeed.v1beta1.Msg)
- [kava/router/v1beta1/tx.proto](#kava/router/v1beta1/tx.proto)
- [MsgDelegateMintDeposit](#kava.router.v1beta1.MsgDelegateMintDeposit)
- [MsgDelegateMintDepositResponse](#kava.router.v1beta1.MsgDelegateMintDepositResponse)
- [MsgMintDeposit](#kava.router.v1beta1.MsgMintDeposit)
- [MsgMintDepositResponse](#kava.router.v1beta1.MsgMintDepositResponse)
- [MsgWithdrawBurn](#kava.router.v1beta1.MsgWithdrawBurn)
- [MsgWithdrawBurnResponse](#kava.router.v1beta1.MsgWithdrawBurnResponse)
- [MsgWithdrawBurnUndelegate](#kava.router.v1beta1.MsgWithdrawBurnUndelegate)
- [MsgWithdrawBurnUndelegateResponse](#kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse)
- [Msg](#kava.router.v1beta1.Msg)
- [kava/savings/v1beta1/store.proto](#kava/savings/v1beta1/store.proto) - [kava/savings/v1beta1/store.proto](#kava/savings/v1beta1/store.proto)
- [Deposit](#kava.savings.v1beta1.Deposit) - [Deposit](#kava.savings.v1beta1.Deposit)
- [Params](#kava.savings.v1beta1.Params) - [Params](#kava.savings.v1beta1.Params)
@ -5957,6 +5969,145 @@ Msg defines the pricefeed Msg service.
<a name="kava/router/v1beta1/tx.proto"></a>
<p align="right"><a href="#top">Top</a></p>
## kava/router/v1beta1/tx.proto
<a name="kava.router.v1beta1.MsgDelegateMintDeposit"></a>
### MsgDelegateMintDeposit
MsgDelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
then deposits to an earn vault.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `depositor` | [string](#string) | | depositor represents the owner of the tokens to delegate |
| `validator` | [string](#string) | | validator is the address of the validator to delegate to |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the tokens to delegate |
<a name="kava.router.v1beta1.MsgDelegateMintDepositResponse"></a>
### MsgDelegateMintDepositResponse
MsgDelegateMintDepositResponse defines the Msg/MsgDelegateMintDeposit response type.
<a name="kava.router.v1beta1.MsgMintDeposit"></a>
### MsgMintDeposit
MsgMintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `depositor` | [string](#string) | | depositor represents the owner of the delegation to convert |
| `validator` | [string](#string) | | validator is the validator for the depositor's delegation |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the delegation balance to convert |
<a name="kava.router.v1beta1.MsgMintDepositResponse"></a>
### MsgMintDepositResponse
MsgMintDepositResponse defines the Msg/MsgMintDeposit response type.
<a name="kava.router.v1beta1.MsgWithdrawBurn"></a>
### MsgWithdrawBurn
MsgWithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `from` | [string](#string) | | from is the owner of the earn vault to withdraw from |
| `validator` | [string](#string) | | validator is the address to select the derivative denom to withdraw |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the staked token equivalent to withdraw |
<a name="kava.router.v1beta1.MsgWithdrawBurnResponse"></a>
### MsgWithdrawBurnResponse
MsgWithdrawBurnResponse defines the Msg/MsgWithdrawBurn response type.
<a name="kava.router.v1beta1.MsgWithdrawBurnUndelegate"></a>
### MsgWithdrawBurnUndelegate
MsgWithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
then undelegates them from their validator.
| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `from` | [string](#string) | | from is the owner of the earn vault to withdraw from |
| `validator` | [string](#string) | | validator is the address to select the derivative denom to withdraw |
| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the staked token equivalent to withdraw |
<a name="kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse"></a>
### MsgWithdrawBurnUndelegateResponse
MsgWithdrawBurnUndelegateResponse defines the Msg/MsgWithdrawBurnUndelegate response type.
<!-- end messages -->
<!-- end enums -->
<!-- end HasExtensions -->
<a name="kava.router.v1beta1.Msg"></a>
### Msg
Msg defines the router Msg service.
| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint |
| ----------- | ------------ | ------------- | ------------| ------- | -------- |
| `MintDeposit` | [MsgMintDeposit](#kava.router.v1beta1.MsgMintDeposit) | [MsgMintDepositResponse](#kava.router.v1beta1.MsgMintDepositResponse) | MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault. | |
| `DelegateMintDeposit` | [MsgDelegateMintDeposit](#kava.router.v1beta1.MsgDelegateMintDeposit) | [MsgDelegateMintDepositResponse](#kava.router.v1beta1.MsgDelegateMintDepositResponse) | DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives, then deposits to an earn vault. | |
| `WithdrawBurn` | [MsgWithdrawBurn](#kava.router.v1beta1.MsgWithdrawBurn) | [MsgWithdrawBurnResponse](#kava.router.v1beta1.MsgWithdrawBurnResponse) | WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation. | |
| `WithdrawBurnUndelegate` | [MsgWithdrawBurnUndelegate](#kava.router.v1beta1.MsgWithdrawBurnUndelegate) | [MsgWithdrawBurnUndelegateResponse](#kava.router.v1beta1.MsgWithdrawBurnUndelegateResponse) | WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation, then undelegates them from their validator. | |
<!-- end services -->
<a name="kava/savings/v1beta1/store.proto"></a> <a name="kava/savings/v1beta1/store.proto"></a>
<p align="right"><a href="#top">Top</a></p> <p align="right"><a href="#top">Top</a></p>

View File

@ -0,0 +1,76 @@
syntax = "proto3";
package kava.router.v1beta1;
import "cosmos_proto/cosmos.proto";
import "cosmos/base/v1beta1/coin.proto";
import "gogoproto/gogo.proto";
option go_package = "github.com/kava-labs/kava/x/router/types";
option (gogoproto.goproto_getters_all) = false;
// Msg defines the router Msg service.
service Msg {
// MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
rpc MintDeposit(MsgMintDeposit) returns (MsgMintDepositResponse);
// DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
// then deposits to an earn vault.
rpc DelegateMintDeposit(MsgDelegateMintDeposit) returns (MsgDelegateMintDepositResponse);
// WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
rpc WithdrawBurn(MsgWithdrawBurn) returns (MsgWithdrawBurnResponse);
// WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
// then undelegates them from their validator.
rpc WithdrawBurnUndelegate(MsgWithdrawBurnUndelegate) returns (MsgWithdrawBurnUndelegateResponse);
}
// MsgMintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault.
message MsgMintDeposit {
// depositor represents the owner of the delegation to convert
string depositor = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// validator is the validator for the depositor's delegation
string validator = 2;
// amount is the delegation balance to convert
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// MsgMintDepositResponse defines the Msg/MsgMintDeposit response type.
message MsgMintDepositResponse {}
// MsgDelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
// then deposits to an earn vault.
message MsgDelegateMintDeposit {
// depositor represents the owner of the tokens to delegate
string depositor = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// validator is the address of the validator to delegate to
string validator = 2;
// amount is the tokens to delegate
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// MsgDelegateMintDepositResponse defines the Msg/MsgDelegateMintDeposit response type.
message MsgDelegateMintDepositResponse {}
// MsgWithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
message MsgWithdrawBurn {
// from is the owner of the earn vault to withdraw from
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// validator is the address to select the derivative denom to withdraw
string validator = 2;
// amount is the staked token equivalent to withdraw
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// MsgWithdrawBurnResponse defines the Msg/MsgWithdrawBurn response type.
message MsgWithdrawBurnResponse {}
// MsgWithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
// then undelegates them from their validator.
message MsgWithdrawBurnUndelegate {
// from is the owner of the earn vault to withdraw from
string from = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"];
// validator is the address to select the derivative denom to withdraw
string validator = 2;
// amount is the staked token equivalent to withdraw
cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false];
}
// MsgWithdrawBurnUndelegateResponse defines the Msg/MsgWithdrawBurnUndelegate response type.
message MsgWithdrawBurnUndelegateResponse {}

View File

@ -155,3 +155,21 @@ func (k Keeper) burnCoins(ctx sdk.Context, sender sdk.AccAddress, amount sdk.Coi
} }
return nil return nil
} }
// DerivativeFromTokens calculates the approximate amount of derivative coins that would be minted for a given amount of staking tokens.
func (k Keeper) DerivativeFromTokens(ctx sdk.Context, valAddr sdk.ValAddress, tokens sdk.Coin) (sdk.Coin, error) {
bondDenom := k.stakingKeeper.BondDenom(ctx)
if tokens.Denom != bondDenom {
return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidDenom, "'%s' does not match staking denom '%s'", tokens.Denom, bondDenom)
}
// Use GetModuleAddress instead of GetModuleAccount to avoid creating a module account if it doesn't exist.
modAddress := k.accountKeeper.GetModuleAddress(types.ModuleAccountName)
derivative, _, err := k.CalculateDerivativeSharesFromTokens(ctx, modAddress, valAddr, tokens.Amount)
if err != nil {
return sdk.Coin{}, err
}
liquidTokenDenom := k.GetLiquidStakingTokenDenom(valAddr)
liquidToken := sdk.NewCoin(liquidTokenDenom, derivative)
return liquidToken, nil
}

View File

@ -469,3 +469,27 @@ func (suite *KeeperTestSuite) TestGetStakedTokensForDerivatives() {
}) })
} }
} }
func (suite *KeeperTestSuite) TestDerivativeFromTokens() {
_, addrs := app.GeneratePrivKeyAddressPairs(1)
valAccAddr := addrs[0]
valAddr := sdk.ValAddress(valAccAddr)
moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName)
initialBalance := i(1e9)
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance))
suite.AddCoinsToModule(types.ModuleAccountName, suite.NewBondCoins(initialBalance))
suite.CreateNewUnbondedValidator(valAddr, initialBalance)
suite.CreateDelegation(valAddr, moduleAccAddress, initialBalance)
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
_, err := suite.Keeper.DerivativeFromTokens(suite.Ctx, valAddr, sdk.NewCoin("invalid", initialBalance))
suite.ErrorIs(err, types.ErrInvalidDenom)
derivatives, err := suite.Keeper.DerivativeFromTokens(suite.Ctx, valAddr, suite.NewBondCoin(initialBalance))
suite.NoError(err)
expected := sdk.NewCoin(fmt.Sprintf("bkava-%s", valAddr), initialBalance)
suite.Equal(expected, derivatives)
}

View File

@ -21,6 +21,7 @@ type BankKeeper interface {
// AccountKeeper defines the expected keeper interface for interacting with account // AccountKeeper defines the expected keeper interface for interacting with account
type AccountKeeper interface { type AccountKeeper interface {
GetModuleAddress(moduleName string) sdk.AccAddress
GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI
} }

174
x/router/client/cli/tx.go Normal file
View File

@ -0,0 +1,174 @@
package cli
import (
"fmt"
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/tx"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/version"
"github.com/kava-labs/kava/x/router/types"
)
// GetTxCmd returns the transaction commands for this module
func GetTxCmd() *cobra.Command {
liquidTxCmd := &cobra.Command{
Use: types.ModuleName,
Short: "router transactions subcommands",
DisableFlagParsing: true,
SuggestionsMinimumDistance: 2,
RunE: client.ValidateCmd,
}
cmds := []*cobra.Command{
getCmdMintDeposit(),
getCmdDelegateMintDeposit(),
getCmdWithdrawBurn(),
getCmdWithdrawBurnUndelegate(),
}
for _, cmd := range cmds {
flags.AddTxFlagsToCmd(cmd)
}
liquidTxCmd.AddCommand(cmds...)
return liquidTxCmd
}
func getCmdMintDeposit() *cobra.Command {
return &cobra.Command{
Use: "mint-deposit [validator-addr] [amount]",
Short: "mints staking derivative from a delegation and deposits them to earn",
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf(
`%s tx %s mint-deposit kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
valAddr, err := sdk.ValAddressFromBech32(args[0])
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
coin, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return err
}
msg := types.NewMsgMintDeposit(clientCtx.GetFromAddress(), valAddr, coin)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
}
func getCmdDelegateMintDeposit() *cobra.Command {
return &cobra.Command{
Use: "delegate-mint-deposit [validator-addr] [amount]",
Short: "delegates tokens, mints staking derivative from a them, and deposits them to earn",
Args: cobra.ExactArgs(2),
Example: fmt.Sprintf(
`%s tx %s delegate-mint-deposit kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
valAddr, err := sdk.ValAddressFromBech32(args[0])
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
coin, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return err
}
msg := types.NewMsgDelegateMintDeposit(clientCtx.GetFromAddress(), valAddr, coin)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
}
func getCmdWithdrawBurn() *cobra.Command {
return &cobra.Command{
Use: "withdraw-burn [validator-addr] [amount]",
Short: "withdraws staking derivatives from earn and burns them to redeem a delegation",
Example: fmt.Sprintf(
`%s tx %s withdraw-burn kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
valAddr, err := sdk.ValAddressFromBech32(args[0])
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
amount, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return err
}
msg := types.NewMsgWithdrawBurn(clientCtx.GetFromAddress(), valAddr, amount)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
}
func getCmdWithdrawBurnUndelegate() *cobra.Command {
return &cobra.Command{
Use: "withdraw-burn-undelegate [validator-addr] [amount]",
Short: "withdraws staking derivatives from earn, burns them to redeem a delegation, then unstakes the delegation",
Example: fmt.Sprintf(
`%s tx %s withdraw-burn-undelegate kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from <key>`, version.AppName, types.ModuleName,
),
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
clientCtx, err := client.GetClientTxContext(cmd)
if err != nil {
return err
}
valAddr, err := sdk.ValAddressFromBech32(args[0])
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
}
amount, err := sdk.ParseCoinNormalized(args[1])
if err != nil {
return err
}
msg := types.NewMsgWithdrawBurnUndelegate(clientCtx.GetFromAddress(), valAddr, amount)
if err := msg.ValidateBasic(); err != nil {
return err
}
return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg)
},
}
}

26
x/router/keeper/keeper.go Normal file
View File

@ -0,0 +1,26 @@
package keeper
import (
"github.com/kava-labs/kava/x/router/types"
)
// Keeper is the keeper for the module
type Keeper struct {
earnKeeper types.EarnKeeper
liquidKeeper types.LiquidKeeper
stakingKeeper types.StakingKeeper
}
// NewKeeper creates a new keeper
func NewKeeper(
earnKeeper types.EarnKeeper,
liquidKeeper types.LiquidKeeper,
stakingKeeper types.StakingKeeper,
) Keeper {
return Keeper{
earnKeeper: earnKeeper,
liquidKeeper: liquidKeeper,
stakingKeeper: stakingKeeper,
}
}

View File

@ -0,0 +1,201 @@
package keeper
import (
"context"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
earntypes "github.com/kava-labs/kava/x/earn/types"
"github.com/kava-labs/kava/x/router/types"
)
type msgServer struct {
keeper Keeper
}
// NewMsgServerImpl returns an implementation of the module's MsgServer interface
// for the provided Keeper.
func NewMsgServerImpl(keeper Keeper) types.MsgServer {
return &msgServer{keeper: keeper}
}
var _ types.MsgServer = msgServer{}
// MintDeposit converts a delegation into staking derivatives and deposits it all into an earn vault
func (m msgServer) MintDeposit(goCtx context.Context, msg *types.MsgMintDeposit) (*types.MsgMintDepositResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
depositor, err := sdk.AccAddressFromBech32(msg.Depositor)
if err != nil {
return nil, err
}
val, err := sdk.ValAddressFromBech32(msg.Validator)
if err != nil {
return nil, err
}
derivative, err := m.keeper.liquidKeeper.MintDerivative(ctx, depositor, val, msg.Amount)
if err != nil {
return nil, err
}
err = m.keeper.earnKeeper.Deposit(ctx, depositor, derivative, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
),
)
return &types.MsgMintDepositResponse{}, nil
}
// DelegateMintDeposit delegates tokens to a validator, then converts them into staking derivatives,
// then deposits to an earn vault.
func (m msgServer) DelegateMintDeposit(goCtx context.Context, msg *types.MsgDelegateMintDeposit) (*types.MsgDelegateMintDepositResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
depositor, err := sdk.AccAddressFromBech32(msg.Depositor)
if err != nil {
return nil, err
}
valAddr, err := sdk.ValAddressFromBech32(msg.Validator)
if err != nil {
return nil, err
}
validator, found := m.keeper.stakingKeeper.GetValidator(ctx, valAddr)
if !found {
return nil, stakingtypes.ErrNoValidatorFound
}
bondDenom := m.keeper.stakingKeeper.BondDenom(ctx)
if msg.Amount.Denom != bondDenom {
return nil, sdkerrors.Wrapf(
sdkerrors.ErrInvalidRequest, "invalid coin denomination: got %s, expected %s", msg.Amount.Denom, bondDenom,
)
}
newShares, err := m.keeper.stakingKeeper.Delegate(ctx, depositor, msg.Amount.Amount, stakingtypes.Unbonded, validator, true)
if err != nil {
return nil, err
}
derivativeMinted, err := m.keeper.liquidKeeper.MintDerivative(ctx, depositor, valAddr, msg.Amount)
if err != nil {
return nil, err
}
err = m.keeper.earnKeeper.Deposit(ctx, depositor, derivativeMinted, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
stakingtypes.EventTypeDelegate,
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, valAddr.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, newShares.String()),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
),
})
return &types.MsgDelegateMintDepositResponse{}, nil
}
// WithdrawBurn removes staking derivatives from an earn vault and converts them back to a staking delegation.
func (m msgServer) WithdrawBurn(goCtx context.Context, msg *types.MsgWithdrawBurn) (*types.MsgWithdrawBurnResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
depositor, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return nil, err
}
val, err := sdk.ValAddressFromBech32(msg.Validator)
if err != nil {
return nil, err
}
tokenAmount, err := m.keeper.liquidKeeper.DerivativeFromTokens(ctx, val, msg.Amount)
if err != nil {
return nil, err
}
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil {
return nil, err
}
_, err = m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvent(
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
),
)
return &types.MsgWithdrawBurnResponse{}, nil
}
// WithdrawBurnUndelegate removes staking derivatives from an earn vault, converts them to a staking delegation,
// then undelegates them from their validator.
func (m msgServer) WithdrawBurnUndelegate(goCtx context.Context, msg *types.MsgWithdrawBurnUndelegate) (*types.MsgWithdrawBurnUndelegateResponse, error) {
ctx := sdk.UnwrapSDKContext(goCtx)
depositor, err := sdk.AccAddressFromBech32(msg.From)
if err != nil {
return nil, err
}
val, err := sdk.ValAddressFromBech32(msg.Validator)
if err != nil {
return nil, err
}
tokenAmount, err := m.keeper.liquidKeeper.DerivativeFromTokens(ctx, val, msg.Amount)
if err != nil {
return nil, err
}
err = m.keeper.earnKeeper.Withdraw(ctx, depositor, tokenAmount, earntypes.STRATEGY_TYPE_SAVINGS)
if err != nil {
return nil, err
}
sharesReturned, err := m.keeper.liquidKeeper.BurnDerivative(ctx, depositor, val, tokenAmount)
if err != nil {
return nil, err
}
completionTime, err := m.keeper.stakingKeeper.Undelegate(ctx, depositor, val, sharesReturned)
if err != nil {
return nil, err
}
ctx.EventManager().EmitEvents(sdk.Events{
sdk.NewEvent(
stakingtypes.EventTypeUnbond,
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, val.String()),
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)),
),
sdk.NewEvent(
sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, depositor.String()),
),
})
return &types.MsgWithdrawBurnUndelegateResponse{}, nil
}

View File

@ -0,0 +1,321 @@
package keeper_test
import (
"fmt"
"testing"
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/x/staking"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/stretchr/testify/suite"
"github.com/kava-labs/kava/app"
earntypes "github.com/kava-labs/kava/x/earn/types"
"github.com/kava-labs/kava/x/router/keeper"
"github.com/kava-labs/kava/x/router/testutil"
"github.com/kava-labs/kava/x/router/types"
)
type msgServerTestSuite struct {
testutil.Suite
msgServer types.MsgServer
}
func (suite *msgServerTestSuite) SetupTest() {
suite.Suite.SetupTest()
suite.msgServer = keeper.NewMsgServerImpl(suite.Keeper)
}
func TestMsgServerTestSuite(t *testing.T) {
suite.Run(t, new(msgServerTestSuite))
}
func (suite *msgServerTestSuite) TestMintDeposit_Events() {
user, valAddr, delegation := suite.setupValidatorAndDelegation()
suite.setupEarnForDeposits(valAddr)
msg := types.NewMsgMintDeposit(
user,
valAddr,
suite.NewBondCoin(delegation),
)
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
),
)
}
func (suite *msgServerTestSuite) TestDelegateMintDeposit_Events() {
user, valAddr, balance := suite.setupValidator()
suite.setupEarnForDeposits(valAddr)
msg := types.NewMsgDelegateMintDeposit(
user,
valAddr,
suite.NewBondCoin(balance),
)
_, err := suite.msgServer.DelegateMintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
),
)
expectedShares := msg.Amount.Amount.ToDec() // no slashes so shares equal staked tokens
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(
stakingtypes.EventTypeDelegate,
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, msg.Validator),
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
sdk.NewAttribute(stakingtypes.AttributeKeyNewShares, expectedShares.String()),
),
)
}
func (suite *msgServerTestSuite) TestWithdrawBurn_Events() {
user, valAddr, delegated := suite.setupDerivatives()
// clear events from setup
suite.Ctx = suite.Ctx.WithEventManager(sdk.NewEventManager())
msg := types.NewMsgWithdrawBurn(
user,
valAddr,
// in this setup where the validator is not slashed, the derivative amount is equal to the staked balance
suite.NewBondCoin(delegated.Amount),
)
_, err := suite.msgServer.WithdrawBurn(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
),
)
}
func (suite *msgServerTestSuite) TestWithdrawBurnUndelegate_Events() {
user, valAddr, delegated := suite.setupDerivatives()
// clear events from setup
suite.Ctx = suite.Ctx.WithEventManager(sdk.NewEventManager())
msg := types.NewMsgWithdrawBurnUndelegate(
user,
valAddr,
// in this setup where the validator is not slashed, the derivative amount is equal to the staked balance
suite.NewBondCoin(delegated.Amount),
)
_, err := suite.msgServer.WithdrawBurnUndelegate(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(sdk.EventTypeMessage,
sdk.NewAttribute(sdk.AttributeKeyModule, types.ModuleName),
sdk.NewAttribute(sdk.AttributeKeySender, user.String()),
),
)
unbondingTime := suite.StakingKeeper.UnbondingTime(suite.Ctx)
completionTime := suite.Ctx.BlockTime().Add(unbondingTime)
suite.EventsContains(suite.Ctx.EventManager().Events(),
sdk.NewEvent(
stakingtypes.EventTypeUnbond,
sdk.NewAttribute(stakingtypes.AttributeKeyValidator, msg.Validator),
sdk.NewAttribute(sdk.AttributeKeyAmount, msg.Amount.String()),
sdk.NewAttribute(stakingtypes.AttributeKeyCompletionTime, completionTime.Format(time.RFC3339)),
),
)
}
func (suite *msgServerTestSuite) TestMintDepositAndWithdrawBurn_TransferEntireBalance() {
_, addrs := app.GeneratePrivKeyAddressPairs(5)
valAccAddr, user := addrs[0], addrs[1]
valAddr := sdk.ValAddress(valAccAddr)
derivativeDenom := suite.setupEarnForDeposits(valAddr)
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(sdk.NewInt(1e9)))
vesting := sdk.NewInt(1000)
suite.CreateVestingAccountWithAddress(user,
suite.NewBondCoins(sdk.NewInt(1e9).Add(vesting)),
suite.NewBondCoins(vesting),
)
// Create a slashed validator, where the delegator owns fractional tokens.
suite.CreateNewUnbondedValidator(valAddr, sdk.NewInt(1e9))
suite.CreateDelegation(valAddr, user, sdk.NewInt(1e9))
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
suite.SlashValidator(valAddr, sdk.MustNewDecFromStr("0.666666666666666667"))
// Query the full staked balance and convert it all to derivatives
// user technically 333_333_333.333333333333333333 staked tokens without rounding
delegation := suite.QueryStaking_Delegation(valAddr, user)
suite.Equal(sdk.NewInt(333_333_333), delegation.Balance.Amount)
msgDeposit := types.NewMsgMintDeposit(
user,
valAddr,
delegation.Balance,
)
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msgDeposit)
suite.Require().NoError(err)
// There should be no extractable balance left in delegation
suite.DelegationBalanceLessThan(valAddr, user, sdk.NewInt(1))
// All derivative coins should be deposited to earn
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
// Earn vault has all minted derivatives
suite.VaultAccountValueEqual(user, sdk.NewInt64Coin(derivativeDenom, 999_999_998)) // 2 lost in conversion
// Query the full kava balance of the earn deposit and convert all to a delegation
deposit := suite.QueryEarn_VaultValue(user, "bkava")
suite.Equal(suite.NewBondCoins(sdk.NewInt(333_333_332)), deposit.Value) // 1 lost due to lost shares
msgWithdraw := types.NewMsgWithdrawBurn(
user,
valAddr,
deposit.Value[0],
)
_, err = suite.msgServer.WithdrawBurn(sdk.WrapSDKContext(suite.Ctx), msgWithdraw)
suite.Require().NoError(err)
// There should be no earn deposit left (earn removes dust amounts)
suite.VaultAccountSharesEqual(user, nil)
// All derivative coins should be converted to a delegation
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
// The user should get back most of their original deposited balance
suite.DelegationBalanceInDeltaBelow(valAddr, user, sdk.NewInt(333_333_332), sdk.NewInt(2))
}
func (suite *msgServerTestSuite) TestDelegateMintDepositAndWithdrawBurnUndelegate_TransferEntireBalance() {
_, addrs := app.GeneratePrivKeyAddressPairs(5)
valAccAddr, user := addrs[0], addrs[1]
valAddr := sdk.ValAddress(valAccAddr)
derivativeDenom := suite.setupEarnForDeposits(valAddr)
valBalance := sdk.NewInt(1e9)
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(valBalance))
// Create a slashed validator, where a future delegator will own fractional tokens.
suite.CreateNewUnbondedValidator(valAddr, valBalance)
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
suite.SlashValidator(valAddr, sdk.MustNewDecFromStr("0.4")) // tokens remaining 600_000_000
userBalance := sdk.NewInt(100e6)
vesting := sdk.NewInt(1000)
suite.CreateVestingAccountWithAddress(user,
suite.NewBondCoins(userBalance.Add(vesting)),
suite.NewBondCoins(vesting),
)
// Query the full vested balance and convert it all to derivatives
balance := suite.QueryBank_SpendableBalance(user)
suite.Equal(suite.NewBondCoins(userBalance), balance)
// When delegation is created it will have 166_666_666.666666666666666666 shares
// newShares = validatorShares * newTokens/validatorTokens, truncated to 18 decimals
msgDeposit := types.NewMsgDelegateMintDeposit(
user,
valAddr,
balance[0],
)
_, err := suite.msgServer.DelegateMintDeposit(sdk.WrapSDKContext(suite.Ctx), msgDeposit)
suite.Require().NoError(err)
// All spendable balance should be withdrawn
suite.AccountSpendableBalanceEqual(user, nil)
// Since shares are newly created, the exact amount should be converted to derivatives, leaving none behind
suite.DelegationSharesEqual(valAddr, user, sdk.ZeroDec())
// All derivative coins should be deposited to earn
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
suite.VaultAccountValueEqual(user, sdk.NewInt64Coin(derivativeDenom, 166_666_666))
// Query the full kava balance of the earn deposit and convert all to a delegation
deposit := suite.QueryEarn_VaultValue(user, "bkava")
suite.Equal(suite.NewBondCoins(sdk.NewInt(99_999_999)), deposit.Value) // 1 lost due to truncating shares to derivatives
msgWithdraw := types.NewMsgWithdrawBurnUndelegate(
user,
valAddr,
deposit.Value[0],
)
_, err = suite.msgServer.WithdrawBurnUndelegate(sdk.WrapSDKContext(suite.Ctx), msgWithdraw)
suite.Require().NoError(err)
// There should be no earn deposit left (earn removes dust amounts)
suite.VaultAccountSharesEqual(user, nil)
// All derivative coins should be converted to a delegation
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
// There should be zero shares left because undelegate removes all created by burn.
suite.AccountBalanceOfEqual(user, derivativeDenom, sdk.ZeroInt())
// User should have most of their original balance back in an unbonding delegation
suite.UnbondingDelegationInDeltaBelow(valAddr, user, userBalance, sdk.NewInt(2))
}
func (suite *msgServerTestSuite) setupValidator() (sdk.AccAddress, sdk.ValAddress, sdk.Int) {
_, addrs := app.GeneratePrivKeyAddressPairs(5)
valAccAddr, user := addrs[0], addrs[1]
valAddr := sdk.ValAddress(valAccAddr)
balance := sdk.NewInt(1e9)
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(balance))
suite.CreateAccountWithAddress(user, suite.NewBondCoins(balance))
suite.CreateNewUnbondedValidator(valAddr, balance)
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
return user, valAddr, balance
}
func (suite *msgServerTestSuite) setupValidatorAndDelegation() (sdk.AccAddress, sdk.ValAddress, sdk.Int) {
_, addrs := app.GeneratePrivKeyAddressPairs(5)
valAccAddr, user := addrs[0], addrs[1]
valAddr := sdk.ValAddress(valAccAddr)
balance := sdk.NewInt(1e9)
suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(balance))
suite.CreateAccountWithAddress(user, suite.NewBondCoins(balance))
suite.CreateNewUnbondedValidator(valAddr, balance)
suite.CreateDelegation(valAddr, user, balance)
staking.EndBlocker(suite.Ctx, suite.StakingKeeper)
return user, valAddr, balance
}
func (suite *msgServerTestSuite) setupEarnForDeposits(valAddr sdk.ValAddress) string {
suite.CreateVault("bkava", earntypes.StrategyTypes{earntypes.STRATEGY_TYPE_SAVINGS}, false, nil)
derivativeDenom := fmt.Sprintf("bkava-%s", valAddr)
suite.SetSavingsSupportedDenoms([]string{derivativeDenom})
return derivativeDenom
}
func (suite *msgServerTestSuite) setupDerivatives() (sdk.AccAddress, sdk.ValAddress, sdk.Coin) {
user, valAddr, delegation := suite.setupValidatorAndDelegation()
suite.setupEarnForDeposits(valAddr)
msg := types.NewMsgMintDeposit(
user,
valAddr,
suite.NewBondCoin(delegation),
)
_, err := suite.msgServer.MintDeposit(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
derivativeDenom := fmt.Sprintf("bkava-%s", valAddr)
derivatives, err := suite.EarnKeeper.GetVaultAccountValue(suite.Ctx, derivativeDenom, user)
suite.Require().NoError(err)
return user, valAddr, derivatives
}

137
x/router/module.go Normal file
View File

@ -0,0 +1,137 @@
package router
import (
"encoding/json"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/codec"
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
"github.com/gorilla/mux"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/spf13/cobra"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/kava-labs/kava/x/router/client/cli"
"github.com/kava-labs/kava/x/router/keeper"
"github.com/kava-labs/kava/x/router/types"
)
var (
_ module.AppModule = AppModule{}
_ module.AppModuleBasic = AppModuleBasic{}
)
// AppModuleBasic app module basics object
type AppModuleBasic struct{}
// Name get module name
func (AppModuleBasic) Name() string {
return types.ModuleName
}
// RegisterLegacyAminoCodec register module codec
func (AppModuleBasic) RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
types.RegisterLegacyAminoCodec(cdc)
}
// DefaultGenesis default genesis state
func (AppModuleBasic) DefaultGenesis(_ codec.JSONCodec) json.RawMessage {
return []byte("{}")
}
// ValidateGenesis module validate genesis
func (AppModuleBasic) ValidateGenesis(_ codec.JSONCodec, _ client.TxEncodingConfig, _ json.RawMessage) error {
return nil
}
// RegisterInterfaces implements InterfaceModule.RegisterInterfaces
func (a AppModuleBasic) RegisterInterfaces(registry codectypes.InterfaceRegistry) {
types.RegisterInterfaces(registry)
}
// RegisterRESTRoutes registers REST routes for the module.
func (a AppModuleBasic) RegisterRESTRoutes(_ client.Context, _ *mux.Router) {}
// RegisterGRPCGatewayRoutes registers the gRPC Gateway routes for the module.
func (a AppModuleBasic) RegisterGRPCGatewayRoutes(_ client.Context, _ *runtime.ServeMux) {
}
// GetTxCmd returns the root tx command for the module.
func (AppModuleBasic) GetTxCmd() *cobra.Command {
return cli.GetTxCmd()
}
// GetQueryCmd returns no root query command for the module.
func (AppModuleBasic) GetQueryCmd() *cobra.Command {
return nil
}
//____________________________________________________________________________
// AppModule app module type
type AppModule struct {
AppModuleBasic
keeper keeper.Keeper
}
// NewAppModule creates a new AppModule object
func NewAppModule(keeper keeper.Keeper) AppModule {
return AppModule{
AppModuleBasic: AppModuleBasic{},
keeper: keeper,
}
}
// Name module name
func (am AppModule) Name() string {
return am.AppModuleBasic.Name()
}
// RegisterInvariants register module invariants
func (am AppModule) RegisterInvariants(_ sdk.InvariantRegistry) {}
// Route module message route name
func (am AppModule) Route() sdk.Route {
return sdk.Route{}
}
// QuerierRoute module querier route name
func (AppModule) QuerierRoute() string {
return ""
}
// LegacyQuerierHandler returns no sdk.Querier.
func (am AppModule) LegacyQuerierHandler(_ *codec.LegacyAmino) sdk.Querier {
return nil
}
// ConsensusVersion implements AppModule/ConsensusVersion.
func (AppModule) ConsensusVersion() uint64 {
return 1
}
// RegisterServices registers module services.
func (am AppModule) RegisterServices(cfg module.Configurator) {
types.RegisterMsgServer(cfg.MsgServer(), keeper.NewMsgServerImpl(am.keeper))
}
// InitGenesis module init-genesis
func (am AppModule) InitGenesis(_ sdk.Context, _ codec.JSONCodec, _ json.RawMessage) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}
// ExportGenesis module export genesis
func (am AppModule) ExportGenesis(_ sdk.Context, cdc codec.JSONCodec) json.RawMessage {
return am.DefaultGenesis(cdc)
}
// BeginBlock module begin-block
func (am AppModule) BeginBlock(_ sdk.Context, _ abci.RequestBeginBlock) {}
// EndBlock module end-block
func (am AppModule) EndBlock(_ sdk.Context, _ abci.RequestEndBlock) []abci.ValidatorUpdate {
return []abci.ValidatorUpdate{}
}

361
x/router/testutil/suite.go Normal file
View File

@ -0,0 +1,361 @@
package testutil
import (
"fmt"
"reflect"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
"github.com/cosmos/cosmos-sdk/simapp"
sdk "github.com/cosmos/cosmos-sdk/types"
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types"
vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
"github.com/stretchr/testify/suite"
abci "github.com/tendermint/tendermint/abci/types"
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
tmtime "github.com/tendermint/tendermint/types/time"
"github.com/kava-labs/kava/app"
earnkeeper "github.com/kava-labs/kava/x/earn/keeper"
earntypes "github.com/kava-labs/kava/x/earn/types"
"github.com/kava-labs/kava/x/router/keeper"
savingstypes "github.com/kava-labs/kava/x/savings/types"
)
// Test suite used for all keeper tests
type Suite struct {
suite.Suite
App app.TestApp
Ctx sdk.Context
Keeper keeper.Keeper
BankKeeper bankkeeper.Keeper
StakingKeeper stakingkeeper.Keeper
EarnKeeper earnkeeper.Keeper
}
// The default state used by each test
func (suite *Suite) SetupTest() {
tApp := app.NewTestApp()
ctx := tApp.NewContext(true, tmproto.Header{Height: 1, Time: tmtime.Now()})
tApp.InitializeFromGenesisStates()
suite.App = tApp
suite.Ctx = ctx
suite.Keeper = tApp.GetRouterKeeper()
suite.StakingKeeper = tApp.GetStakingKeeper()
suite.BankKeeper = tApp.GetBankKeeper()
suite.EarnKeeper = tApp.GetEarnKeeper()
}
// CreateAccount creates a new account from the provided balance and address
func (suite *Suite) CreateAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins) authtypes.AccountI {
ak := suite.App.GetAccountKeeper()
acc := ak.NewAccountWithAddress(suite.Ctx, addr)
ak.SetAccount(suite.Ctx, acc)
err := simapp.FundAccount(suite.BankKeeper, suite.Ctx, acc.GetAddress(), initialBalance)
suite.Require().NoError(err)
return acc
}
// CreateVestingAccount creates a new vesting account. `vestingBalance` should be a fraction of `initialBalance`.
func (suite *Suite) CreateVestingAccountWithAddress(addr sdk.AccAddress, initialBalance sdk.Coins, vestingBalance sdk.Coins) authtypes.AccountI {
if vestingBalance.IsAnyGT(initialBalance) {
panic("vesting balance must be less than initial balance")
}
acc := suite.CreateAccountWithAddress(addr, initialBalance)
bacc := acc.(*authtypes.BaseAccount)
periods := vestingtypes.Periods{
vestingtypes.Period{
Length: 31556952,
Amount: vestingBalance,
},
}
vacc := vestingtypes.NewPeriodicVestingAccount(bacc, vestingBalance, suite.Ctx.BlockTime().Unix(), periods)
suite.App.GetAccountKeeper().SetAccount(suite.Ctx, vacc)
return vacc
}
// AddCoinsToModule adds coins to the a module account, creating it if it doesn't exist.
func (suite *Suite) AddCoinsToModule(module string, amount sdk.Coins) {
err := simapp.FundModuleAccount(suite.BankKeeper, suite.Ctx, module, amount)
suite.Require().NoError(err)
}
// AccountBalanceEqual checks if an account has the specified coins.
func (suite *Suite) AccountBalanceEqual(addr sdk.AccAddress, coins sdk.Coins) {
balance := suite.BankKeeper.GetAllBalances(suite.Ctx, addr)
suite.Equalf(coins, balance, "expected account balance to equal coins %s, but got %s", coins, balance)
}
// AccountBalanceOfEqual checks if an account has the specified amount of one denom.
func (suite *Suite) AccountBalanceOfEqual(addr sdk.AccAddress, denom string, amount sdk.Int) {
balance := suite.BankKeeper.GetBalance(suite.Ctx, addr, denom).Amount
suite.Equalf(amount, balance, "expected account balance to have %[1]s%[2]s, but got %[3]s%[2]s", amount, denom, balance)
}
// AccountSpendableBalanceEqual checks if an account has the specified coins unlocked.
func (suite *Suite) AccountSpendableBalanceEqual(addr sdk.AccAddress, amount sdk.Coins) {
balance := suite.BankKeeper.SpendableCoins(suite.Ctx, addr)
suite.Equalf(amount, balance, "expected account spendable balance to equal coins %s, but got %s", amount, balance)
}
func (suite *Suite) QueryBank_SpendableBalance(user sdk.AccAddress) sdk.Coins {
res, err := suite.BankKeeper.SpendableBalances(
sdk.WrapSDKContext(suite.Ctx),
&banktypes.QuerySpendableBalancesRequest{
Address: user.String(),
},
)
suite.Require().NoError(err)
return *&res.Balances
}
func (suite *Suite) deliverMsgCreateValidator(ctx sdk.Context, address sdk.ValAddress, selfDelegation sdk.Coin) error {
msg, err := stakingtypes.NewMsgCreateValidator(
address,
ed25519.GenPrivKey().PubKey(),
selfDelegation,
stakingtypes.Description{},
stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
sdk.NewInt(1e6),
)
if err != nil {
return err
}
msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
_, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg)
return err
}
// NewBondCoin creates a Coin with the current staking denom.
func (suite *Suite) NewBondCoin(amount sdk.Int) sdk.Coin {
stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
return sdk.NewCoin(stakingDenom, amount)
}
// NewBondCoins creates Coins with the current staking denom.
func (suite *Suite) NewBondCoins(amount sdk.Int) sdk.Coins {
return sdk.NewCoins(suite.NewBondCoin(amount))
}
// CreateNewUnbondedValidator creates a new validator in the staking module.
// New validators are unbonded until the end blocker is run.
func (suite *Suite) CreateNewUnbondedValidator(addr sdk.ValAddress, selfDelegation sdk.Int) stakingtypes.Validator {
// Create a validator
err := suite.deliverMsgCreateValidator(suite.Ctx, addr, suite.NewBondCoin(selfDelegation))
suite.Require().NoError(err)
// New validators are created in an unbonded state. Note if the end blocker is run later this validator could become bonded.
validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, addr)
suite.Require().True(found)
return validator
}
// SlashValidator burns tokens staked in a validator. new_tokens = old_tokens * (1-slashFraction)
func (suite *Suite) SlashValidator(addr sdk.ValAddress, slashFraction sdk.Dec) {
validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, addr)
suite.Require().True(found)
consAddr, err := validator.GetConsAddr()
suite.Require().NoError(err)
// Assume infraction was at current height. Note unbonding delegations and redelegations are only slashed if created after
// the infraction height so none will be slashed.
infractionHeight := suite.Ctx.BlockHeight()
power := suite.StakingKeeper.TokensToConsensusPower(suite.Ctx, validator.GetTokens())
suite.StakingKeeper.Slash(suite.Ctx, consAddr, infractionHeight, power, slashFraction)
}
// CreateDelegation delegates tokens to a validator.
func (suite *Suite) CreateDelegation(valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) sdk.Dec {
stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx)
msg := stakingtypes.NewMsgDelegate(
delegator,
valAddr,
sdk.NewCoin(stakingDenom, amount),
)
msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper)
_, err := msgServer.Delegate(sdk.WrapSDKContext(suite.Ctx), msg)
suite.Require().NoError(err)
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
suite.Require().True(found)
return del.Shares
}
// DelegationSharesEqual checks if a delegation has the specified shares.
// It expects delegations with zero shares to not be stored in state.
func (suite *Suite) DelegationSharesEqual(valAddr sdk.ValAddress, delegator sdk.AccAddress, shares sdk.Dec) bool {
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
if shares.IsZero() {
return suite.Falsef(found, "expected delegator to not be found, got %s shares", del.Shares)
} else {
res := suite.True(found, "expected delegator to be found")
return res && suite.Truef(shares.Equal(del.Shares), "expected %s delegator shares but got %s", shares, del.Shares)
}
}
// DelegationBalanceLessThan checks if a delegation's staked token balance is less the specified amount.
// It treats not found delegations as having zero shares.
func (suite *Suite) DelegationBalanceLessThan(valAddr sdk.ValAddress, delegator sdk.AccAddress, max sdk.Int) bool {
shares := sdk.ZeroDec()
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
if found {
shares = del.Shares
}
val, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
suite.Require().Truef(found, "expected validator to be found")
tokens := val.TokensFromShares(shares).TruncateInt()
return suite.Truef(tokens.LT(max), "expected delegation balance to be less than %s, got %s", max, tokens)
}
// DelegationBalanceInDeltaBelow checks if a delegation's staked token balance is between `expected` and `expected - delta` inclusive.
// It treats not found delegations as having zero shares.
func (suite *Suite) DelegationBalanceInDeltaBelow(valAddr sdk.ValAddress, delegator sdk.AccAddress, expected, delta sdk.Int) bool {
shares := sdk.ZeroDec()
del, found := suite.StakingKeeper.GetDelegation(suite.Ctx, delegator, valAddr)
if found {
shares = del.Shares
}
val, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr)
suite.Require().Truef(found, "expected validator to be found")
tokens := val.TokensFromShares(shares).TruncateInt()
lte := suite.Truef(tokens.LTE(expected), "expected delegation balance to be less than or equal to %s, got %s", expected, tokens)
gte := suite.Truef(tokens.GTE(expected.Sub(delta)), "expected delegation balance to be greater than or equal to %s, got %s", expected.Sub(delta), tokens)
return lte && gte
}
// UnbondingDelegationInDeltaBelow checks if the total balance in an unbonding delegation is between `expected` and `expected - delta` inclusive.
func (suite *Suite) UnbondingDelegationInDeltaBelow(valAddr sdk.ValAddress, delegator sdk.AccAddress, expected, delta sdk.Int) bool {
tokens := sdk.ZeroInt()
ubd, found := suite.StakingKeeper.GetUnbondingDelegation(suite.Ctx, delegator, valAddr)
if found {
for _, entry := range ubd.Entries {
tokens = tokens.Add(entry.Balance)
}
}
lte := suite.Truef(tokens.LTE(expected), "expected unbonding delegation balance to be less than or equal to %s, got %s", expected, tokens)
gte := suite.Truef(tokens.GTE(expected.Sub(delta)), "expected unbonding delegation balance to be greater than or equal to %s, got %s", expected.Sub(delta), tokens)
return lte && gte
}
func (suite *Suite) QueryStaking_Delegation(valAddr sdk.ValAddress, delegator sdk.AccAddress) stakingtypes.DelegationResponse {
stakingQuery := stakingkeeper.Querier{Keeper: suite.StakingKeeper}
res, err := stakingQuery.Delegation(
sdk.WrapSDKContext(suite.Ctx),
&stakingtypes.QueryDelegationRequest{
DelegatorAddr: delegator.String(),
ValidatorAddr: valAddr.String(),
},
)
suite.Require().NoError(err)
return *res.DelegationResponse
}
// EventsContains asserts that the expected event is in the provided events
func (suite *Suite) EventsContains(events sdk.Events, expectedEvent sdk.Event) {
foundMatch := false
for _, event := range events {
if event.Type == expectedEvent.Type {
if reflect.DeepEqual(attrsToMap(expectedEvent.Attributes), attrsToMap(event.Attributes)) {
foundMatch = true
}
}
}
suite.True(foundMatch, fmt.Sprintf("event of type %s not found or did not match", expectedEvent.Type))
}
func attrsToMap(attrs []abci.EventAttribute) []sdk.Attribute {
out := []sdk.Attribute{}
for _, attr := range attrs {
out = append(out, sdk.NewAttribute(string(attr.Key), string(attr.Value)))
}
return out
}
// CreateVault adds a new earn vault to the earn keeper parameters
func (suite *Suite) CreateVault(
vaultDenom string,
vaultStrategies earntypes.StrategyTypes,
isPrivateVault bool,
allowedDepositors []sdk.AccAddress,
) {
vault := earntypes.NewAllowedVault(vaultDenom, vaultStrategies, isPrivateVault, allowedDepositors)
allowedVaults := suite.EarnKeeper.GetAllowedVaults(suite.Ctx)
allowedVaults = append(allowedVaults, vault)
params := earntypes.NewParams(allowedVaults)
suite.EarnKeeper.SetParams(
suite.Ctx,
params,
)
}
// SetSavingsSupportedDenoms overwrites the list of supported denoms in the savings module params.
func (suite *Suite) SetSavingsSupportedDenoms(denoms []string) {
sk := suite.App.GetSavingsKeeper()
sk.SetParams(suite.Ctx, savingstypes.NewParams(denoms))
}
// VaultAccountValueEqual asserts that the vault account value matches the provided coin amount.
func (suite *Suite) VaultAccountValueEqual(acc sdk.AccAddress, coin sdk.Coin) {
accVaultBal, err := suite.EarnKeeper.GetVaultAccountValue(suite.Ctx, coin.Denom, acc)
suite.Require().NoError(err)
suite.Require().Truef(
coin.Equal(accVaultBal),
"expected account vault balance to equal %s, but got %s",
coin, accVaultBal,
)
}
// VaultAccountSharesEqual asserts that the vault account shares match the provided values.
func (suite *Suite) VaultAccountSharesEqual(acc sdk.AccAddress, shares earntypes.VaultShares) { // TODO
accVaultShares, found := suite.EarnKeeper.GetVaultAccountShares(suite.Ctx, acc)
if !found {
suite.Empty(shares)
} else {
suite.Equal(shares, accVaultShares)
}
}
func (suite *Suite) QueryEarn_VaultValue(depositor sdk.AccAddress, vaultDenom string) earntypes.DepositResponse {
earnQuery := earnkeeper.NewQueryServerImpl(suite.EarnKeeper)
res, err := earnQuery.Deposits(
sdk.WrapSDKContext(suite.Ctx),
&earntypes.QueryDepositsRequest{
Depositor: depositor.String(),
Denom: vaultDenom,
},
)
suite.Require().NoError(err)
suite.Require().Equalf(1, len(res.Deposits), "while earn supports one vault per denom, deposits response should be length 1")
return res.Deposits[0]
}

42
x/router/types/codec.go Normal file
View File

@ -0,0 +1,42 @@
package types
import (
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/codec/types"
cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/msgservice"
)
// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the module.
func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) {
cdc.RegisterConcrete(&MsgMintDeposit{}, "router/MsgMintDeposit", nil)
cdc.RegisterConcrete(&MsgDelegateMintDeposit{}, "router/MsgDelegateMintDeposit", nil)
cdc.RegisterConcrete(&MsgWithdrawBurn{}, "router/MsgWithdrawBurn", nil)
cdc.RegisterConcrete(&MsgWithdrawBurnUndelegate{}, "router/MsgWithdrawBurnUndelegate", nil)
}
// RegisterInterfaces registers proto messages under their interfaces for unmarshalling,
// in addition to registering the msg service for handling tx msgs
func RegisterInterfaces(registry types.InterfaceRegistry) {
registry.RegisterImplementations((*sdk.Msg)(nil),
&MsgMintDeposit{},
&MsgDelegateMintDeposit{},
&MsgWithdrawBurn{},
&MsgWithdrawBurnUndelegate{},
)
msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc)
}
var (
amino = codec.NewLegacyAmino()
// ModuleCdc represents the legacy amino codec for the module
ModuleCdc = codec.NewAminoCodec(amino)
)
func init() {
RegisterLegacyAminoCodec(amino)
cryptocodec.RegisterCrypto(amino)
}

View File

@ -0,0 +1,13 @@
package types_test
import (
"os"
"testing"
"github.com/kava-labs/kava/app"
)
func TestMain(m *testing.M) {
app.SetSDKConfig()
os.Exit(m.Run())
}

View File

@ -0,0 +1,34 @@
package types
import (
"time"
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
earntypes "github.com/kava-labs/kava/x/earn/types"
)
type StakingKeeper interface {
BondDenom(ctx sdk.Context) (res string)
GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool)
Delegate(
ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Int, tokenSrc stakingtypes.BondStatus,
validator stakingtypes.Validator, subtractAccount bool,
) (newShares sdk.Dec, err error)
Undelegate(
ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, sharesAmount sdk.Dec,
) (time.Time, error)
}
type LiquidKeeper interface {
DerivativeFromTokens(ctx sdk.Context, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error)
MintDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error)
BurnDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Dec, error)
}
type EarnKeeper interface {
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
}

9
x/router/types/keys.go Normal file
View File

@ -0,0 +1,9 @@
package types
const (
// ModuleName name that will be used throughout the module
ModuleName = "router"
// RouterKey top level router key
RouterKey = ModuleName
)

201
x/router/types/msg.go Normal file
View File

@ -0,0 +1,201 @@
package types
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/cosmos/cosmos-sdk/x/auth/legacy/legacytx"
)
const (
// TypeMsgMintDeposit defines the type for MsgMintDeposit
TypeMsgMintDeposit = "mint_deposit"
// TypeMsgDelegateMintDeposit defines the type for MsgDelegateMintDeposit
TypeMsgDelegateMintDeposit = "delegate_mint_deposit"
// TypeMsgWithdrawBurn defines the type for MsgWithdrawBurn
TypeMsgWithdrawBurn = "withdraw_burn"
// TypeMsgWithdrawBurnUndelegate defines the type for MsgWithdrawBurnUndelegate
TypeMsgWithdrawBurnUndelegate = "withdraw_burn_undelegate"
)
var (
_ sdk.Msg = &MsgMintDeposit{}
_ legacytx.LegacyMsg = &MsgMintDeposit{}
_ sdk.Msg = &MsgDelegateMintDeposit{}
_ legacytx.LegacyMsg = &MsgDelegateMintDeposit{}
_ sdk.Msg = &MsgWithdrawBurn{}
_ legacytx.LegacyMsg = &MsgWithdrawBurn{}
_ sdk.Msg = &MsgWithdrawBurnUndelegate{}
_ legacytx.LegacyMsg = &MsgWithdrawBurnUndelegate{}
)
// NewMsgMintDeposit returns a new MsgMintDeposit.
func NewMsgMintDeposit(depositor sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgMintDeposit {
return &MsgMintDeposit{
Depositor: depositor.String(),
Validator: validator.String(),
Amount: amount,
}
}
// Route return the message type used for routing the message.
func (msg MsgMintDeposit) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgMintDeposit) Type() string { return TypeMsgMintDeposit }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgMintDeposit) ValidateBasic() error {
if _, err := sdk.AccAddressFromBech32(msg.Depositor); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid depositor address: %s", err)
}
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
}
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgMintDeposit) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgMintDeposit) GetSigners() []sdk.AccAddress {
depositor, _ := sdk.AccAddressFromBech32(msg.Depositor)
return []sdk.AccAddress{depositor}
}
// NewMsgDelegateMintDeposit returns a new MsgDelegateMintDeposit.
func NewMsgDelegateMintDeposit(depositor sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgDelegateMintDeposit {
return &MsgDelegateMintDeposit{
Depositor: depositor.String(),
Validator: validator.String(),
Amount: amount,
}
}
// Route return the message type used for routing the message.
func (msg MsgDelegateMintDeposit) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgDelegateMintDeposit) Type() string { return TypeMsgDelegateMintDeposit }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgDelegateMintDeposit) ValidateBasic() error {
if _, err := sdk.AccAddressFromBech32(msg.Depositor); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid depositor address: %s", err)
}
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
}
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgDelegateMintDeposit) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgDelegateMintDeposit) GetSigners() []sdk.AccAddress {
depositor, _ := sdk.AccAddressFromBech32(msg.Depositor)
return []sdk.AccAddress{depositor}
}
// NewMsgWithdrawBurn returns a new MsgWithdrawBurn.
func NewMsgWithdrawBurn(from sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgWithdrawBurn {
return &MsgWithdrawBurn{
From: from.String(),
Validator: validator.String(),
Amount: amount,
}
}
// Route return the message type used for routing the message.
func (msg MsgWithdrawBurn) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgWithdrawBurn) Type() string { return TypeMsgWithdrawBurn }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgWithdrawBurn) ValidateBasic() error {
if _, err := sdk.AccAddressFromBech32(msg.From); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid from address: %s", err)
}
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
}
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgWithdrawBurn) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgWithdrawBurn) GetSigners() []sdk.AccAddress {
from, _ := sdk.AccAddressFromBech32(msg.From)
return []sdk.AccAddress{from}
}
// NewMsgWithdrawBurnUndelegate returns a new MsgWithdrawBurnUndelegate.
func NewMsgWithdrawBurnUndelegate(from sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) *MsgWithdrawBurnUndelegate {
return &MsgWithdrawBurnUndelegate{
From: from.String(),
Validator: validator.String(),
Amount: amount,
}
}
// Route return the message type used for routing the message.
func (msg MsgWithdrawBurnUndelegate) Route() string { return RouterKey }
// Type returns a human-readable string for the message, intended for utilization within tags.
func (msg MsgWithdrawBurnUndelegate) Type() string { return TypeMsgWithdrawBurnUndelegate }
// ValidateBasic does a simple validation check that doesn't require access to any other information.
func (msg MsgWithdrawBurnUndelegate) ValidateBasic() error {
if _, err := sdk.AccAddressFromBech32(msg.From); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid from address: %s", err)
}
if _, err := sdk.ValAddressFromBech32(msg.Validator); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid validator address: %s", err)
}
if msg.Amount.IsNil() || !msg.Amount.IsValid() || msg.Amount.IsZero() {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidCoins, "'%s'", msg.Amount)
}
return nil
}
// GetSignBytes gets the canonical byte representation of the Msg.
func (msg MsgWithdrawBurnUndelegate) GetSignBytes() []byte {
bz := ModuleCdc.MustMarshalJSON(&msg)
return sdk.MustSortJSON(bz)
}
// GetSigners returns the addresses of signers that must sign.
func (msg MsgWithdrawBurnUndelegate) GetSigners() []sdk.AccAddress {
from, _ := sdk.AccAddressFromBech32(msg.From)
return []sdk.AccAddress{from}
}

207
x/router/types/msg_test.go Normal file
View File

@ -0,0 +1,207 @@
package types_test
import (
fmt "fmt"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/kava-labs/kava/x/router/types"
)
func TestMsgMintDeposit_Signing(t *testing.T) {
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
msg := types.NewMsgMintDeposit(
address,
validatorAddress,
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
)
// checking for the "type" field ensures the msg is registered on the amino codec
signBytes := []byte(
`{"type":"router/MsgMintDeposit","value":{"amount":{"amount":"1000000000","denom":"ukava"},"depositor":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
)
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsgDelegateMintDeposit_Signing(t *testing.T) {
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
msg := types.NewMsgDelegateMintDeposit(
address,
validatorAddress,
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
)
// checking for the "type" field ensures the msg is registered on the amino codec
signBytes := []byte(
`{"type":"router/MsgDelegateMintDeposit","value":{"amount":{"amount":"1000000000","denom":"ukava"},"depositor":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
)
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsgWithdrawBurn_Signing(t *testing.T) {
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
msg := types.NewMsgWithdrawBurn(
address,
validatorAddress,
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
)
// checking for the "type" field ensures the msg is registered on the amino codec
signBytes := []byte(
`{"type":"router/MsgWithdrawBurn","value":{"amount":{"amount":"1000000000","denom":"ukava"},"from":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
)
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsgWithdrawBurnUndelegate_Signing(t *testing.T) {
address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d")
validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42")
msg := types.NewMsgWithdrawBurnUndelegate(
address,
validatorAddress,
sdk.NewCoin("ukava", sdk.NewInt(1e9)),
)
// checking for the "type" field ensures the msg is registered on the amino codec
signBytes := []byte(
`{"type":"router/MsgWithdrawBurnUndelegate","value":{"amount":{"amount":"1000000000","denom":"ukava"},"from":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`,
)
assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners())
assert.Equal(t, signBytes, msg.GetSignBytes())
}
func TestMsg_Validate(t *testing.T) {
validAddress := "kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d"
validValidatorAddress := "kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"
validCoin := sdk.NewInt64Coin("ukava", 1e9)
type msgArgs struct {
depositor string
validator string
amount sdk.Coin
}
tests := []struct {
name string
msgArgs msgArgs
expectedErr error
}{
{
name: "normal multiplier is valid",
msgArgs: msgArgs{
depositor: validAddress,
validator: validValidatorAddress,
amount: validCoin,
},
},
{
name: "invalid depositor",
msgArgs: msgArgs{
depositor: "invalid",
validator: validValidatorAddress,
amount: validCoin,
},
expectedErr: sdkerrors.ErrInvalidAddress,
},
{
name: "empty depositor",
msgArgs: msgArgs{
depositor: "",
validator: validValidatorAddress,
amount: validCoin,
},
expectedErr: sdkerrors.ErrInvalidAddress,
},
{
name: "invalid validator",
msgArgs: msgArgs{
depositor: validAddress,
validator: "invalid",
amount: validCoin,
},
expectedErr: sdkerrors.ErrInvalidAddress,
},
{
name: "nil coin",
msgArgs: msgArgs{
depositor: validAddress,
validator: validValidatorAddress,
amount: sdk.Coin{},
},
expectedErr: sdkerrors.ErrInvalidCoins,
},
{
name: "zero coin",
msgArgs: msgArgs{
depositor: validAddress,
validator: validValidatorAddress,
amount: sdk.NewCoin("ukava", sdk.ZeroInt()),
},
expectedErr: sdkerrors.ErrInvalidCoins,
},
{
name: "negative coin",
msgArgs: msgArgs{
depositor: validAddress,
validator: validValidatorAddress,
amount: sdk.Coin{Denom: "ukava", Amount: sdk.NewInt(-1)},
},
expectedErr: sdkerrors.ErrInvalidCoins,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
msgMintDeposit := types.MsgMintDeposit{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
msgDelegateMintDeposit := types.MsgDelegateMintDeposit{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
msgWithdrawBurn := types.MsgWithdrawBurn{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
msgWithdrawBurnUndelegate := types.MsgWithdrawBurnUndelegate{tc.msgArgs.depositor, tc.msgArgs.validator, tc.msgArgs.amount}
msgs := []sdk.Msg{&msgMintDeposit, &msgDelegateMintDeposit, &msgWithdrawBurn, &msgWithdrawBurnUndelegate}
for _, msg := range msgs {
t.Run(fmt.Sprintf("%T", msg), func(t *testing.T) {
err := msg.ValidateBasic()
if tc.expectedErr == nil {
require.NoError(t, err)
} else {
require.ErrorIs(t, err, tc.expectedErr, "expected error '%s' not found in actual '%s'", tc.expectedErr, err)
}
})
}
})
}
}
func mustAccAddressFromBech32(address string) sdk.AccAddress {
addr, err := sdk.AccAddressFromBech32(address)
if err != nil {
panic(err)
}
return addr
}
func mustValAddressFromBech32(address string) sdk.ValAddress {
addr, err := sdk.ValAddressFromBech32(address)
if err != nil {
panic(err)
}
return addr
}

1882
x/router/types/tx.pb.go Normal file

File diff suppressed because it is too large Load Diff