mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-23 23:05:18 +00:00
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 <derrick@dlee.dev> * Update x/liquid/client/cli/tx.go Co-authored-by: Derrick Lee <derrick@dlee.dev> * 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 <ruaridh.odonnell@gmail.com> Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com> Co-authored-by: Derrick Lee <derrick@dlee.dev>
This commit is contained in:
parent
ded692d2f4
commit
ceaed3f0e1
22
app/app.go
22
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
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
||||
|
||||
<a name="kava/liquid/v1beta1/tx.proto"></a>
|
||||
<p align="right"><a href="#top">Top</a></p>
|
||||
|
||||
## kava/liquid/v1beta1/tx.proto
|
||||
|
||||
|
||||
|
||||
<a name="kava.liquid.v1beta1.MsgBurnDerivative"></a>
|
||||
|
||||
### 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 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.liquid.v1beta1.MsgBurnDerivativeResponse"></a>
|
||||
|
||||
### 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 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.liquid.v1beta1.MsgMintDerivative"></a>
|
||||
|
||||
### 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 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<a name="kava.liquid.v1beta1.MsgMintDerivativeResponse"></a>
|
||||
|
||||
### 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 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- end messages -->
|
||||
|
||||
<!-- end enums -->
|
||||
|
||||
<!-- end HasExtensions -->
|
||||
|
||||
|
||||
<a name="kava.liquid.v1beta1.Msg"></a>
|
||||
|
||||
### 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. | |
|
||||
|
||||
<!-- end services -->
|
||||
|
||||
|
||||
|
||||
<a name="kava/pricefeed/v1beta1/store.proto"></a>
|
||||
<p align="right"><a href="#top">Top</a></p>
|
||||
|
||||
|
54
proto/kava/liquid/v1beta1/tx.proto
Normal file
54
proto/kava/liquid/v1beta1/tx.proto
Normal file
@ -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
|
||||
];
|
||||
}
|
31
x/liquid/client/cli/query.go
Normal file
31
x/liquid/client/cli/query.go
Normal file
@ -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
|
||||
}
|
122
x/liquid/client/cli/tx.go
Normal file
122
x/liquid/client/cli/tx.go
Normal file
@ -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 <key>`, version.AppName, types.ModuleName,
|
||||
),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
clientCtx, err := client.GetClientTxContext(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valAddr, err := sdk.ValAddressFromBech32(args[0])
|
||||
if err != nil {
|
||||
return sdkerrors.Wrap(sdkerrors.ErrInvalidAddress, err.Error())
|
||||
}
|
||||
|
||||
coin, err := sdk.ParseCoinNormalized(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
msg := types.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 <key>`, 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
|
||||
}
|
118
x/liquid/keeper/derivative.go
Normal file
118
x/liquid/keeper/derivative.go
Normal file
@ -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
|
||||
}
|
316
x/liquid/keeper/derivative_test.go
Normal file
316
x/liquid/keeper/derivative_test.go
Normal file
@ -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()),
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
52
x/liquid/keeper/keeper.go
Normal file
52
x/liquid/keeper/keeper.go
Normal file
@ -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))
|
||||
}
|
238
x/liquid/keeper/keeper_test.go
Normal file
238
x/liquid/keeper/keeper_test.go
Normal file
@ -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))
|
||||
}
|
84
x/liquid/keeper/msg_server.go
Normal file
84
x/liquid/keeper/msg_server.go
Normal file
@ -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
|
||||
}
|
109
x/liquid/keeper/staking.go
Normal file
109
x/liquid/keeper/staking.go
Normal file
@ -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
|
||||
}
|
378
x/liquid/keeper/staking_test.go
Normal file
378
x/liquid/keeper/staking_test.go
Normal file
@ -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)
|
||||
}
|
138
x/liquid/module.go
Normal file
138
x/liquid/module.go
Normal file
@ -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{}
|
||||
}
|
38
x/liquid/types/codec.go
Normal file
38
x/liquid/types/codec.go
Normal file
@ -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)
|
||||
}
|
13
x/liquid/types/common_test.go
Normal file
13
x/liquid/types/common_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package types_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/kava-labs/kava/app"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
app.SetSDKConfig()
|
||||
os.Exit(m.Run())
|
||||
}
|
15
x/liquid/types/errors.go
Normal file
15
x/liquid/types/errors.go
Normal file
@ -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")
|
||||
)
|
11
x/liquid/types/events.go
Normal file
11
x/liquid/types/events.go
Normal file
@ -0,0 +1,11 @@
|
||||
package types
|
||||
|
||||
const (
|
||||
EventTypeMintDerivative = "mint_derivative"
|
||||
EventTypeBurnDerivative = "burn_derivative"
|
||||
|
||||
AttributeValueCategory = ModuleName
|
||||
AttributeKeyDelegator = "delegator"
|
||||
AttributeKeyValidator = "validator"
|
||||
AttributeKeySharesTransferred = "shares_transferred"
|
||||
)
|
46
x/liquid/types/expected_keepers.go
Normal file
46
x/liquid/types/expected_keepers.go
Normal file
@ -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)
|
||||
}
|
26
x/liquid/types/key.go
Normal file
26
x/liquid/types/key.go
Normal file
@ -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())
|
||||
}
|
120
x/liquid/types/msg.go
Normal file
120
x/liquid/types/msg.go
Normal file
@ -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}
|
||||
}
|
163
x/liquid/types/msg_test.go
Normal file
163
x/liquid/types/msg_test.go
Normal file
@ -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
|
||||
}
|
1188
x/liquid/types/tx.pb.go
Normal file
1188
x/liquid/types/tx.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user