From ed116b24ba98e284d3196ccae2015f6a2da5c089 Mon Sep 17 00:00:00 2001 From: Derrick Lee Date: Mon, 19 Sep 2022 08:51:39 -0700 Subject: [PATCH] Add derivative denom and kava value methods to liquid (#1303) * Add IsDerivativeDenom and GetKavaForDerivatives methods to liquid * Add test case containing 2 different bkava denoms * Add doc to GetKavaForDerivatives * Remove logging statements, use keeper logger * Fix nil err use * Return error from GetKavaForDerivatives * Re-add ParseLiquidStakingTokenDenom * Add ParseLiquidStakingTokenDenom tests * Use DenomSeparator instead of str Co-authored-by: Ruaridh Co-authored-by: Ruaridh --- x/liquid/client/cli/tx.go | 16 +-- x/liquid/keeper/derivative.go | 38 +++++++ x/liquid/keeper/derivative_test.go | 155 +++++++++++++++++++++++++++++ x/liquid/types/key.go | 20 ++++ x/liquid/types/key_test.go | 56 +++++++++++ 5 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 x/liquid/types/key_test.go diff --git a/x/liquid/client/cli/tx.go b/x/liquid/client/cli/tx.go index 6ef2043f..c18f6065 100644 --- a/x/liquid/client/cli/tx.go +++ b/x/liquid/client/cli/tx.go @@ -2,7 +2,6 @@ package cli import ( "fmt" - "strings" "github.com/spf13/cobra" @@ -94,7 +93,7 @@ func getCmdBurnDerivative() *cobra.Command { return err } - valAddr, err := parseLiquidStakingTokenDenom(amount.Denom) + valAddr, err := types.ParseLiquidStakingTokenDenom(amount.Denom) if err != nil { return sdkerrors.Wrap(types.ErrInvalidDenom, err.Error()) } @@ -107,16 +106,3 @@ func getCmdBurnDerivative() *cobra.Command { }, } } - -// parseLiquidStakingTokenDenom extracts a validator address from a derivative denom. -func parseLiquidStakingTokenDenom(denom string) (sdk.ValAddress, error) { - elements := strings.Split(denom, types.DenomSeparator) - if len(elements) != 2 { - return nil, fmt.Errorf("cannot parse denom %s", denom) - } - addr, err := sdk.ValAddressFromBech32(elements[1]) - if err != nil { - return nil, err - } - return addr, nil -} diff --git a/x/liquid/keeper/derivative.go b/x/liquid/keeper/derivative.go index cac2b74b..fbed7ec4 100644 --- a/x/liquid/keeper/derivative.go +++ b/x/liquid/keeper/derivative.go @@ -1,6 +1,8 @@ package keeper import ( + "fmt" + sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" @@ -97,6 +99,42 @@ func (k Keeper) GetLiquidStakingTokenDenom(valAddr sdk.ValAddress) string { return types.GetLiquidStakingTokenDenom(k.derivativeDenom, valAddr) } +// IsDerivativeDenom returns true if the denom is a valid derivative denom and +// corresponds to a valid validator. +func (k Keeper) IsDerivativeDenom(ctx sdk.Context, denom string) bool { + valAddr, err := types.ParseLiquidStakingTokenDenom(denom) + if err != nil { + return false + } + + _, found := k.stakingKeeper.GetValidator(ctx, valAddr) + return found +} + +// GetKavaForDerivatives returns the total amount of the provided derivatives +// in Kava accounting for the specific share prices. +func (k Keeper) GetKavaForDerivatives(ctx sdk.Context, coins sdk.Coins) (sdk.Int, error) { + totalKava := sdk.ZeroInt() + + for _, coin := range coins { + valAddr, err := types.ParseLiquidStakingTokenDenom(coin.Denom) + if err != nil { + return sdk.Int{}, fmt.Errorf("invalid derivative denom: %w", err) + } + + validator, found := k.stakingKeeper.GetValidator(ctx, valAddr) + if !found { + return sdk.Int{}, fmt.Errorf("invalid derivative denom %s: validator not found", coin.Denom) + } + + // bkava is 1:1 to delegation shares + valTokens := validator.TokensFromSharesTruncated(coin.Amount.ToDec()) + totalKava = totalKava.Add(valTokens.TruncateInt()) + } + + return totalKava, nil +} + 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 diff --git a/x/liquid/keeper/derivative_test.go b/x/liquid/keeper/derivative_test.go index f389b722..9713e060 100644 --- a/x/liquid/keeper/derivative_test.go +++ b/x/liquid/keeper/derivative_test.go @@ -314,3 +314,158 @@ func (suite *KeeperTestSuite) TestMintDerivative() { }) } } + +func (suite *KeeperTestSuite) TestIsDerivativeDenom() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr1, delegator, valAccAddr2 := addrs[0], addrs[1], addrs[2] + valAddr1 := sdk.ValAddress(valAccAddr1) + + // Validator addr that has **not** delegated anything + valAddr2 := sdk.ValAddress(valAccAddr2) + + initialBalance := i(1e9) + vestedBalance := i(500e6) + + suite.CreateAccountWithAddress(valAccAddr1, suite.NewBondCoins(initialBalance)) + suite.CreateVestingAccountWithAddress(delegator, suite.NewBondCoins(initialBalance), suite.NewBondCoins(vestedBalance)) + + suite.CreateNewUnbondedValidator(valAddr1, initialBalance) + suite.CreateDelegation(valAddr1, delegator, initialBalance) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + testCases := []struct { + name string + denom string + wantIsDenom bool + }{ + { + name: "valid derivative denom", + denom: suite.Keeper.GetLiquidStakingTokenDenom(valAddr1), + wantIsDenom: true, + }, + { + name: "invalid - undelegated validator addr", + denom: suite.Keeper.GetLiquidStakingTokenDenom(valAddr2), + wantIsDenom: false, + }, + { + name: "invalid - invalid val addr", + denom: "bkava-asdfasdf", + wantIsDenom: false, + }, + { + name: "invalid - ukava", + denom: "ukava", + wantIsDenom: false, + }, + { + name: "invalid - plain bkava", + denom: "bkava", + wantIsDenom: false, + }, + { + name: "invalid - bkava prefix", + denom: "bkava-", + wantIsDenom: false, + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + isDenom := suite.Keeper.IsDerivativeDenom(suite.Ctx, tc.denom) + + suite.Require().Equal(tc.wantIsDenom, isDenom) + }) + } +} + +func (suite *KeeperTestSuite) TestGetKavaForDerivatives() { + _, addrs := app.GeneratePrivKeyAddressPairs(5) + valAccAddr1, delegator, valAccAddr2, valAccAddr3 := addrs[0], addrs[1], addrs[2], addrs[3] + valAddr1 := sdk.ValAddress(valAccAddr1) + + // Validator addr that has **not** delegated anything + valAddr2 := sdk.ValAddress(valAccAddr2) + + valAddr3 := sdk.ValAddress(valAccAddr3) + + initialBalance := i(1e9) + vestedBalance := i(500e6) + delegateAmount := i(100e6) + + suite.CreateAccountWithAddress(valAccAddr1, suite.NewBondCoins(initialBalance)) + suite.CreateVestingAccountWithAddress(delegator, suite.NewBondCoins(initialBalance), suite.NewBondCoins(vestedBalance)) + + suite.CreateNewUnbondedValidator(valAddr1, initialBalance) + suite.CreateDelegation(valAddr1, delegator, delegateAmount) + + suite.CreateAccountWithAddress(valAccAddr3, suite.NewBondCoins(initialBalance)) + + suite.CreateNewUnbondedValidator(valAddr3, initialBalance) + suite.CreateDelegation(valAddr3, delegator, delegateAmount) + staking.EndBlocker(suite.Ctx, suite.StakingKeeper) + + suite.SlashValidator(valAddr3, d("0.05")) + + _, err := suite.Keeper.MintDerivative(suite.Ctx, delegator, valAddr1, suite.NewBondCoin(delegateAmount)) + suite.Require().NoError(err) + + testCases := []struct { + name string + derivatives sdk.Coins + wantKavaAmount sdk.Int + err error + }{ + { + name: "valid derivative denom", + derivatives: sdk.NewCoins( + sdk.NewCoin(suite.Keeper.GetLiquidStakingTokenDenom(valAddr1), vestedBalance), + ), + wantKavaAmount: vestedBalance, + }, + { + name: "valid - slashed validator", + derivatives: sdk.NewCoins( + sdk.NewCoin(suite.Keeper.GetLiquidStakingTokenDenom(valAddr3), vestedBalance), + ), + // vestedBalance * 95% + wantKavaAmount: vestedBalance.Mul(sdk.NewInt(95)).Quo(sdk.NewInt(100)), + }, + { + name: "valid - sum", + derivatives: sdk.NewCoins( + sdk.NewCoin(suite.Keeper.GetLiquidStakingTokenDenom(valAddr3), vestedBalance), + sdk.NewCoin(suite.Keeper.GetLiquidStakingTokenDenom(valAddr1), vestedBalance), + ), + // vestedBalance + (vestedBalance * 95%) + wantKavaAmount: vestedBalance.Mul(sdk.NewInt(95)).Quo(sdk.NewInt(100)).Add(vestedBalance), + }, + { + name: "invalid - undelegated validator address denom", + derivatives: sdk.NewCoins( + sdk.NewCoin(suite.Keeper.GetLiquidStakingTokenDenom(valAddr2), vestedBalance), + ), + err: fmt.Errorf("invalid derivative denom %s: validator not found", suite.Keeper.GetLiquidStakingTokenDenom(valAddr2)), + }, + { + name: "invalid - denom", + derivatives: sdk.NewCoins( + sdk.NewCoin("kava", vestedBalance), + ), + err: fmt.Errorf("invalid derivative denom: cannot parse denom kava"), + }, + } + + for _, tc := range testCases { + suite.Run(tc.name, func() { + kavaAmount, err := suite.Keeper.GetKavaForDerivatives(suite.Ctx, tc.derivatives) + + if tc.err != nil { + suite.Require().Error(err) + } else { + suite.Require().NoError(err) + suite.Require().Equal(tc.wantKavaAmount, kavaAmount) + } + }) + } +} diff --git a/x/liquid/types/key.go b/x/liquid/types/key.go index ba3d9822..a244ec5e 100644 --- a/x/liquid/types/key.go +++ b/x/liquid/types/key.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "strings" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -24,3 +25,22 @@ const ( func GetLiquidStakingTokenDenom(bondDenom string, valAddr sdk.ValAddress) string { return fmt.Sprintf("%s%s%s", bondDenom, DenomSeparator, valAddr.String()) } + +// ParseLiquidStakingTokenDenom extracts a validator address from a derivative denom. +func ParseLiquidStakingTokenDenom(denom string) (sdk.ValAddress, error) { + elements := strings.Split(denom, DenomSeparator) + if len(elements) != 2 { + return nil, fmt.Errorf("cannot parse denom %s", denom) + } + + if elements[0] != DefaultDerivativeDenom { + return nil, fmt.Errorf("invalid denom prefix, expected %s, got %s", DefaultDerivativeDenom, elements[0]) + } + + addr, err := sdk.ValAddressFromBech32(elements[1]) + if err != nil { + return nil, fmt.Errorf("invalid denom validator address: %w", err) + } + + return addr, nil +} diff --git a/x/liquid/types/key_test.go b/x/liquid/types/key_test.go new file mode 100644 index 00000000..bb324a46 --- /dev/null +++ b/x/liquid/types/key_test.go @@ -0,0 +1,56 @@ +package types_test + +import ( + "fmt" + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/kava-labs/kava/app" + "github.com/kava-labs/kava/x/liquid/types" + "github.com/stretchr/testify/require" +) + +func TestParseLiquidStakingTokenDenom(t *testing.T) { + config := sdk.GetConfig() + app.SetBech32AddressPrefixes(config) + + tests := []struct { + name string + giveDenom string + wantAddress sdk.ValAddress + wantErr error + }{ + { + name: "valid denom", + giveDenom: "bkava-kavavaloper1ze7y9qwdddejmy7jlw4cymqqlt2wh05y6cpt5a", + wantAddress: mustValAddressFromBech32("kavavaloper1ze7y9qwdddejmy7jlw4cymqqlt2wh05y6cpt5a"), + wantErr: nil, + }, + { + name: "invalid prefix", + giveDenom: "ukava-kavavaloper1ze7y9qwdddejmy7jlw4cymqqlt2wh05y6cpt5a", + wantAddress: mustValAddressFromBech32("kavavaloper1ze7y9qwdddejmy7jlw4cymqqlt2wh05y6cpt5a"), + wantErr: fmt.Errorf("invalid denom prefix, expected %s, got %s", types.DefaultDerivativeDenom, "ukava"), + }, + { + name: "invalid validator address", + giveDenom: "bkava-kavavaloper1ze7y9qw", + wantAddress: sdk.ValAddress{}, + wantErr: fmt.Errorf("invalid denom validator address: decoding bech32 failed: invalid checksum"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr, err := types.ParseLiquidStakingTokenDenom(tt.giveDenom) + + if tt.wantErr != nil { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr.Error()) + } else { + require.NoError(t, err) + require.Equal(t, tt.wantAddress, addr) + } + }) + } +}