mirror of
https://github.com/0glabs/0g-chain.git
synced 2024-12-25 07:45: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"
|
kavadistclient "github.com/kava-labs/kava/x/kavadist/client"
|
||||||
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
|
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
|
||||||
kavadisttypes "github.com/kava-labs/kava/x/kavadist/types"
|
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"
|
pricefeed "github.com/kava-labs/kava/x/pricefeed"
|
||||||
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
||||||
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
|
pricefeedtypes "github.com/kava-labs/kava/x/pricefeed/types"
|
||||||
@ -190,6 +193,7 @@ var (
|
|||||||
savings.AppModuleBasic{},
|
savings.AppModuleBasic{},
|
||||||
validatorvesting.AppModuleBasic{},
|
validatorvesting.AppModuleBasic{},
|
||||||
evmutil.AppModuleBasic{},
|
evmutil.AppModuleBasic{},
|
||||||
|
liquid.AppModuleBasic{},
|
||||||
earn.AppModuleBasic{},
|
earn.AppModuleBasic{},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,12 +219,12 @@ var (
|
|||||||
cdptypes.LiquidatorMacc: {authtypes.Minter, authtypes.Burner},
|
cdptypes.LiquidatorMacc: {authtypes.Minter, authtypes.Burner},
|
||||||
hardtypes.ModuleAccountName: {authtypes.Minter},
|
hardtypes.ModuleAccountName: {authtypes.Minter},
|
||||||
savingstypes.ModuleAccountName: nil,
|
savingstypes.ModuleAccountName: nil,
|
||||||
earntypes.ModuleName: nil,
|
liquidtypes.ModuleAccountName: {authtypes.Minter, authtypes.Burner},
|
||||||
|
earntypes.ModuleAccountName: nil,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Verify app interface at compile time
|
// Verify app interface at compile time
|
||||||
// var _ simapp.App = (*App)(nil) // TODO
|
|
||||||
var _ servertypes.Application = (*App)(nil)
|
var _ servertypes.Application = (*App)(nil)
|
||||||
|
|
||||||
// Options bundles several configuration params for an App.
|
// Options bundles several configuration params for an App.
|
||||||
@ -285,6 +289,7 @@ type App struct {
|
|||||||
committeeKeeper committeekeeper.Keeper
|
committeeKeeper committeekeeper.Keeper
|
||||||
incentiveKeeper incentivekeeper.Keeper
|
incentiveKeeper incentivekeeper.Keeper
|
||||||
savingsKeeper savingskeeper.Keeper
|
savingsKeeper savingskeeper.Keeper
|
||||||
|
liquidKeeper liquidkeeper.Keeper
|
||||||
earnKeeper earnkeeper.Keeper
|
earnKeeper earnkeeper.Keeper
|
||||||
|
|
||||||
// make scoped keepers public for test purposes
|
// make scoped keepers public for test purposes
|
||||||
@ -586,6 +591,12 @@ func NewApp(
|
|||||||
app.accountKeeper,
|
app.accountKeeper,
|
||||||
app.bankKeeper,
|
app.bankKeeper,
|
||||||
)
|
)
|
||||||
|
app.liquidKeeper = liquidkeeper.NewDefaultKeeper(
|
||||||
|
appCodec,
|
||||||
|
app.accountKeeper,
|
||||||
|
app.bankKeeper,
|
||||||
|
&app.stakingKeeper,
|
||||||
|
)
|
||||||
app.incentiveKeeper = incentivekeeper.NewKeeper(
|
app.incentiveKeeper = incentivekeeper.NewKeeper(
|
||||||
appCodec,
|
appCodec,
|
||||||
keys[incentivetypes.StoreKey],
|
keys[incentivetypes.StoreKey],
|
||||||
@ -691,6 +702,7 @@ func NewApp(
|
|||||||
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.bankKeeper, app.cdpKeeper),
|
incentive.NewAppModule(app.incentiveKeeper, app.accountKeeper, app.bankKeeper, app.cdpKeeper),
|
||||||
evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper),
|
evmutil.NewAppModule(app.evmutilKeeper, app.bankKeeper),
|
||||||
savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
|
savings.NewAppModule(app.savingsKeeper, app.accountKeeper, app.bankKeeper),
|
||||||
|
liquid.NewAppModule(app.liquidKeeper),
|
||||||
earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
|
earn.NewAppModule(app.earnKeeper, app.accountKeeper, app.bankKeeper),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -738,6 +750,7 @@ func NewApp(
|
|||||||
authz.ModuleName,
|
authz.ModuleName,
|
||||||
evmutiltypes.ModuleName,
|
evmutiltypes.ModuleName,
|
||||||
savingstypes.ModuleName,
|
savingstypes.ModuleName,
|
||||||
|
liquidtypes.ModuleName,
|
||||||
earntypes.ModuleName,
|
earntypes.ModuleName,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -777,6 +790,7 @@ func NewApp(
|
|||||||
authz.ModuleName,
|
authz.ModuleName,
|
||||||
evmutiltypes.ModuleName,
|
evmutiltypes.ModuleName,
|
||||||
savingstypes.ModuleName,
|
savingstypes.ModuleName,
|
||||||
|
liquidtypes.ModuleName,
|
||||||
earntypes.ModuleName,
|
earntypes.ModuleName,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -816,6 +830,7 @@ func NewApp(
|
|||||||
paramstypes.ModuleName,
|
paramstypes.ModuleName,
|
||||||
upgradetypes.ModuleName,
|
upgradetypes.ModuleName,
|
||||||
validatorvestingtypes.ModuleName,
|
validatorvestingtypes.ModuleName,
|
||||||
|
liquidtypes.ModuleName,
|
||||||
)
|
)
|
||||||
|
|
||||||
app.mm.RegisterInvariants(&app.crisisKeeper)
|
app.mm.RegisterInvariants(&app.crisisKeeper)
|
||||||
@ -979,10 +994,11 @@ func (app *App) loadBlockedMaccAddrs() map[string]bool {
|
|||||||
modAccAddrs := app.ModuleAccountAddrs()
|
modAccAddrs := app.ModuleAccountAddrs()
|
||||||
kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName)
|
kavadistMaccAddr := app.accountKeeper.GetModuleAddress(kavadisttypes.ModuleName)
|
||||||
earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName)
|
earnMaccAddr := app.accountKeeper.GetModuleAddress(earntypes.ModuleName)
|
||||||
|
liquidMaccAddr := app.accountKeeper.GetModuleAddress(liquidtypes.ModuleName)
|
||||||
|
|
||||||
for addr := range modAccAddrs {
|
for addr := range modAccAddrs {
|
||||||
// Set the kavadist and earn module account address as unblocked
|
// Set the kavadist and earn module account address as unblocked
|
||||||
if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() {
|
if addr == kavadistMaccAddr.String() || addr == earnMaccAddr.String() || addr == liquidMaccAddr.String() {
|
||||||
modAccAddrs[addr] = false
|
modAccAddrs[addr] = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import (
|
|||||||
incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper"
|
incentivekeeper "github.com/kava-labs/kava/x/incentive/keeper"
|
||||||
issuancekeeper "github.com/kava-labs/kava/x/issuance/keeper"
|
issuancekeeper "github.com/kava-labs/kava/x/issuance/keeper"
|
||||||
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
|
kavadistkeeper "github.com/kava-labs/kava/x/kavadist/keeper"
|
||||||
|
liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
|
||||||
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
pricefeedkeeper "github.com/kava-labs/kava/x/pricefeed/keeper"
|
||||||
savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
|
savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
|
||||||
swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
|
swapkeeper "github.com/kava-labs/kava/x/swap/keeper"
|
||||||
@ -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) GetEvmKeeper() *evmkeeper.Keeper { return tApp.evmKeeper }
|
||||||
func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper { return tApp.savingsKeeper }
|
func (tApp TestApp) GetSavingsKeeper() savingskeeper.Keeper { return tApp.savingsKeeper }
|
||||||
func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
|
func (tApp TestApp) GetFeeMarketKeeper() feemarketkeeper.Keeper { return tApp.feeMarketKeeper }
|
||||||
|
func (tApp TestApp) GetLiquidKeeper() liquidkeeper.Keeper { return tApp.liquidKeeper }
|
||||||
func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper }
|
func (tApp TestApp) GetEarnKeeper() earnkeeper.Keeper { return tApp.earnKeeper }
|
||||||
|
|
||||||
// LegacyAmino returns the app's amino codec.
|
// 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
|
# Set evm tracer to json
|
||||||
sed -in-place='' 's/tracer = ""/tracer = "json"/g' $DATA/config/app.toml
|
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
|
# Set client chain id
|
||||||
sed -in-place='' 's/chain-id = ""/chain-id = "kavalocalnet_8888-1"/g' $DATA/config/client.toml
|
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)
|
- [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)
|
- [kava/pricefeed/v1beta1/store.proto](#kava/pricefeed/v1beta1/store.proto)
|
||||||
- [CurrentPrice](#kava.pricefeed.v1beta1.CurrentPrice)
|
- [CurrentPrice](#kava.pricefeed.v1beta1.CurrentPrice)
|
||||||
- [Market](#kava.pricefeed.v1beta1.Market)
|
- [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>
|
<a name="kava/pricefeed/v1beta1/store.proto"></a>
|
||||||
<p align="right"><a href="#top">Top</a></p>
|
<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