From ceaed3f0e12b5ff334c3bd2b37e221dee341b769 Mon Sep 17 00:00:00 2001 From: Denali Marsh Date: Fri, 16 Sep 2022 00:00:32 +0200 Subject: [PATCH] liquid staking (#1273) * proto types * proto generated types * liquidstaking top level files: module, genesis * liquidstaking types * liquidstaking keeper * liquidstaking client/cli * add liquidstaking to app, simapp * implement mint derivative * set up liquidstaking keeper test suite * test mint derivative * rename module to liquid * rename proto types, app.go to liquid * use sdk.Coin instead of shares * mint liquid tokens to delegator * burn derivative tokens to receive delegation * use conversion method instead of type cast * simplify delegation transfer logic * broaden delegation transfer tests * simplify transfer delegation method This removes a source of rounding errors * move derivative denom to keeper config * check for invalid coins in msg validation * block 0share transfers to avoid handling edge case * refactor MintDerivative to test calculations * simplify burn method so shares and tokens equal * convert TransferShares back to old design this makes handling vesting tokens easier * fix missed merge conflict * remove deprecated constants * tidy up msg.go * add msg tests * remove unused store key * fix msg event sender * remove unused params * tidy up documentation and errors * remove unused mocks * remove unused keepers from AppModule * tidy up msg return values keeper return values to be used in router msgs * reinstate unintentionally removed interface check * catch invalid input for MnitDerivative clear up test TODOs * clear up InitGenesis TODO * Update x/liquid/client/cli/tx.go Co-authored-by: Derrick Lee * Update x/liquid/client/cli/tx.go Co-authored-by: Derrick Lee * show error logs in devnet * unblock mod account so it can receive dist rewards * catch zero amout msgs early * minor cli fixes Co-authored-by: rhuairahrighairigh Co-authored-by: Ruaridh Co-authored-by: Derrick Lee --- app/app.go | 22 +- app/test_common.go | 2 + contrib/devnet/init-new-chain.sh | 4 + docs/core/proto-docs.md | 99 +++ proto/kava/liquid/v1beta1/tx.proto | 54 ++ x/liquid/client/cli/query.go | 31 + x/liquid/client/cli/tx.go | 122 +++ x/liquid/keeper/derivative.go | 118 +++ x/liquid/keeper/derivative_test.go | 316 ++++++++ x/liquid/keeper/keeper.go | 52 ++ x/liquid/keeper/keeper_test.go | 238 ++++++ x/liquid/keeper/msg_server.go | 84 ++ x/liquid/keeper/staking.go | 109 +++ x/liquid/keeper/staking_test.go | 378 +++++++++ x/liquid/module.go | 138 ++++ x/liquid/types/codec.go | 38 + x/liquid/types/common_test.go | 13 + x/liquid/types/errors.go | 15 + x/liquid/types/events.go | 11 + x/liquid/types/expected_keepers.go | 46 ++ x/liquid/types/key.go | 26 + x/liquid/types/msg.go | 120 +++ x/liquid/types/msg_test.go | 163 ++++ x/liquid/types/tx.pb.go | 1188 ++++++++++++++++++++++++++++ 24 files changed, 3384 insertions(+), 3 deletions(-) create mode 100644 proto/kava/liquid/v1beta1/tx.proto create mode 100644 x/liquid/client/cli/query.go create mode 100644 x/liquid/client/cli/tx.go create mode 100644 x/liquid/keeper/derivative.go create mode 100644 x/liquid/keeper/derivative_test.go create mode 100644 x/liquid/keeper/keeper.go create mode 100644 x/liquid/keeper/keeper_test.go create mode 100644 x/liquid/keeper/msg_server.go create mode 100644 x/liquid/keeper/staking.go create mode 100644 x/liquid/keeper/staking_test.go create mode 100644 x/liquid/module.go create mode 100644 x/liquid/types/codec.go create mode 100644 x/liquid/types/common_test.go create mode 100644 x/liquid/types/errors.go create mode 100644 x/liquid/types/events.go create mode 100644 x/liquid/types/expected_keepers.go create mode 100644 x/liquid/types/key.go create mode 100644 x/liquid/types/msg.go create mode 100644 x/liquid/types/msg_test.go create mode 100644 x/liquid/types/tx.pb.go diff --git a/app/app.go b/app/app.go index 7c0bf157..9843e066 100644 --- a/app/app.go +++ b/app/app.go @@ -125,6 +125,9 @@ import ( kavadistclient "github.com/kava-labs/kava/x/kavadist/client" kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper" kavadisttypes "github.com/kava-labs/kava/x/kavadist/types" + "github.com/kava-labs/kava/x/liquid" + liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper" + liquidtypes "github.com/kava-labs/kava/x/liquid/types" pricefeed "github.com/kava-labs/kava/x/pricefeed" pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types" @@ -190,6 +193,7 @@ var ( savings.AppModuleBasic{}, validatorvesting.AppModuleBasic{}, evmutil.AppModuleBasic{}, + liquid.AppModuleBasic{}, earn.AppModuleBasic{}, ) @@ -215,12 +219,12 @@ var ( cdptypes.LiquidatorMacc: {authtypes.Minter, authtypes.Burner}, hardtypes.ModuleAccountName: {authtypes.Minter}, savingstypes.ModuleAccountName: nil, - earntypes.ModuleName: nil, + liquidtypes.ModuleAccountName: {authtypes.Minter, authtypes.Burner}, + earntypes.ModuleAccountName: nil, } ) // Verify app interface at compile time -// var _ simapp.App = (*App)(nil) // TODO var _ servertypes.Application = (*App)(nil) // Options bundles several configuration params for an App. @@ -285,6 +289,7 @@ type App struct { committeeKeeper committeekeeper.Keeper incentiveKeeper incentivekeeper.Keeper savingsKeeper savingskeeper.Keeper + liquidKeeper liquidkeeper.Keeper earnKeeper earnkeeper.Keeper // make scoped keepers public for test purposes @@ -586,6 +591,12 @@ func NewApp( app.accountKeeper, app.bankKeeper, ) + app.liquidKeeper = liquidkeeper.NewDefaultKeeper( + appCodec, + app.accountKeeper, + app.bankKeeper, + &app.stakingKeeper, + ) app.incentiveKeeper = incentivekeeper.NewKeeper( appCodec, keys[incentivetypes.StoreKey], @@ -691,6 +702,7 @@ func NewApp( incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.bankKeeper, app.cdpKeeper), evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper), savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper), + liquid.NewAppModule(app.liquidKeeper), earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper), ) @@ -738,6 +750,7 @@ func NewApp( authz.ModuleName, evmutiltypes.ModuleName, savingstypes.ModuleName, + liquidtypes.ModuleName, earntypes.ModuleName, ) @@ -777,6 +790,7 @@ func NewApp( authz.ModuleName, evmutiltypes.ModuleName, savingstypes.ModuleName, + liquidtypes.ModuleName, earntypes.ModuleName, ) @@ -816,6 +830,7 @@ func NewApp( paramstypes.ModuleName, upgradetypes.ModuleName, validatorvestingtypes.ModuleName, + liquidtypes.ModuleName, ) app.mm.RegisterInvariants(&app.crisisKeeper) @@ -979,10 +994,11 @@ func (app *App) loadBlockedMaccAddrs() map[string]bool { modAccAddrs := app.ModuleAccountAddrs() kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName) earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName) + liquidMaccAddr := app.accountKeeper.GetModuleAddress(liquidtypes.ModuleName) for addr := range modAccAddrs { // Set the kavadist and earn module account address as unblocked - if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() { + if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() || addr == liquidMaccAddr.String() { modAccAddrs[addr] = false } } diff --git a/app/test_common.go b/app/test_common.go index 084ae182..ccb5455a 100644 --- a/app/test_common.go +++ b/app/test_common.go @@ -40,6 +40,7 @@ import ( incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper" issuancekeeper "github.com/kava-labs/kava/x/issuance/keeper" kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper" + liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper" pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper" savingskeeper "github.com/kava-labs/kava/x/savings/keeper" swapkeeper "github.com/kava-labs/kava/x/swap/keeper" @@ -109,6 +110,7 @@ func (tApp TestApp) GetEvmutilKeeper() evmutilkeeper.Keeper { return tApp.ev func (tApp TestApp) GetEvmKeeper() *evmkeeper.Keeper { return tApp.evmKeeper } func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper { return tApp.savingsKeeper } func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper } +func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.liquidKeeper } func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper } // LegacyAmino returns the app's amino codec. diff --git a/contrib/devnet/init-new-chain.sh b/contrib/devnet/init-new-chain.sh index 20674282..58e29d20 100755 --- a/contrib/devnet/init-new-chain.sh +++ b/contrib/devnet/init-new-chain.sh @@ -35,6 +35,10 @@ sed -in-place='' 's/enable = false/enable = true/g' $DATA/config/app.toml # Set evm tracer to json sed -in-place='' 's/tracer = ""/tracer = "json"/g' $DATA/config/app.toml +# Enable full error trace to be returned on tx failure +sed -in-place='' '/iavl-cache-size/a\ +trace = true' $DATA/config/app.toml + # Set client chain id sed -in-place='' 's/chain-id = ""/chain-id = "kavalocalnet_8888-1"/g' $DATA/config/client.toml diff --git a/docs/core/proto-docs.md b/docs/core/proto-docs.md index 3a7d4561..5ccc403c 100644 --- a/docs/core/proto-docs.md +++ b/docs/core/proto-docs.md @@ -385,6 +385,14 @@ - [Query](#kava.kavadist.v1beta1.Query) +- [kava/liquid/v1beta1/tx.proto](#kava/liquid/v1beta1/tx.proto) + - [MsgBurnDerivative](#kava.liquid.v1beta1.MsgBurnDerivative) + - [MsgBurnDerivativeResponse](#kava.liquid.v1beta1.MsgBurnDerivativeResponse) + - [MsgMintDerivative](#kava.liquid.v1beta1.MsgMintDerivative) + - [MsgMintDerivativeResponse](#kava.liquid.v1beta1.MsgMintDerivativeResponse) + + - [Msg](#kava.liquid.v1beta1.Msg) + - [kava/pricefeed/v1beta1/store.proto](#kava/pricefeed/v1beta1/store.proto) - [CurrentPrice](#kava.pricefeed.v1beta1.CurrentPrice) - [Market](#kava.pricefeed.v1beta1.Market) @@ -5384,6 +5392,97 @@ Query defines the gRPC querier service. + +

Top

+ +## kava/liquid/v1beta1/tx.proto + + + + + +### MsgBurnDerivative +MsgBurnDerivative defines the Msg/BurnDerivative request type. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `sender` | [string](#string) | | sender is the owner of the derivatives to be converted | +| `validator` | [string](#string) | | validator is the validator of the derivatives to be converted | +| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the quantity of derivatives to be converted | + + + + + + + + +### MsgBurnDerivativeResponse +MsgBurnDerivativeResponse defines the Msg/BurnDerivative response type. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `received` | [string](#string) | | received is the number of delegation shares sent to the sender | + + + + + + + + +### MsgMintDerivative +MsgMintDerivative defines the Msg/MintDerivative request type. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `sender` | [string](#string) | | sender is the owner of the delegation to be converted | +| `validator` | [string](#string) | | validator is the validator of the delegation to be converted | +| `amount` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | amount is the quantity of staked assets to be converted | + + + + + + + + +### MsgMintDerivativeResponse +MsgMintDerivativeResponse defines the Msg/MintDerivative response type. + + +| Field | Type | Label | Description | +| ----- | ---- | ----- | ----------- | +| `received` | [cosmos.base.v1beta1.Coin](#cosmos.base.v1beta1.Coin) | | received is the amount of staking derivative minted and sent to the sender | + + + + + + + + + + + + + + +### Msg +Msg defines the liquid Msg service. + +| Method Name | Request Type | Response Type | Description | HTTP Verb | Endpoint | +| ----------- | ------------ | ------------- | ------------| ------- | -------- | +| `MintDerivative` | [MsgMintDerivative](#kava.liquid.v1beta1.MsgMintDerivative) | [MsgMintDerivativeResponse](#kava.liquid.v1beta1.MsgMintDerivativeResponse) | MintDerivative defines a method for converting a delegation into staking deriviatives. | | +| `BurnDerivative` | [MsgBurnDerivative](#kava.liquid.v1beta1.MsgBurnDerivative) | [MsgBurnDerivativeResponse](#kava.liquid.v1beta1.MsgBurnDerivativeResponse) | BurnDerivative defines a method for converting staking deriviatives into a delegation. | | + + + + +

Top

diff --git a/proto/kava/liquid/v1beta1/tx.proto b/proto/kava/liquid/v1beta1/tx.proto new file mode 100644 index 00000000..da6925d2 --- /dev/null +++ b/proto/kava/liquid/v1beta1/tx.proto @@ -0,0 +1,54 @@ +syntax = "proto3"; +package kava.liquid.v1beta1; + +import "gogoproto/gogo.proto"; +import "cosmos/base/v1beta1/coin.proto"; +import "cosmos_proto/cosmos.proto"; + +option go_package = "github.com/kava-labs/kava/x/liquid/types"; + +// Msg defines the liquid Msg service. +service Msg { + + // MintDerivative defines a method for converting a delegation into staking deriviatives. + rpc MintDerivative(MsgMintDerivative) returns (MsgMintDerivativeResponse); + + // BurnDerivative defines a method for converting staking deriviatives into a delegation. + rpc BurnDerivative(MsgBurnDerivative) returns (MsgBurnDerivativeResponse); +} + +// MsgMintDerivative defines the Msg/MintDerivative request type. +message MsgMintDerivative { + // sender is the owner of the delegation to be converted + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + // validator is the validator of the delegation to be converted + string validator = 2; + // amount is the quantity of staked assets to be converted + cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false]; +} + +// MsgMintDerivativeResponse defines the Msg/MintDerivative response type. +message MsgMintDerivativeResponse { + // received is the amount of staking derivative minted and sent to the sender + cosmos.base.v1beta1.Coin received = 1 [(gogoproto.nullable) = false]; +} + +// MsgBurnDerivative defines the Msg/BurnDerivative request type. +message MsgBurnDerivative { + // sender is the owner of the derivatives to be converted + string sender = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; + // validator is the validator of the derivatives to be converted + string validator = 2; + // amount is the quantity of derivatives to be converted + cosmos.base.v1beta1.Coin amount = 3 [(gogoproto.nullable) = false]; +} + +// MsgBurnDerivativeResponse defines the Msg/BurnDerivative response type. +message MsgBurnDerivativeResponse { + // received is the number of delegation shares sent to the sender + string received = 1 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; +} \ No newline at end of file diff --git a/x/liquid/client/cli/query.go b/x/liquid/client/cli/query.go new file mode 100644 index 00000000..70054c2a --- /dev/null +++ b/x/liquid/client/cli/query.go @@ -0,0 +1,31 @@ +package cli + +import ( + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + + "github.com/kava-labs/kava/x/liquid/types" +) + +// GetQueryCmd returns the cli query commands for this module +func GetQueryCmd() *cobra.Command { + liquidQueryCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "Querying commands for the liquid module", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmds := []*cobra.Command{} + + for _, cmd := range cmds { + flags.AddQueryFlagsToCmd(cmd) + } + + liquidQueryCmd.AddCommand(cmds...) + + return liquidQueryCmd +} diff --git a/x/liquid/client/cli/tx.go b/x/liquid/client/cli/tx.go new file mode 100644 index 00000000..6ef2043f --- /dev/null +++ b/x/liquid/client/cli/tx.go @@ -0,0 +1,122 @@ +package cli + +import ( + "fmt" + "strings" + + "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/liquid/types" +) + +// GetTxCmd returns the transaction commands for this module +func GetTxCmd() *cobra.Command { + liquidTxCmd := &cobra.Command{ + Use: types.ModuleName, + Short: "liquid transactions subcommands", + DisableFlagParsing: true, + SuggestionsMinimumDistance: 2, + RunE: client.ValidateCmd, + } + + cmds := []*cobra.Command{ + getCmdMintDerivative(), + getCmdBurnDerivative(), + } + + for _, cmd := range cmds { + flags.AddTxFlagsToCmd(cmd) + } + + liquidTxCmd.AddCommand(cmds...) + + return liquidTxCmd +} + +func getCmdMintDerivative() *cobra.Command { + return &cobra.Command{ + Use: "mint [validator-addr] [amount]", + Short: "mints staking derivative from a delegation", + Long: "Mint removes a portion of a user's staking delegation and issues them validator specific staking derivative tokens.", + Args: cobra.ExactArgs(2), + Example: fmt.Sprintf( + `%s tx %s mint kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd 10000000ukava --from `, 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.NewMsgMintDerivative(clientCtx.GetFromAddress(), valAddr, coin) + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } +} + +func getCmdBurnDerivative() *cobra.Command { + return &cobra.Command{ + Use: "burn [amount]", + Short: "burns staking derivative to redeem a delegation", + Long: "Burn removes some staking derivative from a user's account and converts it back to a staking delegation.", + Example: fmt.Sprintf( + `%s tx %s burn 10000000bkava-kavavaloper16lnfpgn6llvn4fstg5nfrljj6aaxyee9z59jqd --from `, version.AppName, types.ModuleName, + ), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientTxContext(cmd) + if err != nil { + return err + } + + amount, err := sdk.ParseCoinNormalized(args[0]) + if err != nil { + return err + } + + valAddr, err := parseLiquidStakingTokenDenom(amount.Denom) + if err != nil { + return sdkerrors.Wrap(types.ErrInvalidDenom, err.Error()) + } + + msg := types.NewMsgBurnDerivative(clientCtx.GetFromAddress(), valAddr, amount) + if err := msg.ValidateBasic(); err != nil { + return err + } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), &msg) + }, + } +} + +// parseLiquidStakingTokenDenom extracts a validator address from a derivative denom. +func parseLiquidStakingTokenDenom(denom string) (sdk.ValAddress, error) { + elements := strings.Split(denom, types.DenomSeparator) + if len(elements) != 2 { + return nil, fmt.Errorf("cannot parse denom %s", denom) + } + addr, err := sdk.ValAddressFromBech32(elements[1]) + if err != nil { + return nil, err + } + return addr, nil +} diff --git a/x/liquid/keeper/derivative.go b/x/liquid/keeper/derivative.go new file mode 100644 index 00000000..cac2b74b --- /dev/null +++ b/x/liquid/keeper/derivative.go @@ -0,0 +1,118 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + + "github.com/kava-labs/kava/x/liquid/types" +) + +// MintDerivative removes a user's staking delegation and mints them equivalent staking derivative coins. +// +// The input staking token amount is used to calculate shares in the user's delegation, which are transferred to a delegation owned by the module. +// Derivative coins are them minted and transferred to the user. +func (k Keeper) MintDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Coin, error) { + bondDenom := k.stakingKeeper.BondDenom(ctx) + if amount.Denom != bondDenom { + return sdk.Coin{}, sdkerrors.Wrapf(types.ErrInvalidDenom, "expected %s", bondDenom) + } + + derivativeAmount, shares, err := k.CalculateDerivativeSharesFromTokens(ctx, delegatorAddr, valAddr, amount.Amount) + if err != nil { + return sdk.Coin{}, err + } + + // Fetching the module account will create it if it doesn't exist. + // This is necessary as otherwise TransferDelegation will create a normal account. + modAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName) + if _, err := k.TransferDelegation(ctx, valAddr, delegatorAddr, modAcc.GetAddress(), shares); err != nil { + return sdk.Coin{}, err + } + + liquidTokenDenom := k.GetLiquidStakingTokenDenom(valAddr) + liquidToken := sdk.NewCoin(liquidTokenDenom, derivativeAmount) + if err = k.mintCoins(ctx, delegatorAddr, sdk.NewCoins(liquidToken)); err != nil { + return sdk.Coin{}, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeMintDerivative, + sdk.NewAttribute(types.AttributeKeyDelegator, delegatorAddr.String()), + sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, liquidToken.String()), + sdk.NewAttribute(types.AttributeKeySharesTransferred, shares.String()), + ), + ) + + return liquidToken, nil +} + +// CalculateDerivativeSharesFromTokens converts a staking token amount into its equivalent delegation shares, and staking derivative amount. +// This combines the code for calculating the shares to be transferred, and the derivative coins to be minted. +func (k Keeper) CalculateDerivativeSharesFromTokens(ctx sdk.Context, delegator sdk.AccAddress, validator sdk.ValAddress, tokens sdk.Int) (sdk.Int, sdk.Dec, error) { + if !tokens.IsPositive() { + return sdk.Int{}, sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "token amount must be positive") + } + shares, err := k.stakingKeeper.ValidateUnbondAmount(ctx, delegator, validator, tokens) + if err != nil { + return sdk.Int{}, sdk.Dec{}, err + } + return shares.TruncateInt(), shares, nil +} + +// BurnDerivative burns an user's staking derivative coins and returns them an equivalent staking delegation. +// +// The derivative coins are burned, and an equivalent number of shares in the module's staking delegation are transferred back to the user. +func (k Keeper) BurnDerivative(ctx sdk.Context, delegatorAddr sdk.AccAddress, valAddr sdk.ValAddress, amount sdk.Coin) (sdk.Dec, error) { + + if amount.Denom != k.GetLiquidStakingTokenDenom(valAddr) { + return sdk.Dec{}, sdkerrors.Wrap(types.ErrInvalidDenom, "derivative denom does not match validator") + } + + if err := k.burnCoins(ctx, delegatorAddr, sdk.NewCoins(amount)); err != nil { + return sdk.Dec{}, err + } + + modAcc := k.accountKeeper.GetModuleAccount(ctx, types.ModuleAccountName) + shares := amount.Amount.ToDec() + receivedShares, err := k.TransferDelegation(ctx, valAddr, modAcc.GetAddress(), delegatorAddr, shares) + if err != nil { + return sdk.Dec{}, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + types.EventTypeBurnDerivative, + sdk.NewAttribute(types.AttributeKeyDelegator, delegatorAddr.String()), + sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, amount.String()), + sdk.NewAttribute(types.AttributeKeySharesTransferred, shares.String()), + ), + ) + return receivedShares, nil +} + +func (k Keeper) GetLiquidStakingTokenDenom(valAddr sdk.ValAddress) string { + return types.GetLiquidStakingTokenDenom(k.derivativeDenom, valAddr) +} + +func (k Keeper) mintCoins(ctx sdk.Context, receiver sdk.AccAddress, amount sdk.Coins) error { + if err := k.bankKeeper.MintCoins(ctx, types.ModuleAccountName, amount); err != nil { + return err + } + if err := k.bankKeeper.SendCoinsFromModuleToAccount(ctx, types.ModuleAccountName, receiver, amount); err != nil { + return err + } + return nil +} + +func (k Keeper) burnCoins(ctx sdk.Context, sender sdk.AccAddress, amount sdk.Coins) error { + if err := k.bankKeeper.SendCoinsFromAccountToModule(ctx, sender, types.ModuleAccountName, amount); err != nil { + return err + } + if err := k.bankKeeper.BurnCoins(ctx, types.ModuleAccountName, amount); err != nil { + return err + } + return nil +} diff --git a/x/liquid/keeper/derivative_test.go b/x/liquid/keeper/derivative_test.go new file mode 100644 index 00000000..f389b722 --- /dev/null +++ b/x/liquid/keeper/derivative_test.go @@ -0,0 +1,316 @@ +package keeper_test + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/liquid/types" +) + +func (suite *KeeperTestSuite) TestBurnDerivative() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, user := addrs[0], addrs[1] + valAddr := sdk.ValAddress(valAccAddr) + + liquidDenom := suite.Keeper.GetLiquidStakingTokenDenom(valAddr) + + testCases := []struct { + name string + balance sdk.Coin + moduleDelegation sdk.Int + burnAmount sdk.Coin + expectedErr error + }{ + { + name: "user can burn their entire balance", + balance: c(liquidDenom, 1e9), + moduleDelegation: i(1e9), + burnAmount: c(liquidDenom, 1e9), + }, + { + name: "user can burn minimum derivative unit", + balance: c(liquidDenom, 1e9), + moduleDelegation: i(1e9), + burnAmount: c(liquidDenom, 1), + }, + { + name: "error when denom cannot be parsed", + balance: c(liquidDenom, 1e9), + moduleDelegation: i(1e9), + burnAmount: c(fmt.Sprintf("ckava-%s", valAddr), 1e6), + expectedErr: types.ErrInvalidDenom, + }, + { + name: "error when burn amount is 0", + balance: c(liquidDenom, 1e9), + moduleDelegation: i(1e9), + burnAmount: c(liquidDenom, 0), + expectedErr: types.ErrUntransferableShares, + }, + { + name: "error when user doesn't have enough funds", + balance: c("ukava", 10), + moduleDelegation: i(1e9), + burnAmount: c(liquidDenom, 1e9), + expectedErr: sdkerrors.ErrInsufficientFunds, + }, + { + name: "error when backing delegation isn't large enough", + balance: c(liquidDenom, 1e9), + moduleDelegation: i(999_999_999), + burnAmount: c(liquidDenom, 1e9), + expectedErr: stakingtypes.ErrNotEnoughDelegationShares, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e6))) + suite.CreateAccountWithAddress(user, sdk.NewCoins(tc.balance)) + suite.AddCoinsToModule(types.ModuleAccountName, suite.NewBondCoins(tc.moduleDelegation)) + + // create delegation from module account to back the derivatives + moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName) + suite.CreateNewUnbondedValidator(valAddr, i(1e6)) + suite.CreateDelegation(valAddr, moduleAccAddress, tc.moduleDelegation) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + modBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, moduleAccAddress) + + _, err := suite.Keeper.BurnDerivative(suite.Ctx, user, valAddr, tc.burnAmount) + + suite.Require().ErrorIs(err, tc.expectedErr) + if tc.expectedErr != nil { + // if an error is expected, state should be reverted so don't need to test state is unchanged + return + } + + suite.AccountBalanceEqual(user, sdk.NewCoins(tc.balance.Sub(tc.burnAmount))) + suite.AccountBalanceEqual(moduleAccAddress, modBalance) // ensure derivatives are burned, and not in module account + + sharesTransferred := tc.burnAmount.Amount.ToDec() + suite.DelegationSharesEqual(valAddr, user, sharesTransferred) + suite.DelegationSharesEqual(valAddr, moduleAccAddress, tc.moduleDelegation.ToDec().Sub(sharesTransferred)) + + suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent( + types.EventTypeBurnDerivative, + sdk.NewAttribute(types.AttributeKeyDelegator, user.String()), + sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, tc.burnAmount.String()), + sdk.NewAttribute(types.AttributeKeySharesTransferred, sharesTransferred.String()), + )) + }) + } +} + +func (suite *KeeperTestSuite) TestCalculateShares() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, delegator := addrs[0], addrs[1] + valAddr := sdk.ValAddress(valAccAddr) + + type returns struct { + derivatives sdk.Int + shares sdk.Dec + err error + } + type validator struct { + tokens sdk.Int + delegatorShares sdk.Dec + } + testCases := []struct { + name string + validator *validator + delegation sdk.Dec + transfer sdk.Int + expected returns + }{ + { + name: "error when validator not found", + validator: nil, + delegation: d("1000000000"), + transfer: i(500e6), + expected: returns{ + err: stakingtypes.ErrNoValidatorFound, + }, + }, + { + name: "error when delegation not found", + validator: &validator{i(1e9), d("1000000000")}, + delegation: sdk.Dec{}, + transfer: i(500e6), + expected: returns{ + err: stakingtypes.ErrNoDelegation, + }, + }, + { + name: "error when transfer < 0", + validator: &validator{i(10), d("10")}, + delegation: d("10"), + transfer: i(-1), + expected: returns{ + err: types.ErrUntransferableShares, + }, + }, + { // disallow zero transfers + name: "error when transfer = 0", + validator: &validator{i(10), d("10")}, + delegation: d("10"), + transfer: i(0), + expected: returns{ + err: types.ErrUntransferableShares, + }, + }, + { + name: "error when transfer > delegated shares", + validator: &validator{i(10), d("10")}, + delegation: d("10"), + transfer: i(11), + expected: returns{ + err: sdkerrors.ErrInvalidRequest, + }, + }, + { + name: "error when validator has no tokens", + validator: &validator{i(0), d("10")}, + delegation: d("10"), + transfer: i(5), + expected: returns{ + err: stakingtypes.ErrInsufficientShares, + }, + }, + { + name: "shares and derivatives are truncated", + validator: &validator{i(3), d("4")}, + delegation: d("4"), + transfer: i(2), + expected: returns{ + derivatives: i(2), // truncated down + shares: d("2.666666666666666666"), // 2/3 * 4 not rounded to ...667 + }, + }, + { + name: "error if calculated shares > shares in delegation", + validator: &validator{i(3), d("4")}, + delegation: d("2.666666666666666665"), // one less than 2/3 * 4 + transfer: i(2), + expected: returns{ + err: sdkerrors.ErrInvalidRequest, + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + if tc.validator != nil { + suite.StakingKeeper.SetValidator(suite.Ctx, stakingtypes.Validator{ + OperatorAddress: valAddr.String(), + Tokens: tc.validator.tokens, + DelegatorShares: tc.validator.delegatorShares, + }) + } + if !tc.delegation.IsNil() { + suite.StakingKeeper.SetDelegation(suite.Ctx, stakingtypes.Delegation{ + DelegatorAddress: delegator.String(), + ValidatorAddress: valAddr.String(), + Shares: tc.delegation, + }) + } + + derivatives, shares, err := suite.Keeper.CalculateDerivativeSharesFromTokens(suite.Ctx, delegator, valAddr, tc.transfer) + if tc.expected.err != nil { + suite.ErrorIs(err, tc.expected.err) + } else { + suite.NoError(err) + suite.Equal(tc.expected.derivatives, derivatives, "expected '%s' got '%s'", tc.expected.derivatives, derivatives) + suite.Equal(tc.expected.shares, shares) + } + }) + } +} + +func (suite *KeeperTestSuite) TestMintDerivative() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, delegator := addrs[0], addrs[1] + valAddr := sdk.ValAddress(valAccAddr) + moduleAccAddress := authtypes.NewModuleAddress(types.ModuleAccountName) + + initialBalance := i(1e9) + vestedBalance := i(500e6) + + testCases := []struct { + name string + amount sdk.Coin + expectedDerivatives sdk.Int + expectedSharesRemaining sdk.Dec + expectedSharesAdded sdk.Dec + expectedErr error + }{ + { + name: "derivative is minted", + amount: suite.NewBondCoin(vestedBalance), + expectedDerivatives: i(500e6), + expectedSharesRemaining: d("500000000.0"), + expectedSharesAdded: d("500000000.0"), + }, + { + name: "error when the input denom isn't correct", + amount: sdk.NewCoin("invalid", i(1000)), + expectedErr: types.ErrInvalidDenom, + }, + { + name: "error when shares cannot be calculated", + amount: suite.NewBondCoin(initialBalance.Mul(i(100))), + expectedErr: sdkerrors.ErrInvalidRequest, + }, + { + name: "error when shares cannot be transferred", + amount: suite.NewBondCoin(initialBalance), // trying to move vesting coins will fail in `TransferShares` + expectedErr: sdkerrors.ErrInsufficientFunds, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance)) + suite.CreateVestingAccountWithAddress(delegator, suite.NewBondCoins(initialBalance), suite.NewBondCoins(vestedBalance)) + + suite.CreateNewUnbondedValidator(valAddr, initialBalance) + suite.CreateDelegation(valAddr, delegator, initialBalance) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + _, err := suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr, tc.amount) + + suite.Require().ErrorIs(err, tc.expectedErr) + if tc.expectedErr != nil { + // if an error is expected, state should be reverted so don't need to test state is unchanged + return + } + + derivative := sdk.NewCoins(sdk.NewCoin(fmt.Sprintf("bkava-%s", valAddr), tc.expectedDerivatives)) + suite.AccountBalanceEqual(delegator, derivative) + + suite.DelegationSharesEqual(valAddr, delegator, tc.expectedSharesRemaining) + suite.DelegationSharesEqual(valAddr, moduleAccAddress, tc.expectedSharesAdded) + + sharesTransferred := initialBalance.ToDec().Sub(tc.expectedSharesRemaining) + suite.EventsContains(suite.Ctx.EventManager().Events(), sdk.NewEvent( + types.EventTypeMintDerivative, + sdk.NewAttribute(types.AttributeKeyDelegator, delegator.String()), + sdk.NewAttribute(types.AttributeKeyValidator, valAddr.String()), + sdk.NewAttribute(sdk.AttributeKeyAmount, derivative.String()), + sdk.NewAttribute(types.AttributeKeySharesTransferred, sharesTransferred.String()), + )) + }) + } +} diff --git a/x/liquid/keeper/keeper.go b/x/liquid/keeper/keeper.go new file mode 100644 index 00000000..0756f38c --- /dev/null +++ b/x/liquid/keeper/keeper.go @@ -0,0 +1,52 @@ +package keeper + +import ( + "fmt" + + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/tendermint/tendermint/libs/log" + + "github.com/kava-labs/kava/x/liquid/types" +) + +// Keeper struct for the liquid module. +type Keeper struct { + cdc codec.Codec + + accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper + stakingKeeper types.StakingKeeper + + derivativeDenom string +} + +// NewKeeper returns a new keeper for the liquid module. +func NewKeeper( + cdc codec.Codec, + ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper, + derivativeDenom string, +) Keeper { + + return Keeper{ + cdc: cdc, + accountKeeper: ak, + bankKeeper: bk, + stakingKeeper: sk, + derivativeDenom: derivativeDenom, + } +} + +// NewDefaultKeeper returns a new keeper for the liquid module with default values. +func NewDefaultKeeper( + cdc codec.Codec, + ak types.AccountKeeper, bk types.BankKeeper, sk types.StakingKeeper, +) Keeper { + + return NewKeeper(cdc, ak, bk, sk, types.DefaultDerivativeDenom) +} + +// Logger returns a module-specific logger. +func (k Keeper) Logger(ctx sdk.Context) log.Logger { + return ctx.Logger().With("module", fmt.Sprintf("x/%s", types.ModuleName)) +} diff --git a/x/liquid/keeper/keeper_test.go b/x/liquid/keeper/keeper_test.go new file mode 100644 index 00000000..37a16a37 --- /dev/null +++ b/x/liquid/keeper/keeper_test.go @@ -0,0 +1,238 @@ +package keeper_test + +import ( + "fmt" + "reflect" + "testing" + + "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" + 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" + "github.com/kava-labs/kava/x/liquid/keeper" +) + +// Test suite used for all keeper tests +type KeeperTestSuite struct { + suite.Suite + App app.TestApp + Ctx sdk.Context + Keeper keeper.Keeper + BankKeeper bankkeeper.Keeper + StakingKeeper stakingkeeper.Keeper +} + +// The default state used by each test +func (suite *KeeperTestSuite) 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.GetLiquidKeeper() + suite.StakingKeeper = tApp.GetStakingKeeper() + suite.BankKeeper = tApp.GetBankKeeper() +} + +// CreateAccount creates a new account (with a fixed address) from the provided balance. +func (suite *KeeperTestSuite) CreateAccount(initialBalance sdk.Coins) authtypes.AccountI { + _, addrs := app.GeneratePrivKeyAddressPairs(1) + + return suite.CreateAccountWithAddress(addrs[0], initialBalance) +} + +// CreateAccount creates a new account from the provided balance and address +func (suite *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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) +} + +func (suite *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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 *KeeperTestSuite) 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 +} + +// CreateRedelegation undelegates tokens from one validator and delegates to another. +func (suite *KeeperTestSuite) CreateRedelegation(delegator sdk.AccAddress, fromValidator, toValidator sdk.ValAddress, amount sdk.Int) { + stakingDenom := suite.StakingKeeper.BondDenom(suite.Ctx) + msg := stakingtypes.NewMsgBeginRedelegate( + delegator, + fromValidator, + toValidator, + sdk.NewCoin(stakingDenom, amount), + ) + + msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper) + _, err := msgServer.BeginRedelegate(sdk.WrapSDKContext(suite.Ctx), msg) + suite.Require().NoError(err) +} + +// DelegationSharesEqual checks if a delegation has the specified shares. +// It expects delegations with zero shares to not be stored in state. +func (suite *KeeperTestSuite) 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) + } +} + +// EventsContains asserts that the expected event is in the provided events +func (suite *KeeperTestSuite) 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)) +} + +// EventsDoNotContainType asserts that the provided events do contain an event of a certain type. +func (suite *KeeperTestSuite) EventsDoNotContainType(events sdk.Events, eventType string) { + for _, event := range events { + suite.Falsef(event.Type == eventType, "found unexpected event %s", eventType) + } +} + +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 +} + +func TestKeeperTestSuite(t *testing.T) { + suite.Run(t, new(KeeperTestSuite)) +} diff --git a/x/liquid/keeper/msg_server.go b/x/liquid/keeper/msg_server.go new file mode 100644 index 00000000..b09e6b4b --- /dev/null +++ b/x/liquid/keeper/msg_server.go @@ -0,0 +1,84 @@ +package keeper + +import ( + "context" + + sdk "github.com/cosmos/cosmos-sdk/types" + + "github.com/kava-labs/kava/x/liquid/types" +) + +type msgServer struct { + keeper Keeper +} + +// NewMsgServerImpl returns an implementation of the liquid MsgServer interface +// for the provided Keeper. +func NewMsgServerImpl(keeper Keeper) types.MsgServer { + return &msgServer{keeper: keeper} +} + +var _ types.MsgServer = msgServer{} + +// MintDerivative handles MintDerivative msgs. +func (k msgServer) MintDerivative(goCtx context.Context, msg *types.MsgMintDerivative) (*types.MsgMintDerivativeResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + validator, err := sdk.ValAddressFromBech32(msg.Validator) + if err != nil { + return nil, err + } + + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, err + } + + mintedDerivative, err := k.keeper.MintDerivative(ctx, sender, validator, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + ), + ) + + return &types.MsgMintDerivativeResponse{ + Received: mintedDerivative, + }, nil +} + +// BurnDerivative handles BurnDerivative msgs. +func (k msgServer) BurnDerivative(goCtx context.Context, msg *types.MsgBurnDerivative) (*types.MsgBurnDerivativeResponse, error) { + ctx := sdk.UnwrapSDKContext(goCtx) + + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return nil, err + } + + validator, err := sdk.ValAddressFromBech32(msg.Validator) + if err != nil { + return nil, err + } + + sharesReceived, err := k.keeper.BurnDerivative(ctx, sender, validator, msg.Amount) + if err != nil { + return nil, err + } + + ctx.EventManager().EmitEvent( + sdk.NewEvent( + sdk.EventTypeMessage, + sdk.NewAttribute(sdk.AttributeKeyModule, types.AttributeValueCategory), + sdk.NewAttribute(sdk.AttributeKeySender, msg.Sender), + ), + ) + return &types.MsgBurnDerivativeResponse{ + Received: sharesReceived, + }, nil +} diff --git a/x/liquid/keeper/staking.go b/x/liquid/keeper/staking.go new file mode 100644 index 00000000..8c809518 --- /dev/null +++ b/x/liquid/keeper/staking.go @@ -0,0 +1,109 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/kava-labs/kava/x/liquid/types" +) + +// TransferDelegation moves some delegation shares between addresses, while keeping the same validator. +// +// Internally shares are unbonded, tokens moved then bonded again. This limits only vested tokens from being transferred. +// The sending delegation must not have any active redelegations. +// A validator cannot reduce self delegated shares below its min self delegation. +// Attempting to transfer zero shares will error. +func (k Keeper) TransferDelegation(ctx sdk.Context, valAddr sdk.ValAddress, fromDelegator, toDelegator sdk.AccAddress, shares sdk.Dec) (sdk.Dec, error) { + // Redelegations link a delegation to it's previous validator so slashes are propagated to the new validator. + // If the delegation is transferred to a new owner, the redelegation object must be updated. + // For expediency all transfers with redelegations are blocked. + if k.stakingKeeper.HasReceivingRedelegation(ctx, fromDelegator, valAddr) { + return sdk.Dec{}, types.ErrRedelegationsNotCompleted + } + + if shares.IsNil() || shares.LT(sdk.ZeroDec()) { + return sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "nil or negative shares") + } + if shares.Equal(sdk.ZeroDec()) { + // Block 0 transfers to reduce edge cases. + return sdk.Dec{}, sdkerrors.Wrap(types.ErrUntransferableShares, "zero shares") + } + + fromDelegation, found := k.stakingKeeper.GetDelegation(ctx, fromDelegator, valAddr) + if !found { + return sdk.Dec{}, types.ErrNoDelegatorForAddress + } + validator, found := k.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + return sdk.Dec{}, types.ErrNoValidatorFound + } + // Prevent validators from reducing their self delegation below the min. + isValidatorOperator := fromDelegator.Equals(valAddr) + if isValidatorOperator { + if isBelowMinSelfDelegation(validator, fromDelegation.Shares.Sub(shares)) { + return sdk.Dec{}, types.ErrSelfDelegationBelowMinimum + } + } + + returnAmount, err := k.fastUndelegate(ctx, valAddr, fromDelegator, shares) + if err != nil { + return sdk.Dec{}, err + } + returnCoins := sdk.NewCoins(sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), returnAmount)) + + if err := k.bankKeeper.SendCoins(ctx, fromDelegator, toDelegator, returnCoins); err != nil { + return sdk.Dec{}, err + } + receivedShares, err := k.delegateFromAccount(ctx, valAddr, toDelegator, returnAmount) + if err != nil { + return sdk.Dec{}, err + } + + return receivedShares, nil +} + +// isBelowMinSelfDelegation check if the supplied shares, converted to tokens, are under the validator's min_self_delegation. +func isBelowMinSelfDelegation(validator stakingtypes.ValidatorI, shares sdk.Dec) bool { + return validator.TokensFromShares(shares).TruncateInt().LT(validator.GetMinSelfDelegation()) +} + +// fastUndelegate undelegates shares from a validator skipping the unbonding period and not creating any unbonding delegations. +func (k Keeper) fastUndelegate(ctx sdk.Context, valAddr sdk.ValAddress, delegator sdk.AccAddress, shares sdk.Dec) (sdk.Int, error) { + validator, found := k.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + return sdk.Int{}, types.ErrNoDelegatorForAddress + } + + returnAmount, err := k.stakingKeeper.Unbond(ctx, delegator, valAddr, shares) + if err != nil { + return sdk.Int{}, err + } + returnCoins := sdk.NewCoins(sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), returnAmount)) + + // transfer the validator tokens to the not bonded pool + if validator.IsBonded() { + if err := k.bankKeeper.SendCoinsFromModuleToModule(ctx, stakingtypes.BondedPoolName, stakingtypes.NotBondedPoolName, returnCoins); err != nil { + panic(err) + } + } + + if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(ctx, stakingtypes.NotBondedPoolName, delegator, returnCoins); err != nil { + return sdk.Int{}, err + } + return returnAmount, nil +} + +// delegateFromAccount delegates to a validator from an account (vs redelegating from an existing delegation) +func (k Keeper) delegateFromAccount(ctx sdk.Context, valAddr sdk.ValAddress, delegator sdk.AccAddress, amount sdk.Int) (sdk.Dec, error) { + validator, found := k.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + return sdk.Dec{}, types.ErrNoValidatorFound + } + // source tokens are from an account, so subtractAccount true and tokenSrc unbonded + newShares, err := k.stakingKeeper.Delegate(ctx, delegator, amount, stakingtypes.Unbonded, validator, true) + if err != nil { + return sdk.Dec{}, err + } + return newShares, nil +} diff --git a/x/liquid/keeper/staking_test.go b/x/liquid/keeper/staking_test.go new file mode 100644 index 00000000..8ab495ee --- /dev/null +++ b/x/liquid/keeper/staking_test.go @@ -0,0 +1,378 @@ +package keeper_test + +import ( + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" + sdk "github.com/cosmos/cosmos-sdk/types" + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/liquid/types" +) + +var ( + // d is an alias for sdk.MustNewDecFromStr + d = sdk.MustNewDecFromStr + // i is an alias for sdk.NewInt + i = sdk.NewInt + // c is an alias for sdk.NewInt64Coin + c = sdk.NewInt64Coin +) + +func (suite *KeeperTestSuite) TestTransferDelegation_ValidatorStates() { + _, addrs := app.GeneratePrivKeyAddressPairs(3) + valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2] + valAddr := sdk.ValAddress(valAccAddr) + + initialBalance := i(1e9) + + notBondedModAddr := authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName) + bondedModAddr := authtypes.NewModuleAddress(stakingtypes.BondedPoolName) + + testCases := []struct { + name string + createValidator func() (delegatorShares sdk.Dec, err error) + }{ + { + name: "bonded validator", + createValidator: func() (sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, initialBalance) + delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + + // Run end blocker to update validator state to bonded. + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return delegatorShares, nil + }, + }, + { + name: "unbonded validator", + createValidator: func() (sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, initialBalance) + delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + + // Don't run end blocker, new validators are by default unbonded. + return delegatorShares, nil + }, + }, + { + name: "ubonding (jailed) validator", + createValidator: func() (sdk.Dec, error) { + val := suite.CreateNewUnbondedValidator(valAddr, initialBalance) + delegatorShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + + // Run end blocker to update validator state to bonded. + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + // Jail and run end blocker to transition validator to unbonding. + consAddr, err := val.GetConsAddr() + if err != nil { + return sdk.Dec{}, err + } + suite.StakingKeeper.Jail(suite.Ctx, consAddr) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return delegatorShares, nil + }, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9))) + suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(i(1e9))) + + fromDelegationShares, err := tc.createValidator() + suite.Require().NoError(err) + + validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr) + suite.Require().True(found) + notBondedBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, notBondedModAddr) + bondedBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, bondedModAddr) + + shares := d("1000") + + _, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, shares) + suite.Require().NoError(err) + + // Transferring a delegation should move shares, and leave the validator and pool balances the same. + + suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(shares)) + suite.DelegationSharesEqual(valAddr, toDelegator, shares) // also creates new delegation + + validatorAfter, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr) + suite.Require().True(found) + suite.Equal(validator.GetTokens(), validatorAfter.GetTokens()) + suite.Equal(validator.GetDelegatorShares(), validatorAfter.GetDelegatorShares()) + suite.Equal(validator.GetStatus(), validatorAfter.GetStatus()) + + suite.AccountBalanceEqual(notBondedModAddr, notBondedBalance) + suite.AccountBalanceEqual(bondedModAddr, bondedBalance) + }) + } +} + +func (suite *KeeperTestSuite) TestTransferDelegation_Shares() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2] + valAddr := sdk.ValAddress(valAccAddr) + + initialBalance := i(1e12) + + testCases := []struct { + name string + createDelegations func() (fromDelegatorShares, toDelegatorShares sdk.Dec, err error) + shares sdk.Dec + expectReceived sdk.Dec + expectedErr error + }{ + { + name: "negative shares cannot be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + // Run end blocker to update validator state to bonded. + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, sdk.ZeroDec(), nil + }, + shares: d("-1.0"), + expectedErr: types.ErrUntransferableShares, + }, + { + name: "nil shares cannot be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, sdk.ZeroDec(), nil + }, + shares: sdk.Dec{}, + expectedErr: types.ErrUntransferableShares, + }, + { + name: "0 shares cannot be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(2e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, toDelegationShares, nil + }, + shares: sdk.ZeroDec(), + expectedErr: types.ErrUntransferableShares, + }, + { + name: "all shares can be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(2e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, toDelegationShares, nil + }, + shares: d("1000000000.0"), + expectReceived: d("1000000000.0"), + }, + { + name: "excess shares cannot be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, sdk.ZeroDec(), nil + }, + shares: d("1000000000.000000000000000001"), + expectedErr: stakingtypes.ErrNotEnoughDelegationShares, + }, + { + name: "shares can be transferred to a non existent delegation", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return fromDelegationShares, sdk.ZeroDec(), nil + }, + shares: d("500000000.0"), + expectReceived: d("500000000.0"), + }, + { + name: "shares cannot be transferred from a non existent delegation", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + return sdk.ZeroDec(), sdk.ZeroDec(), nil + }, + shares: d("500000000.0"), + expectedErr: types.ErrNoDelegatorForAddress, + }, + { + name: "slashed validator shares can be transferred", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + suite.SlashValidator(valAddr, d("0.05")) + + return fromDelegationShares, sdk.ZeroDec(), nil + }, + shares: d("500000000.0"), + expectReceived: d("500000000.0"), + }, + { + name: "zero shares received when transfer < 1 token", + createDelegations: func() (sdk.Dec, sdk.Dec, error) { + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(1e9)) + toDelegationShares := suite.CreateDelegation(valAddr, toDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + // make 1 share worth more than 1 token + suite.SlashValidator(valAddr, d("0.05")) + + return fromDelegationShares, toDelegationShares, nil + }, + shares: d("1.0"), // send 1 share (truncates to zero tokens) + expectReceived: d("0.0"), + }, + } + for _, tc := range testCases { + suite.Run(tc.name, func() { + suite.SetupTest() + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(initialBalance)) + suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(initialBalance)) + suite.CreateAccountWithAddress(toDelegator, suite.NewBondCoins(initialBalance)) + + fromDelegationShares, toDelegationShares, err := tc.createDelegations() + suite.Require().NoError(err) + validator, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr) + suite.Require().True(found) + + _, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, tc.shares) + + if tc.expectedErr != nil { + suite.ErrorIs(err, tc.expectedErr) + return + } + + suite.NoError(err) + suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(tc.shares)) + suite.DelegationSharesEqual(valAddr, toDelegator, toDelegationShares.Add(tc.expectReceived)) + + validatorAfter, found := suite.StakingKeeper.GetValidator(suite.Ctx, valAddr) + suite.Require().True(found) + // total tokens should not change + suite.Equal(validator.GetTokens(), validatorAfter.GetTokens()) + // but total shares can differ + suite.Equal( + validator.GetDelegatorShares().Sub(tc.shares).Add(tc.expectReceived), + validatorAfter.GetDelegatorShares(), + ) + }) + } +} + +func (suite *KeeperTestSuite) TestTransferDelegation_RedelegationsForbidden() { + _, addrs := app.GeneratePrivKeyAddressPairs(4) + val1AccAddr, val2AccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2], addrs[3] + val1Addr := sdk.ValAddress(val1AccAddr) + val2Addr := sdk.ValAddress(val2AccAddr) + + initialBalance := i(1e12) + + suite.CreateAccountWithAddress(val1AccAddr, suite.NewBondCoins(initialBalance)) + suite.CreateAccountWithAddress(val2AccAddr, suite.NewBondCoins(initialBalance)) + suite.CreateAccountWithAddress(fromDelegator, suite.NewBondCoins(initialBalance)) + + // create bonded validator 1 with a delegation + suite.CreateNewUnbondedValidator(val1Addr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(val1Addr, fromDelegator, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + // create validator 2 and redelegate to it + suite.CreateNewUnbondedValidator(val2Addr, i(1e9)) + suite.CreateRedelegation(fromDelegator, val1Addr, val2Addr, i(1e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + _, err := suite.Keeper.TransferDelegation(suite.Ctx, val2Addr, fromDelegator, toDelegator, fromDelegationShares) + suite.ErrorIs(err, types.ErrRedelegationsNotCompleted) + suite.DelegationSharesEqual(val2Addr, fromDelegator, fromDelegationShares) + suite.DelegationSharesEqual(val2Addr, toDelegator, sdk.ZeroDec()) +} + +func (suite *KeeperTestSuite) TestTransferDelegation_CompliesWithMinSelfDelegation() { + _, addrs := app.GeneratePrivKeyAddressPairs(4) + valAccAddr, toDelegator := addrs[0], addrs[1] + valAddr := sdk.ValAddress(valAccAddr) + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e12))) + + // create bonded validator with minimum delegated + minSelfDelegation := i(1e9) + delegation := suite.NewBondCoin(i(1e9)) + msg, err := stakingtypes.NewMsgCreateValidator( + valAddr, + ed25519.GenPrivKey().PubKey(), + delegation, + stakingtypes.Description{}, + stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), + minSelfDelegation, + ) + suite.Require().NoError(err) + + msgServer := stakingkeeper.NewMsgServerImpl(suite.StakingKeeper) + _, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg) + suite.Require().NoError(err) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + _, err = suite.Keeper.TransferDelegation(suite.Ctx, valAddr, valAccAddr, toDelegator, d("0.000000000000000001")) + suite.ErrorIs(err, types.ErrSelfDelegationBelowMinimum) + suite.DelegationSharesEqual(valAddr, valAccAddr, delegation.Amount.ToDec()) +} + +func (suite *KeeperTestSuite) TestTransferDelegation_CanTransferVested() { + _, addrs := app.GeneratePrivKeyAddressPairs(4) + valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2] + valAddr := sdk.ValAddress(valAccAddr) + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9))) + suite.CreateVestingAccountWithAddress(fromDelegator, suite.NewBondCoins(i(2e9)), suite.NewBondCoins(i(1e9))) + + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + fromDelegationShares := suite.CreateDelegation(valAddr, fromDelegator, i(2e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + shares := d("1000000000.0") + _, err := suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, shares) + suite.NoError(err) + suite.DelegationSharesEqual(valAddr, fromDelegator, fromDelegationShares.Sub(shares)) + suite.DelegationSharesEqual(valAddr, toDelegator, shares) +} + +func (suite *KeeperTestSuite) TestTransferDelegation_CannotTransferVesting() { + _, addrs := app.GeneratePrivKeyAddressPairs(4) + valAccAddr, fromDelegator, toDelegator := addrs[0], addrs[1], addrs[2] + valAddr := sdk.ValAddress(valAccAddr) + + suite.CreateAccountWithAddress(valAccAddr, suite.NewBondCoins(i(1e9))) + suite.CreateVestingAccountWithAddress(fromDelegator, suite.NewBondCoins(i(2e9)), suite.NewBondCoins(i(1e9))) + + suite.CreateNewUnbondedValidator(valAddr, i(1e9)) + suite.CreateDelegation(valAddr, fromDelegator, i(2e9)) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + _, err := suite.Keeper.TransferDelegation(suite.Ctx, valAddr, fromDelegator, toDelegator, d("1000000001.0")) + suite.ErrorIs(err, sdkerrors.ErrInsufficientFunds) +} diff --git a/x/liquid/module.go b/x/liquid/module.go new file mode 100644 index 00000000..a3756120 --- /dev/null +++ b/x/liquid/module.go @@ -0,0 +1,138 @@ +package liquid + +import ( + "encoding/json" + + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/spf13/cobra" + + "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" + + abci "github.com/tendermint/tendermint/abci/types" + + "github.com/kava-labs/kava/x/liquid/client/cli" + "github.com/kava-labs/kava/x/liquid/keeper" + "github.com/kava-labs/kava/x/liquid/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(clientCtx client.Context, mux *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 cli.GetQueryCmd() +} + +//____________________________________________________________________________ + +// 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{} +} diff --git a/x/liquid/types/codec.go b/x/liquid/types/codec.go new file mode 100644 index 00000000..9a6a1599 --- /dev/null +++ b/x/liquid/types/codec.go @@ -0,0 +1,38 @@ +package types + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/codec/types" + "github.com/cosmos/cosmos-sdk/types/msgservice" + + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// RegisterLegacyAminoCodec registers all the necessary types and interfaces for the module. +func RegisterLegacyAminoCodec(cdc *codec.LegacyAmino) { + cdc.RegisterConcrete(&MsgMintDerivative{}, "liquid/MsgMintDerivative", nil) + cdc.RegisterConcrete(&MsgBurnDerivative{}, "liquid/MsgBurnDerivative", 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), + &MsgMintDerivative{}, + &MsgBurnDerivative{}, + ) + + msgservice.RegisterMsgServiceDesc(registry, &_Msg_serviceDesc) +} + +var ( + amino = codec.NewLegacyAmino() + ModuleCdc = codec.NewAminoCodec(amino) +) + +func init() { + RegisterLegacyAminoCodec(amino) + cryptocodec.RegisterCrypto(amino) +} diff --git a/x/liquid/types/common_test.go b/x/liquid/types/common_test.go new file mode 100644 index 00000000..e88e1d5e --- /dev/null +++ b/x/liquid/types/common_test.go @@ -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()) +} diff --git a/x/liquid/types/errors.go b/x/liquid/types/errors.go new file mode 100644 index 00000000..e4d6652a --- /dev/null +++ b/x/liquid/types/errors.go @@ -0,0 +1,15 @@ +package types + +import ( + sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" +) + +var ( + ErrNoValidatorFound = sdkerrors.New(ModuleName, 2, "validator does not exist") + ErrNoDelegatorForAddress = sdkerrors.New(ModuleName, 3, "delegator does not contain delegation") + ErrInvalidDenom = sdkerrors.New(ModuleName, 4, "invalid denom") + ErrNotEnoughDelegationShares = sdkerrors.New(ModuleName, 5, "not enough delegation shares") + ErrRedelegationsNotCompleted = sdkerrors.New(ModuleName, 6, "active redelegations cannot be transferred") + ErrUntransferableShares = sdkerrors.New(ModuleName, 7, "shares cannot be transferred") + ErrSelfDelegationBelowMinimum = sdkerrors.Register(ModuleName, 8, "validator's self delegation must be greater than their minimum self delegation") +) diff --git a/x/liquid/types/events.go b/x/liquid/types/events.go new file mode 100644 index 00000000..27658719 --- /dev/null +++ b/x/liquid/types/events.go @@ -0,0 +1,11 @@ +package types + +const ( + EventTypeMintDerivative = "mint_derivative" + EventTypeBurnDerivative = "burn_derivative" + + AttributeValueCategory = ModuleName + AttributeKeyDelegator = "delegator" + AttributeKeyValidator = "validator" + AttributeKeySharesTransferred = "shares_transferred" +) diff --git a/x/liquid/types/expected_keepers.go b/x/liquid/types/expected_keepers.go new file mode 100644 index 00000000..c3d9ca8a --- /dev/null +++ b/x/liquid/types/expected_keepers.go @@ -0,0 +1,46 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/staking/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" +) + +// BankKeeper defines the expected bank keeper +type BankKeeper interface { + SendCoinsFromModuleToModule(ctx sdk.Context, senderModule, recipientModule string, amt sdk.Coins) error + SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error + SendCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error + SendCoins(ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) error + + MintCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + BurnCoins(ctx sdk.Context, moduleName string, amt sdk.Coins) error + UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error +} + +// AccountKeeper defines the expected keeper interface for interacting with account +type AccountKeeper interface { + GetModuleAccount(ctx sdk.Context, name string) authtypes.ModuleAccountI +} + +// StakingKeeper defines the expected keeper interface for interacting with staking +type StakingKeeper interface { + BondDenom(ctx sdk.Context) (res string) + + GetValidator(ctx sdk.Context, addr sdk.ValAddress) (validator stakingtypes.Validator, found bool) + GetDelegation(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) (delegation types.Delegation, found bool) + HasReceivingRedelegation(ctx sdk.Context, delAddr sdk.AccAddress, valDstAddr sdk.ValAddress) bool + + ValidateUnbondAmount( + ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt sdk.Int, + ) (shares sdk.Dec, err error) + + Delegate( + ctx sdk.Context, delAddr sdk.AccAddress, bondAmt sdk.Int, tokenSrc stakingtypes.BondStatus, + validator stakingtypes.Validator, subtractAccount bool, + ) (newShares sdk.Dec, err error) + Unbond( + ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares sdk.Dec, + ) (amount sdk.Int, err error) +} diff --git a/x/liquid/types/key.go b/x/liquid/types/key.go new file mode 100644 index 00000000..ba3d9822 --- /dev/null +++ b/x/liquid/types/key.go @@ -0,0 +1,26 @@ +package types + +import ( + "fmt" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +const ( + // ModuleName The name that will be used throughout the module + ModuleName = "liquid" + + // RouterKey Top level router key + RouterKey = ModuleName + + // ModuleAccountName is the module account's name + ModuleAccountName = ModuleName + + DefaultDerivativeDenom = "bkava" + + DenomSeparator = "-" +) + +func GetLiquidStakingTokenDenom(bondDenom string, valAddr sdk.ValAddress) string { + return fmt.Sprintf("%s%s%s", bondDenom, DenomSeparator, valAddr.String()) +} diff --git a/x/liquid/types/msg.go b/x/liquid/types/msg.go new file mode 100644 index 00000000..8b37ac22 --- /dev/null +++ b/x/liquid/types/msg.go @@ -0,0 +1,120 @@ +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 ( + // TypeMsgMintDerivative represents the type string for MsgMintDerivative + TypeMsgMintDerivative = "mint_derivative" + // TypeMsgBurnDerivative represents the type string for MsgBurnDerivative + TypeMsgBurnDerivative = "burn_derivative" +) + +// ensure Msg interface compliance at compile time +var ( + _ sdk.Msg = &MsgMintDerivative{} + _ legacytx.LegacyMsg = &MsgMintDerivative{} + _ sdk.Msg = &MsgBurnDerivative{} + _ legacytx.LegacyMsg = &MsgBurnDerivative{} +) + +// NewMsgMintDerivative returns a new MsgMintDerivative +func NewMsgMintDerivative(sender sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) MsgMintDerivative { + return MsgMintDerivative{ + Sender: sender.String(), + Validator: validator.String(), + Amount: amount, + } +} + +// Route return the message type used for routing the message. +func (msg MsgMintDerivative) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgMintDerivative) Type() string { return TypeMsgMintDerivative } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgMintDerivative) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + + _, err = sdk.ValAddressFromBech32(msg.Validator) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + + 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 MsgMintDerivative) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgMintDerivative) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} + +// NewMsgBurnDerivative returns a new MsgBurnDerivative +func NewMsgBurnDerivative(sender sdk.AccAddress, validator sdk.ValAddress, amount sdk.Coin) MsgBurnDerivative { + return MsgBurnDerivative{ + Sender: sender.String(), + Validator: validator.String(), + Amount: amount, + } +} + +// Route return the message type used for routing the message. +func (msg MsgBurnDerivative) Route() string { return RouterKey } + +// Type returns a human-readable string for the message, intended for utilization within tags. +func (msg MsgBurnDerivative) Type() string { return TypeMsgBurnDerivative } + +// ValidateBasic does a simple validation check that doesn't require access to any other information. +func (msg MsgBurnDerivative) ValidateBasic() error { + _, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + + _, err = sdk.ValAddressFromBech32(msg.Validator) + if err != nil { + return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error()) + } + + 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 MsgBurnDerivative) GetSignBytes() []byte { + bz := ModuleCdc.MustMarshalJSON(&msg) + return sdk.MustSortJSON(bz) +} + +// GetSigners returns the addresses of signers that must sign. +func (msg MsgBurnDerivative) GetSigners() []sdk.AccAddress { + sender, err := sdk.AccAddressFromBech32(msg.Sender) + if err != nil { + panic(err) + } + return []sdk.AccAddress{sender} +} diff --git a/x/liquid/types/msg_test.go b/x/liquid/types/msg_test.go new file mode 100644 index 00000000..1ca3cda8 --- /dev/null +++ b/x/liquid/types/msg_test.go @@ -0,0 +1,163 @@ +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/liquid/types" +) + +func TestMsgMintDerivative_Signing(t *testing.T) { + address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d") + validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42") + + msg := types.NewMsgMintDerivative( + 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":"liquid/MsgMintDerivative","value":{"amount":{"amount":"1000000000","denom":"ukava"},"sender":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`, + ) + + assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners()) + assert.Equal(t, signBytes, msg.GetSignBytes()) +} + +func TestMsgBurnDerivative_Signing(t *testing.T) { + address := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d") + validatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42") + + msg := types.NewMsgBurnDerivative( + address, + validatorAddress, + sdk.NewCoin("bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42", sdk.NewInt(1e9)), + ) + + // checking for the "type" field ensures the msg is registered on the amino codec + signBytes := []byte( + `{"type":"liquid/MsgBurnDerivative","value":{"amount":{"amount":"1000000000","denom":"bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"},"sender":"kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d","validator":"kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42"}}`, + ) + + assert.Equal(t, []sdk.AccAddress{address}, msg.GetSigners()) + assert.Equal(t, signBytes, msg.GetSignBytes()) +} + +func TestMsg_Validate(t *testing.T) { + validAddress := mustAccAddressFromBech32("kava1gepm4nwzz40gtpur93alv9f9wm5ht4l0hzzw9d") + validValidatorAddress := mustValAddressFromBech32("kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42") + validCoin := sdk.NewInt64Coin("ukava", 1e9) + + type msgArgs struct { + sender string + validator string + amount sdk.Coin + } + tests := []struct { + name string + msgArgs msgArgs + expectedErr error + }{ + { + name: "normal is valid", + msgArgs: msgArgs{ + sender: validAddress.String(), + validator: validValidatorAddress.String(), + amount: validCoin, + }, + }, + { + name: "invalid sender", + msgArgs: msgArgs{ + sender: "invalid", + validator: validValidatorAddress.String(), + amount: validCoin, + }, + expectedErr: sdkerrors.ErrInvalidAddress, + }, + { + name: "invalid short sender", + msgArgs: msgArgs{ + sender: "kava1uexte6", // encoded zero length address + validator: validValidatorAddress.String(), + amount: validCoin, + }, + expectedErr: sdkerrors.ErrInvalidAddress, + }, + { + name: "invalid validator", + msgArgs: msgArgs{ + sender: validAddress.String(), + validator: "invalid", + amount: validCoin, + }, + expectedErr: sdkerrors.ErrInvalidAddress, + }, + { + name: "invalid nil coin", + msgArgs: msgArgs{ + sender: validAddress.String(), + validator: validValidatorAddress.String(), + amount: sdk.Coin{}, + }, + expectedErr: sdkerrors.ErrInvalidCoins, + }, + { + name: "invalid zero coin", + msgArgs: msgArgs{ + sender: validAddress.String(), + validator: validValidatorAddress.String(), + amount: sdk.NewInt64Coin("ukava", 0), + }, + expectedErr: sdkerrors.ErrInvalidCoins, + }, + } + + for _, tc := range tests { + msgs := []sdk.Msg{ + &types.MsgMintDerivative{ + Sender: tc.msgArgs.sender, + Validator: tc.msgArgs.validator, + Amount: tc.msgArgs.amount, + }, + &types.MsgBurnDerivative{ + Sender: tc.msgArgs.sender, + Validator: tc.msgArgs.validator, + Amount: tc.msgArgs.amount, + }, + } + for _, msg := range msgs { + t.Run(fmt.Sprintf("%s/%T", tc.name, 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 +} diff --git a/x/liquid/types/tx.pb.go b/x/liquid/types/tx.pb.go new file mode 100644 index 00000000..42619710 --- /dev/null +++ b/x/liquid/types/tx.pb.go @@ -0,0 +1,1188 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: kava/liquid/v1beta1/tx.proto + +package types + +import ( + context "context" + fmt "fmt" + _ "github.com/cosmos/cosmos-proto" + github_com_cosmos_cosmos_sdk_types "github.com/cosmos/cosmos-sdk/types" + types "github.com/cosmos/cosmos-sdk/types" + _ "github.com/gogo/protobuf/gogoproto" + grpc1 "github.com/gogo/protobuf/grpc" + proto "github.com/gogo/protobuf/proto" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + io "io" + math "math" + math_bits "math/bits" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +// MsgMintDerivative defines the Msg/MintDerivative request type. +type MsgMintDerivative struct { + // sender is the owner of the delegation to be converted + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + // validator is the validator of the delegation to be converted + Validator string `protobuf:"bytes,2,opt,name=validator,proto3" json:"validator,omitempty"` + // amount is the quantity of staked assets to be converted + Amount types.Coin `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount"` +} + +func (m *MsgMintDerivative) Reset() { *m = MsgMintDerivative{} } +func (m *MsgMintDerivative) String() string { return proto.CompactTextString(m) } +func (*MsgMintDerivative) ProtoMessage() {} +func (*MsgMintDerivative) Descriptor() ([]byte, []int) { + return fileDescriptor_738981106e50f269, []int{0} +} +func (m *MsgMintDerivative) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgMintDerivative) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgMintDerivative.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgMintDerivative) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgMintDerivative.Merge(m, src) +} +func (m *MsgMintDerivative) XXX_Size() int { + return m.Size() +} +func (m *MsgMintDerivative) XXX_DiscardUnknown() { + xxx_messageInfo_MsgMintDerivative.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgMintDerivative proto.InternalMessageInfo + +func (m *MsgMintDerivative) GetSender() string { + if m != nil { + return m.Sender + } + return "" +} + +func (m *MsgMintDerivative) GetValidator() string { + if m != nil { + return m.Validator + } + return "" +} + +func (m *MsgMintDerivative) GetAmount() types.Coin { + if m != nil { + return m.Amount + } + return types.Coin{} +} + +// MsgMintDerivativeResponse defines the Msg/MintDerivative response type. +type MsgMintDerivativeResponse struct { + // received is the amount of staking derivative minted and sent to the sender + Received types.Coin `protobuf:"bytes,1,opt,name=received,proto3" json:"received"` +} + +func (m *MsgMintDerivativeResponse) Reset() { *m = MsgMintDerivativeResponse{} } +func (m *MsgMintDerivativeResponse) String() string { return proto.CompactTextString(m) } +func (*MsgMintDerivativeResponse) ProtoMessage() {} +func (*MsgMintDerivativeResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_738981106e50f269, []int{1} +} +func (m *MsgMintDerivativeResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgMintDerivativeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgMintDerivativeResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgMintDerivativeResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgMintDerivativeResponse.Merge(m, src) +} +func (m *MsgMintDerivativeResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgMintDerivativeResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgMintDerivativeResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgMintDerivativeResponse proto.InternalMessageInfo + +func (m *MsgMintDerivativeResponse) GetReceived() types.Coin { + if m != nil { + return m.Received + } + return types.Coin{} +} + +// MsgBurnDerivative defines the Msg/BurnDerivative request type. +type MsgBurnDerivative struct { + // sender is the owner of the derivatives to be converted + Sender string `protobuf:"bytes,1,opt,name=sender,proto3" json:"sender,omitempty"` + // validator is the validator of the derivatives to be converted + Validator string `protobuf:"bytes,2,opt,name=validator,proto3" json:"validator,omitempty"` + // amount is the quantity of derivatives to be converted + Amount types.Coin `protobuf:"bytes,3,opt,name=amount,proto3" json:"amount"` +} + +func (m *MsgBurnDerivative) Reset() { *m = MsgBurnDerivative{} } +func (m *MsgBurnDerivative) String() string { return proto.CompactTextString(m) } +func (*MsgBurnDerivative) ProtoMessage() {} +func (*MsgBurnDerivative) Descriptor() ([]byte, []int) { + return fileDescriptor_738981106e50f269, []int{2} +} +func (m *MsgBurnDerivative) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgBurnDerivative) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgBurnDerivative.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgBurnDerivative) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgBurnDerivative.Merge(m, src) +} +func (m *MsgBurnDerivative) XXX_Size() int { + return m.Size() +} +func (m *MsgBurnDerivative) XXX_DiscardUnknown() { + xxx_messageInfo_MsgBurnDerivative.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgBurnDerivative proto.InternalMessageInfo + +func (m *MsgBurnDerivative) GetSender() string { + if m != nil { + return m.Sender + } + return "" +} + +func (m *MsgBurnDerivative) GetValidator() string { + if m != nil { + return m.Validator + } + return "" +} + +func (m *MsgBurnDerivative) GetAmount() types.Coin { + if m != nil { + return m.Amount + } + return types.Coin{} +} + +// MsgBurnDerivativeResponse defines the Msg/BurnDerivative response type. +type MsgBurnDerivativeResponse struct { + // received is the number of delegation shares sent to the sender + Received github_com_cosmos_cosmos_sdk_types.Dec `protobuf:"bytes,1,opt,name=received,proto3,customtype=github.com/cosmos/cosmos-sdk/types.Dec" json:"received"` +} + +func (m *MsgBurnDerivativeResponse) Reset() { *m = MsgBurnDerivativeResponse{} } +func (m *MsgBurnDerivativeResponse) String() string { return proto.CompactTextString(m) } +func (*MsgBurnDerivativeResponse) ProtoMessage() {} +func (*MsgBurnDerivativeResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_738981106e50f269, []int{3} +} +func (m *MsgBurnDerivativeResponse) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *MsgBurnDerivativeResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_MsgBurnDerivativeResponse.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *MsgBurnDerivativeResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_MsgBurnDerivativeResponse.Merge(m, src) +} +func (m *MsgBurnDerivativeResponse) XXX_Size() int { + return m.Size() +} +func (m *MsgBurnDerivativeResponse) XXX_DiscardUnknown() { + xxx_messageInfo_MsgBurnDerivativeResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_MsgBurnDerivativeResponse proto.InternalMessageInfo + +func init() { + proto.RegisterType((*MsgMintDerivative)(nil), "kava.liquid.v1beta1.MsgMintDerivative") + proto.RegisterType((*MsgMintDerivativeResponse)(nil), "kava.liquid.v1beta1.MsgMintDerivativeResponse") + proto.RegisterType((*MsgBurnDerivative)(nil), "kava.liquid.v1beta1.MsgBurnDerivative") + proto.RegisterType((*MsgBurnDerivativeResponse)(nil), "kava.liquid.v1beta1.MsgBurnDerivativeResponse") +} + +func init() { proto.RegisterFile("kava/liquid/v1beta1/tx.proto", fileDescriptor_738981106e50f269) } + +var fileDescriptor_738981106e50f269 = []byte{ + // 421 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xcc, 0x53, 0xbd, 0xae, 0xda, 0x30, + 0x18, 0x8d, 0x7b, 0x2b, 0x54, 0x5c, 0xe9, 0x4a, 0x4d, 0xef, 0x10, 0xd0, 0x55, 0x40, 0x0c, 0x88, + 0x25, 0x4e, 0xa1, 0x43, 0x87, 0x76, 0x69, 0xca, 0xca, 0x92, 0x2e, 0xa8, 0x4b, 0xe5, 0x24, 0x56, + 0xb0, 0x00, 0x9b, 0xda, 0x4e, 0x44, 0xdf, 0xa2, 0x0f, 0xd0, 0xc7, 0xe0, 0x21, 0x18, 0x11, 0x53, + 0xdb, 0x01, 0x55, 0xf0, 0x22, 0x55, 0x12, 0x13, 0xca, 0x9f, 0xc4, 0x78, 0xa7, 0x38, 0x3e, 0xe7, + 0x7c, 0x3e, 0xe7, 0xfb, 0x6c, 0xf8, 0x38, 0xc6, 0x29, 0x76, 0x27, 0xf4, 0x5b, 0x42, 0x23, 0x37, + 0xed, 0x06, 0x44, 0xe1, 0xae, 0xab, 0xe6, 0x68, 0x26, 0xb8, 0xe2, 0xe6, 0xeb, 0x0c, 0x45, 0x05, + 0x8a, 0x34, 0x5a, 0x7f, 0x88, 0x79, 0xcc, 0x73, 0xdc, 0xcd, 0x56, 0x05, 0xb5, 0x6e, 0x87, 0x5c, + 0x4e, 0xb9, 0x74, 0x03, 0x2c, 0x49, 0x59, 0x28, 0xe4, 0x94, 0x69, 0xbc, 0x56, 0xe0, 0x5f, 0x0b, + 0x61, 0xf1, 0x53, 0x40, 0xad, 0x9f, 0x00, 0xbe, 0x1a, 0xc8, 0x78, 0x40, 0x99, 0xea, 0x13, 0x41, + 0x53, 0xac, 0x68, 0x4a, 0xcc, 0x37, 0xb0, 0x22, 0x09, 0x8b, 0x88, 0xb0, 0x40, 0x13, 0x74, 0xaa, + 0x9e, 0xb5, 0x5e, 0x38, 0x0f, 0x5a, 0xf7, 0x31, 0x8a, 0x04, 0x91, 0xf2, 0xb3, 0x12, 0x94, 0xc5, + 0xbe, 0xe6, 0x99, 0x8f, 0xb0, 0x9a, 0xe2, 0x09, 0x8d, 0xb0, 0xe2, 0xc2, 0x7a, 0x96, 0x89, 0xfc, + 0xc3, 0x86, 0xf9, 0x0e, 0x56, 0xf0, 0x94, 0x27, 0x4c, 0x59, 0x77, 0x4d, 0xd0, 0x79, 0xd9, 0xab, + 0x21, 0x5d, 0x2c, 0x73, 0xbc, 0x0f, 0x87, 0x3e, 0x71, 0xca, 0xbc, 0xe7, 0xcb, 0x4d, 0xc3, 0xf0, + 0x35, 0xbd, 0x35, 0x84, 0xb5, 0x33, 0x77, 0x3e, 0x91, 0x33, 0xce, 0x24, 0x31, 0xdf, 0xc3, 0x17, + 0x82, 0x84, 0x84, 0xa6, 0x24, 0xca, 0x7d, 0xde, 0x50, 0xb7, 0x14, 0xec, 0x83, 0x7b, 0x89, 0x60, + 0x4f, 0x31, 0x78, 0x92, 0x07, 0x3f, 0x76, 0x57, 0x06, 0x1f, 0x9e, 0x04, 0xaf, 0x7a, 0x1f, 0x32, + 0xf1, 0x9f, 0x4d, 0xa3, 0x1d, 0x53, 0x35, 0x4a, 0x02, 0x14, 0xf2, 0xa9, 0x9e, 0xb3, 0xfe, 0x38, + 0x32, 0x1a, 0xbb, 0xea, 0xfb, 0x8c, 0x48, 0xd4, 0x27, 0xe1, 0x7a, 0xe1, 0x40, 0x6d, 0xa4, 0x4f, + 0xc2, 0x43, 0x57, 0x7a, 0xbf, 0x01, 0xbc, 0x1b, 0xc8, 0xd8, 0x1c, 0xc1, 0xfb, 0x93, 0x2b, 0xd1, + 0x46, 0x17, 0xee, 0x23, 0x3a, 0x1b, 0x4e, 0x1d, 0xdd, 0xc6, 0x2b, 0xb3, 0x8c, 0xe0, 0xfd, 0xc9, + 0x0c, 0xae, 0x9e, 0x74, 0xcc, 0xbb, 0x7e, 0xd2, 0xe5, 0xae, 0x79, 0xde, 0x72, 0x6b, 0x83, 0xd5, + 0xd6, 0x06, 0x7f, 0xb7, 0x36, 0xf8, 0xb1, 0xb3, 0x8d, 0xd5, 0xce, 0x36, 0x7e, 0xed, 0x6c, 0xe3, + 0x4b, 0xe7, 0xbf, 0xae, 0x65, 0x35, 0x9d, 0x09, 0x0e, 0x64, 0xbe, 0x72, 0xe7, 0xfb, 0xf7, 0x99, + 0xf7, 0x2e, 0xa8, 0xe4, 0xaf, 0xe6, 0xed, 0xbf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x0a, 0x25, 0x6e, + 0x57, 0xbb, 0x03, 0x00, 0x00, +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// MsgClient is the client API for Msg service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. +type MsgClient interface { + // MintDerivative defines a method for converting a delegation into staking deriviatives. + MintDerivative(ctx context.Context, in *MsgMintDerivative, opts ...grpc.CallOption) (*MsgMintDerivativeResponse, error) + // BurnDerivative defines a method for converting staking deriviatives into a delegation. + BurnDerivative(ctx context.Context, in *MsgBurnDerivative, opts ...grpc.CallOption) (*MsgBurnDerivativeResponse, error) +} + +type msgClient struct { + cc grpc1.ClientConn +} + +func NewMsgClient(cc grpc1.ClientConn) MsgClient { + return &msgClient{cc} +} + +func (c *msgClient) MintDerivative(ctx context.Context, in *MsgMintDerivative, opts ...grpc.CallOption) (*MsgMintDerivativeResponse, error) { + out := new(MsgMintDerivativeResponse) + err := c.cc.Invoke(ctx, "/kava.liquid.v1beta1.Msg/MintDerivative", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *msgClient) BurnDerivative(ctx context.Context, in *MsgBurnDerivative, opts ...grpc.CallOption) (*MsgBurnDerivativeResponse, error) { + out := new(MsgBurnDerivativeResponse) + err := c.cc.Invoke(ctx, "/kava.liquid.v1beta1.Msg/BurnDerivative", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MsgServer is the server API for Msg service. +type MsgServer interface { + // MintDerivative defines a method for converting a delegation into staking deriviatives. + MintDerivative(context.Context, *MsgMintDerivative) (*MsgMintDerivativeResponse, error) + // BurnDerivative defines a method for converting staking deriviatives into a delegation. + BurnDerivative(context.Context, *MsgBurnDerivative) (*MsgBurnDerivativeResponse, error) +} + +// UnimplementedMsgServer can be embedded to have forward compatible implementations. +type UnimplementedMsgServer struct { +} + +func (*UnimplementedMsgServer) MintDerivative(ctx context.Context, req *MsgMintDerivative) (*MsgMintDerivativeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method MintDerivative not implemented") +} +func (*UnimplementedMsgServer) BurnDerivative(ctx context.Context, req *MsgBurnDerivative) (*MsgBurnDerivativeResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method BurnDerivative not implemented") +} + +func RegisterMsgServer(s grpc1.Server, srv MsgServer) { + s.RegisterService(&_Msg_serviceDesc, srv) +} + +func _Msg_MintDerivative_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgMintDerivative) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).MintDerivative(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/kava.liquid.v1beta1.Msg/MintDerivative", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).MintDerivative(ctx, req.(*MsgMintDerivative)) + } + return interceptor(ctx, in, info, handler) +} + +func _Msg_BurnDerivative_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MsgBurnDerivative) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MsgServer).BurnDerivative(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/kava.liquid.v1beta1.Msg/BurnDerivative", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MsgServer).BurnDerivative(ctx, req.(*MsgBurnDerivative)) + } + return interceptor(ctx, in, info, handler) +} + +var _Msg_serviceDesc = grpc.ServiceDesc{ + ServiceName: "kava.liquid.v1beta1.Msg", + HandlerType: (*MsgServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "MintDerivative", + Handler: _Msg_MintDerivative_Handler, + }, + { + MethodName: "BurnDerivative", + Handler: _Msg_BurnDerivative_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "kava/liquid/v1beta1/tx.proto", +} + +func (m *MsgMintDerivative) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgMintDerivative) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgMintDerivative) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Amount.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + if len(m.Validator) > 0 { + i -= len(m.Validator) + copy(dAtA[i:], m.Validator) + i = encodeVarintTx(dAtA, i, uint64(len(m.Validator))) + i-- + dAtA[i] = 0x12 + } + if len(m.Sender) > 0 { + i -= len(m.Sender) + copy(dAtA[i:], m.Sender) + i = encodeVarintTx(dAtA, i, uint64(len(m.Sender))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgMintDerivativeResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgMintDerivativeResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgMintDerivativeResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Received.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func (m *MsgBurnDerivative) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgBurnDerivative) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgBurnDerivative) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size, err := m.Amount.MarshalToSizedBuffer(dAtA[:i]) + if err != nil { + return 0, err + } + i -= size + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0x1a + if len(m.Validator) > 0 { + i -= len(m.Validator) + copy(dAtA[i:], m.Validator) + i = encodeVarintTx(dAtA, i, uint64(len(m.Validator))) + i-- + dAtA[i] = 0x12 + } + if len(m.Sender) > 0 { + i -= len(m.Sender) + copy(dAtA[i:], m.Sender) + i = encodeVarintTx(dAtA, i, uint64(len(m.Sender))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *MsgBurnDerivativeResponse) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *MsgBurnDerivativeResponse) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *MsgBurnDerivativeResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + { + size := m.Received.Size() + i -= size + if _, err := m.Received.MarshalTo(dAtA[i:]); err != nil { + return 0, err + } + i = encodeVarintTx(dAtA, i, uint64(size)) + } + i-- + dAtA[i] = 0xa + return len(dAtA) - i, nil +} + +func encodeVarintTx(dAtA []byte, offset int, v uint64) int { + offset -= sovTx(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *MsgMintDerivative) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Sender) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.Validator) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = m.Amount.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgMintDerivativeResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Received.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgBurnDerivative) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Sender) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = len(m.Validator) + if l > 0 { + n += 1 + l + sovTx(uint64(l)) + } + l = m.Amount.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func (m *MsgBurnDerivativeResponse) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = m.Received.Size() + n += 1 + l + sovTx(uint64(l)) + return n +} + +func sovTx(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozTx(x uint64) (n int) { + return sovTx(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *MsgMintDerivative) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgMintDerivative: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgMintDerivative: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Sender = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Validator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Validator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Amount.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgMintDerivativeResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgMintDerivativeResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgMintDerivativeResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Received", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Received.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgBurnDerivative) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgBurnDerivative: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgBurnDerivative: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Sender", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Sender = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Validator", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Validator = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Amount", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + msglen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Amount.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *MsgBurnDerivativeResponse) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: MsgBurnDerivativeResponse: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: MsgBurnDerivativeResponse: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Received", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTx + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTx + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthTx + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + if err := m.Received.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTx(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthTx + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipTx(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowTx + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthTx + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupTx + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthTx + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthTx = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowTx = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupTx = fmt.Errorf("proto: unexpected end of group") +)