mirror of
				https://github.com/0glabs/0g-chain.git
				synced 2025-11-03 23:57:26 +00:00 
			
		
		
		
	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:
		
							parent
							
								
									ceaed3f0e1
								
							
						
					
					
						commit
						ed116b24ba
					
				@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										56
									
								
								x/liquid/types/key_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								x/liquid/types/key_test.go
									
									
									
									
									
										Normal 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)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user