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 <rhuairahrighairidh@users.noreply.github.com>

Co-authored-by: Ruaridh <rhuairahrighairidh@users.noreply.github.com>
This commit is contained in:
Derrick Lee 2022-09-19 08:51:39 -07:00 committed by GitHub
parent ceaed3f0e1
commit ed116b24ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 270 additions and 15 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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)
}
})
}
}

View File

@ -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
}

View File

@ -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)
}
})
}
}