Add specific /vaults/bkava and /deposits query handler to get aggregated bkava amounts (#1293)

* Use custom aggregate handler for querying 'bkava' vault

* Add 3rd bkava vault

* Add special kava deposit handlers

* Separate bkava logic to parent deposits handler

* Rename single vault/account queries

* Remove all deposits queries

* Include empty vaults in /vaults query

* Respond with empty values when querying account deposits with no deposits

* return ukava value in bkava vault queries

* remove refernce to specific staked token denom

* return ukava value in bkava deposit queries

Co-authored-by: rhuairahrighairigh <ruaridh.odonnell@gmail.com>
This commit is contained in:
Derrick Lee 2022-09-20 11:52:40 -07:00 committed by GitHub
parent ed116b24ba
commit 9fb64b1f11
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 568 additions and 188 deletions

View File

@ -615,6 +615,7 @@ func NewApp(
earnSubspace,
app.accountKeeper,
app.bankKeeper,
app.liquidKeeper,
&hardKeeper,
&savingsKeeper,
)

View File

@ -170,7 +170,7 @@ func (suite *depositTestSuite) TestDeposit_PrivateVault() {
func (suite *depositTestSuite) TestDeposit_bKava() {
vaultDenom := "bkava"
coinDenom := vaultDenom + "-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l"
coinDenom := testutil.TestBkavaDenoms[0]
startBalance := sdk.NewInt64Coin(coinDenom, 1000)
depositAmount := sdk.NewInt64Coin(coinDenom, 100)

View File

@ -3,13 +3,12 @@ package keeper
import (
"context"
"fmt"
"strings"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/cosmos/cosmos-sdk/store/prefix"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/query"
"github.com/kava-labs/kava/x/earn/types"
)
@ -51,6 +50,14 @@ func (s queryServer) Vaults(
sdkCtx := sdk.UnwrapSDKContext(ctx)
allowedVaults := s.keeper.GetAllowedVaults(sdkCtx)
allowedVaultsMap := make(map[string]types.AllowedVault)
visitedMap := make(map[string]bool)
for _, av := range allowedVaults {
allowedVaultsMap[av.Denom] = av
visitedMap[av.Denom] = false
}
vaults := []types.VaultResponse{}
var vaultRecordsErr error
@ -58,9 +65,16 @@ func (s queryServer) Vaults(
// Iterate over vault records instead of AllowedVaults to get all bkava-*
// vaults
s.keeper.IterateVaultRecords(sdkCtx, func(record types.VaultRecord) bool {
allowedVault, found := s.keeper.GetAllowedVault(sdkCtx, record.TotalShares.Denom)
// Check if bkava, use allowed vault
allowedVaultDenom := record.TotalShares.Denom
if strings.HasPrefix(record.TotalShares.Denom, bkavaPrefix) {
allowedVaultDenom = bkavaDenom
}
allowedVault, found := allowedVaultsMap[allowedVaultDenom]
if !found {
vaultRecordsErr = fmt.Errorf("vault record not found for vault record denom %s", record.TotalShares.Denom)
return true
}
totalValue, err := s.keeper.GetVaultTotalValue(sdkCtx, record.TotalShares.Denom)
@ -79,6 +93,9 @@ func (s queryServer) Vaults(
TotalValue: totalValue.Amount,
})
// Mark this allowed vault as visited
visitedMap[allowedVaultDenom] = true
return false
})
@ -86,6 +103,30 @@ func (s queryServer) Vaults(
return nil, vaultRecordsErr
}
// Add the allowed vaults that have not been visited yet
// These are always empty vaults, as the vault would have been visited
// earlier if there are any deposits
for denom, visited := range visitedMap {
if visited {
continue
}
allowedVault, found := allowedVaultsMap[denom]
if !found {
return nil, fmt.Errorf("vault record not found for vault record denom %s", denom)
}
vaults = append(vaults, types.VaultResponse{
Denom: denom,
Strategies: allowedVault.Strategies,
IsPrivateVault: allowedVault.IsPrivateVault,
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
// No shares, no value
TotalShares: sdk.ZeroDec().String(),
TotalValue: sdk.ZeroInt(),
})
}
// Does not include vaults that have no deposits, only iterates over vault
// records which exists only for those with deposits.
return &types.QueryVaultsResponse{
@ -114,6 +155,11 @@ func (s queryServer) Vault(
return nil, status.Errorf(codes.NotFound, "vault not found with specified denom")
}
// Handle bkava separately to get total of **all** bkava vaults
if req.Denom == "bkava" {
return s.getAggregateBkavaVault(sdkCtx, allowedVault)
}
// Must be req.Denom and not allowedVault.Denom to get full "bkava" denom
vaultRecord, found := s.keeper.GetVaultRecord(sdkCtx, req.Denom)
if !found {
@ -141,6 +187,54 @@ func (s queryServer) Vault(
}, nil
}
// getAggregateBkavaVault returns a VaultResponse of the total of all bkava
// vaults.
func (s queryServer) getAggregateBkavaVault(
ctx sdk.Context,
allowedVault types.AllowedVault,
) (*types.QueryVaultResponse, error) {
allBkava := sdk.NewCoins()
var iterErr error
s.keeper.IterateVaultRecords(ctx, func(record types.VaultRecord) (stop bool) {
// Skip non bkava vaults
if !strings.HasPrefix(record.TotalShares.Denom, "bkava") {
return false
}
vaultValue, err := s.keeper.GetVaultTotalValue(ctx, record.TotalShares.Denom)
if err != nil {
iterErr = err
return false
}
allBkava = allBkava.Add(vaultValue)
return false
})
if iterErr != nil {
return nil, iterErr
}
vaultValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, allBkava)
if err != nil {
return nil, err
}
return &types.QueryVaultResponse{
Vault: types.VaultResponse{
Denom: "bkava",
Strategies: allowedVault.Strategies,
IsPrivateVault: allowedVault.IsPrivateVault,
AllowedDepositors: addressSliceToStringSlice(allowedVault.AllowedDepositors),
// Empty for shares, as adding up all shares is not useful information
TotalShares: "0",
TotalValue: vaultValue.Amount,
},
}, nil
}
// Deposits implements the gRPC service handler for querying x/earn deposits.
func (s queryServer) Deposits(
ctx context.Context,
@ -150,30 +244,29 @@ func (s queryServer) Deposits(
return nil, status.Errorf(codes.InvalidArgument, "empty request")
}
if req.Depositor == "" {
return nil, status.Errorf(codes.InvalidArgument, "depositor is required")
}
sdkCtx := sdk.UnwrapSDKContext(ctx)
// 1. Specific account and specific vault
if req.Depositor != "" && req.Denom != "" {
return s.getAccountVaultDeposit(sdkCtx, req)
// bkava aggregate total
if req.Denom == "bkava" {
return s.getOneAccountBkavaVaultDeposit(sdkCtx, req)
}
// 2. All accounts, specific vault
if req.Depositor == "" && req.Denom != "" {
return s.getVaultAllDeposits(sdkCtx, req)
// specific vault
if req.Denom != "" {
return s.getOneAccountOneVaultDeposit(sdkCtx, req)
}
// 3. Specific account, all vaults
if req.Depositor != "" && req.Denom == "" {
return s.getAccountAllDeposits(sdkCtx, req)
// all vaults
return s.getOneAccountAllDeposits(sdkCtx, req)
}
// 4. All accounts, all vaults
return s.getAllDeposits(sdkCtx, req)
}
// getAccountVaultDeposit returns deposits for a specific vault and a specific
// getOneAccountOneVaultDeposit returns deposits for a specific vault and a specific
// account
func (s queryServer) getAccountVaultDeposit(
func (s queryServer) getOneAccountOneVaultDeposit(
ctx sdk.Context,
req *types.QueryDepositsRequest,
) (*types.QueryDepositsResponse, error) {
@ -184,90 +277,95 @@ func (s queryServer) getAccountVaultDeposit(
shareRecord, found := s.keeper.GetVaultShareRecord(ctx, depositor)
if !found {
return nil, status.Error(codes.NotFound, "No deposit found for owner")
}
if shareRecord.Shares.AmountOf(req.Denom).IsZero() {
return nil, status.Error(codes.NotFound, fmt.Sprintf("No deposit for denom %s found for owner", req.Denom))
}
value, err := getAccountValue(ctx, s.keeper, depositor, shareRecord.Shares)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return &types.QueryDepositsResponse{
Deposits: []types.DepositResponse{
{
Depositor: depositor.String(),
Shares: shareRecord.Shares,
Value: value,
// Zero shares and zero value for no deposits
Shares: types.NewVaultShares(types.NewVaultShare(req.Denom, sdk.ZeroDec())),
Value: sdk.NewCoins(sdk.NewCoin(req.Denom, sdk.ZeroInt())),
},
},
Pagination: nil,
}, nil
}
// getVaultAllDeposits returns all deposits for a specific vault
func (s queryServer) getVaultAllDeposits(
// Only requesting the value of the specified denom
value, err := s.keeper.GetVaultAccountValue(ctx, req.Denom, depositor)
if err != nil {
return nil, status.Error(codes.NotFound, err.Error())
}
return &types.QueryDepositsResponse{
Deposits: []types.DepositResponse{
{
Depositor: depositor.String(),
// Only respond with requested denom shares
Shares: types.NewVaultShares(
types.NewVaultShare(req.Denom, shareRecord.Shares.AmountOf(req.Denom)),
),
Value: sdk.NewCoins(value),
},
},
Pagination: nil,
}, nil
}
// getOneAccountBkavaVaultDeposit returns deposits for the aggregated bkava vault
// and a specific account
func (s queryServer) getOneAccountBkavaVaultDeposit(
ctx sdk.Context,
req *types.QueryDepositsRequest,
) (*types.QueryDepositsResponse, error) {
_, found := s.keeper.GetVaultRecord(ctx, req.Denom)
depositor, err := sdk.AccAddressFromBech32(req.Depositor)
if err != nil {
return nil, status.Error(codes.InvalidArgument, "Invalid address")
}
shareRecord, found := s.keeper.GetVaultShareRecord(ctx, depositor)
if !found {
return nil, status.Error(codes.NotFound, "Vault record for denom not found")
}
deposits := []types.DepositResponse{}
store := prefix.NewStore(ctx.KVStore(s.keeper.key), types.VaultShareRecordKeyPrefix)
pageRes, err := query.FilteredPaginate(
store,
req.Pagination,
func(key []byte, value []byte, accumulate bool) (bool, error) {
var record types.VaultShareRecord
err := s.keeper.cdc.Unmarshal(value, &record)
if err != nil {
return false, err
}
// Only those that have amount of requested denom
if record.Shares.AmountOf(req.Denom).IsZero() {
// inform paginate that there was no match on this key
return false, nil
}
if accumulate {
accValue, err := getAccountValue(ctx, s.keeper, record.Depositor, record.Shares)
if err != nil {
return false, err
}
// only add to results if paginate tells us to
deposits = append(deposits, types.DepositResponse{
Depositor: record.Depositor.String(),
Shares: record.Shares,
Value: accValue,
})
}
// inform paginate that were was a match on this key
return true, nil
return &types.QueryDepositsResponse{
Deposits: []types.DepositResponse{
{
Depositor: depositor.String(),
// Zero shares and zero value for no deposits
Shares: types.NewVaultShares(types.NewVaultShare(req.Denom, sdk.ZeroDec())),
Value: sdk.NewCoins(sdk.NewCoin(req.Denom, sdk.ZeroInt())),
},
)
},
Pagination: nil,
}, nil
}
// Get all account deposit values to add up bkava
totalAccountValue, err := getAccountTotalValue(ctx, s.keeper, depositor, shareRecord.Shares)
if err != nil {
return nil, err
}
// Use account value with only the aggregate bkava converted to underlying staked tokens
stakedValue, err := s.keeper.liquidKeeper.GetStakedTokensForDerivatives(ctx, totalAccountValue)
if err != nil {
return nil, err
}
return &types.QueryDepositsResponse{
Deposits: deposits,
Pagination: pageRes,
Deposits: []types.DepositResponse{
{
Depositor: depositor.String(),
// Only respond with requested denom shares
Shares: types.NewVaultShares(
types.NewVaultShare(req.Denom, shareRecord.Shares.AmountOf(req.Denom)),
),
Value: sdk.NewCoins(stakedValue),
},
},
Pagination: nil,
}, nil
}
// getAccountAllDeposits returns deposits for all vaults for a specific account
func (s queryServer) getAccountAllDeposits(
// getOneAccountAllDeposits returns deposits for all vaults for a specific account
func (s queryServer) getOneAccountAllDeposits(
ctx sdk.Context,
req *types.QueryDepositsRequest,
) (*types.QueryDepositsResponse, error) {
@ -280,10 +378,13 @@ func (s queryServer) getAccountAllDeposits(
accountShare, found := s.keeper.GetVaultShareRecord(ctx, depositor)
if !found {
return nil, status.Error(codes.NotFound, "No deposit found for depositor")
return &types.QueryDepositsResponse{
Deposits: []types.DepositResponse{},
Pagination: nil,
}, nil
}
value, err := getAccountValue(ctx, s.keeper, depositor, accountShare.Shares)
value, err := getAccountTotalValue(ctx, s.keeper, depositor, accountShare.Shares)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
@ -300,51 +401,9 @@ func (s queryServer) getAccountAllDeposits(
}, nil
}
// getAllDeposits returns all deposits for all vaults
func (s queryServer) getAllDeposits(
ctx sdk.Context,
req *types.QueryDepositsRequest,
) (*types.QueryDepositsResponse, error) {
deposits := []types.DepositResponse{}
store := prefix.NewStore(ctx.KVStore(s.keeper.key), types.VaultShareRecordKeyPrefix)
pageRes, err := query.Paginate(
store,
req.Pagination,
func(key []byte, value []byte) error {
var record types.VaultShareRecord
err := s.keeper.cdc.Unmarshal(value, &record)
if err != nil {
return err
}
accValue, err := getAccountValue(ctx, s.keeper, record.Depositor, record.Shares)
if err != nil {
return err
}
// only add to results if paginate tells us to
deposits = append(deposits, types.DepositResponse{
Depositor: record.Depositor.String(),
Shares: record.Shares,
Value: accValue,
})
return nil
},
)
if err != nil {
return nil, err
}
return &types.QueryDepositsResponse{
Deposits: deposits,
Pagination: pageRes,
}, nil
}
func getAccountValue(
// getAccountTotalValue returns the total value for all vaults for a specific
// account based on their shares.
func getAccountTotalValue(
ctx sdk.Context,
keeper Keeper,
account sdk.AccAddress,

View File

@ -5,14 +5,19 @@ import (
"testing"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"
sdk "github.com/cosmos/cosmos-sdk/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/stretchr/testify/suite"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/kava-labs/kava/x/earn/keeper"
"github.com/kava-labs/kava/x/earn/testutil"
"github.com/kava-labs/kava/x/earn/types"
"github.com/stretchr/testify/suite"
liquidtypes "github.com/kava-labs/kava/x/liquid/types"
)
type grpcQueryTestSuite struct {
@ -81,13 +86,32 @@ func (suite *grpcQueryTestSuite) TestVaults_ZeroSupply() {
suite.Run("all", func() {
res, err := suite.queryClient.Vaults(context.Background(), types.NewQueryVaultsRequest())
suite.Require().NoError(err)
suite.Require().Empty(res.Vaults)
suite.Require().ElementsMatch([]types.VaultResponse{
{
Denom: "usdx",
Strategies: []types.StrategyType{types.STRATEGY_TYPE_HARD},
IsPrivateVault: false,
AllowedDepositors: nil,
TotalShares: sdk.ZeroDec().String(),
TotalValue: sdk.ZeroInt(),
},
{
Denom: "busd",
Strategies: []types.StrategyType{types.STRATEGY_TYPE_HARD},
IsPrivateVault: false,
AllowedDepositors: nil,
TotalShares: sdk.ZeroDec().String(),
TotalValue: sdk.ZeroInt(),
},
},
res.Vaults,
)
})
}
func (suite *grpcQueryTestSuite) TestVaults_WithSupply() {
vaultDenom := "usdx"
vault2Denom := "bkava-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l"
vault2Denom := testutil.TestBkavaDenoms[0]
depositAmount := sdk.NewInt64Coin(vaultDenom, 100)
deposit2Amount := sdk.NewInt64Coin(vault2Denom, 100)
@ -132,6 +156,60 @@ func (suite *grpcQueryTestSuite) TestVaults_WithSupply() {
)
}
func (suite *grpcQueryTestSuite) TestVaults_MixedSupply() {
vaultDenom := "usdx"
vault2Denom := "busd"
vault3Denom := testutil.TestBkavaDenoms[0]
depositAmount := sdk.NewInt64Coin(vault3Denom, 100)
suite.CreateVault(vaultDenom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil)
suite.CreateVault(vault2Denom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil)
suite.CreateVault("bkava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil)
acc := suite.CreateAccount(sdk.NewCoins(
sdk.NewInt64Coin(vaultDenom, 1000),
sdk.NewInt64Coin(vault2Denom, 1000),
sdk.NewInt64Coin(vault3Denom, 1000),
), 0)
err := suite.Keeper.Deposit(suite.Ctx, acc.GetAddress(), depositAmount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
res, err := suite.queryClient.Vaults(context.Background(), types.NewQueryVaultsRequest())
suite.Require().NoError(err)
suite.Require().Len(res.Vaults, 3)
suite.Require().ElementsMatch(
[]types.VaultResponse{
{
Denom: vaultDenom,
Strategies: []types.StrategyType{types.STRATEGY_TYPE_HARD},
IsPrivateVault: false,
AllowedDepositors: nil,
TotalShares: sdk.ZeroDec().String(),
TotalValue: sdk.ZeroInt(),
},
{
Denom: vault2Denom,
Strategies: []types.StrategyType{types.STRATEGY_TYPE_HARD},
IsPrivateVault: false,
AllowedDepositors: nil,
TotalShares: sdk.ZeroDec().String(),
TotalValue: sdk.ZeroInt(),
},
{
Denom: vault3Denom,
Strategies: []types.StrategyType{types.STRATEGY_TYPE_SAVINGS},
IsPrivateVault: false,
AllowedDepositors: nil,
TotalShares: depositAmount.Amount.ToDec().String(),
TotalValue: depositAmount.Amount,
},
},
res.Vaults,
)
}
func (suite *grpcQueryTestSuite) TestVault_NotFound() {
_, err := suite.queryClient.Vault(context.Background(), types.NewQueryVaultRequest("usdx"))
suite.Require().Error(err)
@ -141,7 +219,7 @@ func (suite *grpcQueryTestSuite) TestVault_NotFound() {
func (suite *grpcQueryTestSuite) TestDeposits() {
vault1Denom := "usdx"
vault2Denom := "busd"
vault3Denom := "bkava-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l"
vault3Denom := testutil.TestBkavaDenoms[0]
// Add vaults
suite.CreateVault(vault1Denom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil)
@ -153,6 +231,7 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
sdk.NewInt64Coin(vault2Denom, 1000),
sdk.NewInt64Coin(vault3Denom, 1000),
)
deposit1Amount := sdk.NewInt64Coin(vault1Denom, 100)
deposit2Amount := sdk.NewInt64Coin(vault2Denom, 200)
deposit3Amount := sdk.NewInt64Coin(vault3Denom, 200)
@ -163,7 +242,7 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
// Deposit into each vault from each account - 4 total deposits
// Acc 1: usdx + busd
// Acc 2: usdx + usdc
// Acc 2: usdx + bkava
err := suite.Keeper.Deposit(suite.Ctx, acc1, deposit1Amount, types.STRATEGY_TYPE_HARD)
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, acc1, deposit2Amount, types.STRATEGY_TYPE_HARD)
@ -174,7 +253,7 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
err = suite.Keeper.Deposit(suite.Ctx, acc2, deposit3Amount, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
suite.Run("1) 1 vault for 1 account", func() {
suite.Run("specific vault", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
@ -186,12 +265,12 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
[]types.DepositResponse{
{
Depositor: acc1.String(),
// Still includes all deposits
// Only includes specified deposit shares
Shares: types.NewVaultShares(
types.NewVaultShare(deposit1Amount.Denom, deposit1Amount.Amount.ToDec()),
types.NewVaultShare(deposit2Amount.Denom, deposit2Amount.Amount.ToDec()),
),
Value: sdk.NewCoins(deposit1Amount, deposit2Amount),
// Only the specified vault denom value
Value: sdk.NewCoins(deposit1Amount),
},
},
res.Deposits,
@ -200,16 +279,41 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
)
})
suite.Run("1) invalid vault for 1 account", func() {
suite.Run("specific bkava vault", func() {
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest(acc2.String(), vault3Denom, nil),
)
suite.Require().NoError(err)
suite.Require().Len(res.Deposits, 1)
suite.Require().ElementsMatchf(
[]types.DepositResponse{
{
Depositor: acc2.String(),
// Only includes specified deposit shares
Shares: types.NewVaultShares(
types.NewVaultShare(deposit3Amount.Denom, deposit3Amount.Amount.ToDec()),
),
// Only the specified vault denom value
Value: sdk.NewCoins(deposit3Amount),
},
},
res.Deposits,
"deposits should match, got %v",
res.Deposits,
)
})
suite.Run("invalid vault", func() {
_, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest(acc1.String(), "notavaliddenom", nil),
)
suite.Require().Error(err)
suite.Require().ErrorIs(err, status.Errorf(codes.NotFound, "No deposit for denom notavaliddenom found for owner"))
suite.Require().ErrorIs(err, status.Errorf(codes.NotFound, "vault for notavaliddenom not found"))
})
suite.Run("3) all vaults for 1 account", func() {
suite.Run("all vaults", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
@ -231,55 +335,35 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
res.Deposits,
)
})
}
suite.Run("2) all accounts, specific vault", func() {
// Query all deposits for vault 3
func (suite *grpcQueryTestSuite) TestDeposits_NoDeposits() {
vault1Denom := "usdx"
vault2Denom := "busd"
// Add vaults
suite.CreateVault(vault1Denom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil)
suite.CreateVault(vault2Denom, types.StrategyTypes{types.STRATEGY_TYPE_HARD}, false, nil)
suite.CreateVault("bkava", types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS}, false, nil)
// Accounts
acc1 := suite.CreateAccount(sdk.NewCoins(), 0).GetAddress()
suite.Run("specific vault", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest("", vault3Denom, nil),
types.NewQueryDepositsRequest(acc1.String(), vault1Denom, nil),
)
suite.Require().NoError(err)
suite.Require().Len(res.Deposits, 1)
suite.Require().ElementsMatch(
[]types.DepositResponse{
{
Depositor: acc2.String(),
Shares: types.NewVaultShares(
types.NewVaultShare(deposit1Amount.Denom, deposit1Amount.Amount.ToDec()),
types.NewVaultShare(deposit3Amount.Denom, deposit3Amount.Amount.ToDec()),
),
Value: sdk.NewCoins(deposit1Amount, deposit3Amount),
},
},
res.Deposits,
)
})
suite.Run("4) all vaults and all accounts", func() {
// Query all deposits for all vaults
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest("", "", nil),
)
suite.Require().NoError(err)
suite.Require().Len(res.Deposits, 2)
suite.Require().ElementsMatchf(
[]types.DepositResponse{
{
Depositor: acc1.String(),
Shares: types.NewVaultShares(
types.NewVaultShare(deposit1Amount.Denom, deposit1Amount.Amount.ToDec()),
types.NewVaultShare(deposit2Amount.Denom, deposit2Amount.Amount.ToDec()),
),
Value: sdk.NewCoins(deposit1Amount, deposit2Amount),
},
{
Depositor: acc2.String(),
Shares: types.NewVaultShares(
types.NewVaultShare(deposit1Amount.Denom, deposit1Amount.Amount.ToDec()),
types.NewVaultShare(deposit3Amount.Denom, deposit3Amount.Amount.ToDec()),
),
Value: sdk.NewCoins(deposit1Amount, deposit3Amount),
// Zero shares and zero value
Shares: nil,
Value: nil,
},
},
res.Deposits,
@ -287,15 +371,25 @@ func (suite *grpcQueryTestSuite) TestDeposits() {
res.Deposits,
)
})
suite.Run("all vaults", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest(acc1.String(), "", nil),
)
suite.Require().NoError(err)
suite.Require().Empty(res.Deposits)
})
}
func (suite *grpcQueryTestSuite) TestDeposits_NotFound() {
func (suite *grpcQueryTestSuite) TestDeposits_NoDepositor() {
_, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest("", "usdx", nil),
)
suite.Require().Error(err)
suite.Require().ErrorIs(err, status.Error(codes.NotFound, "Vault record for denom not found"))
suite.Require().ErrorIs(err, status.Error(codes.InvalidArgument, "depositor is required"))
}
func (suite *grpcQueryTestSuite) TestDeposits_InvalidAddress() {
@ -314,9 +408,89 @@ func (suite *grpcQueryTestSuite) TestDeposits_InvalidAddress() {
suite.Require().ErrorIs(err, status.Error(codes.InvalidArgument, "Invalid address"))
}
func (suite *grpcQueryTestSuite) TestVault_bKava() {
func (suite *grpcQueryTestSuite) TestDeposits_bKava() {
// vault denom is only "bkava" which has it's own special handler
suite.CreateVault(
"bkava",
types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS},
false,
[]sdk.AccAddress{},
)
address1, derivatives1, _ := suite.createAccountWithDerivatives(testutil.TestBkavaDenoms[0], sdk.NewInt(1e9))
address2, derivatives2, _ := suite.createAccountWithDerivatives(testutil.TestBkavaDenoms[1], sdk.NewInt(1e9))
// Slash the last validator to reduce the value of it's derivatives to test bkava to underlying token conversion.
// First call end block to bond validator to enable slashing.
staking.EndBlocker(suite.Ctx, suite.App.GetStakingKeeper())
err := suite.slashValidator(sdk.ValAddress(address2), sdk.MustNewDecFromStr("0.5"))
suite.Require().NoError(err)
suite.Run("no deposits", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest(address1.String(), "bkava", nil),
)
suite.Require().NoError(err)
suite.Require().Len(res.Deposits, 1)
suite.Require().ElementsMatchf(
[]types.DepositResponse{
{
Depositor: address1.String(),
// Zero shares for "bkava" aggregate
Shares: nil,
// Only the specified vault denom value
Value: nil,
},
},
res.Deposits,
"deposits should match, got %v",
res.Deposits,
)
})
err = suite.Keeper.Deposit(suite.Ctx, address1, derivatives1, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
err = suite.BankKeeper.SendCoins(suite.Ctx, address2, address1, sdk.NewCoins(derivatives2))
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, address1, derivatives2, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
suite.Run("multiple deposits", func() {
// Query all deposits for account 1
res, err := suite.queryClient.Deposits(
context.Background(),
types.NewQueryDepositsRequest(address1.String(), "bkava", nil),
)
suite.Require().NoError(err)
suite.Require().Len(res.Deposits, 1)
// first validator isn't slashed, so bkava units equal to underlying staked tokens
// last validator slashed 50% so derivatives are worth half
expectedValue := derivatives1.Amount.Add(derivatives2.Amount.QuoRaw(2))
suite.Require().ElementsMatchf(
[]types.DepositResponse{
{
Depositor: address1.String(),
// Zero shares for "bkava" aggregate
Shares: nil,
// Value returned in units of staked token
Value: sdk.NewCoins(
sdk.NewCoin(suite.bondDenom(), expectedValue),
),
},
},
res.Deposits,
"deposits should match, got %v",
res.Deposits,
)
})
}
func (suite *grpcQueryTestSuite) TestVault_bKava_Single() {
vaultDenom := "bkava"
coinDenom := vaultDenom + "-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l"
coinDenom := testutil.TestBkavaDenoms[0]
startBalance := sdk.NewInt64Coin(coinDenom, 1000)
depositAmount := sdk.NewInt64Coin(coinDenom, 100)
@ -356,3 +530,132 @@ func (suite *grpcQueryTestSuite) TestVault_bKava() {
res.Vault,
)
}
func (suite *grpcQueryTestSuite) TestVault_bKava_Aggregate() {
vaultDenom := "bkava"
address1, derivatives1, _ := suite.createAccountWithDerivatives(testutil.TestBkavaDenoms[0], sdk.NewInt(1e9))
address2, derivatives2, _ := suite.createAccountWithDerivatives(testutil.TestBkavaDenoms[1], sdk.NewInt(1e9))
address3, derivatives3, _ := suite.createAccountWithDerivatives(testutil.TestBkavaDenoms[2], sdk.NewInt(1e9))
// Slash the last validator to reduce the value of it's derivatives to test bkava to underlying token conversion.
// First call end block to bond validator to enable slashing.
staking.EndBlocker(suite.Ctx, suite.App.GetStakingKeeper())
err := suite.slashValidator(sdk.ValAddress(address3), sdk.MustNewDecFromStr("0.5"))
suite.Require().NoError(err)
// vault denom is only "bkava" which has it's own special handler
suite.CreateVault(
vaultDenom,
types.StrategyTypes{types.STRATEGY_TYPE_SAVINGS},
false,
[]sdk.AccAddress{},
)
err = suite.Keeper.Deposit(suite.Ctx, address1, derivatives1, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, address2, derivatives2, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
err = suite.Keeper.Deposit(suite.Ctx, address3, derivatives3, types.STRATEGY_TYPE_SAVINGS)
suite.Require().NoError(err)
// Query "bkava" to get aggregate amount
res, err := suite.queryClient.Vault(
context.Background(),
types.NewQueryVaultRequest(vaultDenom),
)
suite.Require().NoError(err)
// first two validators are not slashed, so bkava units equal to underlying staked tokens
expectedValue := derivatives1.Amount.Add(derivatives2.Amount)
// last validator slashed 50% so derivatives are worth half
expectedValue = expectedValue.Add(derivatives2.Amount.QuoRaw(2))
suite.Require().Equal(
types.VaultResponse{
Denom: vaultDenom,
Strategies: types.StrategyTypes{
types.STRATEGY_TYPE_SAVINGS,
},
IsPrivateVault: false,
AllowedDepositors: []string(nil),
// No shares for aggregate
TotalShares: "0",
TotalValue: expectedValue,
},
res.Vault,
)
}
// createUnbondedValidator creates an unbonded validator with the given amount of self-delegation.
func (suite *grpcQueryTestSuite) createUnbondedValidator(address sdk.ValAddress, selfDelegation sdk.Coin, minSelfDelegation sdk.Int) error {
msg, err := stakingtypes.NewMsgCreateValidator(
address,
ed25519.GenPrivKey().PubKey(),
selfDelegation,
stakingtypes.Description{},
stakingtypes.NewCommissionRates(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()),
minSelfDelegation,
)
if err != nil {
return err
}
msgServer := stakingkeeper.NewMsgServerImpl(suite.App.GetStakingKeeper())
_, err = msgServer.CreateValidator(sdk.WrapSDKContext(suite.Ctx), msg)
return err
}
// createAccountWithDerivatives creates an account with the given amount and denom of derivative token.
// Internally, it creates a validator account and mints derivatives from the validator's self delegation.
func (suite *grpcQueryTestSuite) createAccountWithDerivatives(denom string, amount sdk.Int) (sdk.AccAddress, sdk.Coin, sdk.Coins) {
valAddress, err := liquidtypes.ParseLiquidStakingTokenDenom(denom)
suite.Require().NoError(err)
address := sdk.AccAddress(valAddress)
remainingSelfDelegation := sdk.NewInt(1e6)
selfDelegation := sdk.NewCoin(
suite.bondDenom(),
amount.Add(remainingSelfDelegation),
)
suite.NewAccountFromAddr(address, sdk.NewCoins(selfDelegation))
err = suite.createUnbondedValidator(valAddress, selfDelegation, remainingSelfDelegation)
suite.Require().NoError(err)
toConvert := sdk.NewCoin(suite.bondDenom(), amount)
derivatives, err := suite.App.GetLiquidKeeper().MintDerivative(suite.Ctx,
address,
valAddress,
toConvert,
)
suite.Require().NoError(err)
fullBalance := suite.BankKeeper.GetAllBalances(suite.Ctx, address)
return address, derivatives, fullBalance
}
// slashValidator slashes the validator with the given address by the given percentage.
func (suite *grpcQueryTestSuite) slashValidator(address sdk.ValAddress, slashFraction sdk.Dec) error {
stakingKeeper := suite.App.GetStakingKeeper()
validator, found := stakingKeeper.GetValidator(suite.Ctx, address)
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 := stakingKeeper.TokensToConsensusPower(suite.Ctx, validator.GetTokens())
stakingKeeper.Slash(suite.Ctx, consAddr, infractionHeight, power, slashFraction)
return nil
}
// bondDenom fetches the staking denom from the staking module.
func (suite *grpcQueryTestSuite) bondDenom() string {
return suite.App.GetStakingKeeper().BondDenom(suite.Ctx)
}

View File

@ -16,6 +16,7 @@ type Keeper struct {
hooks types.EarnHooks
accountKeeper types.AccountKeeper
bankKeeper types.BankKeeper
liquidKeeper types.LiquidKeeper
// Keepers used for strategies
hardKeeper types.HardKeeper
@ -29,6 +30,7 @@ func NewKeeper(
paramstore paramtypes.Subspace,
accountKeeper types.AccountKeeper,
bankKeeper types.BankKeeper,
liquidKeeper types.LiquidKeeper,
hardKeeper types.HardKeeper,
savingsKeeper types.SavingsKeeper,
) Keeper {
@ -42,6 +44,7 @@ func NewKeeper(
paramSubspace: paramstore,
accountKeeper: accountKeeper,
bankKeeper: bankKeeper,
liquidKeeper: liquidKeeper,
hardKeeper: hardKeeper,
savingsKeeper: savingsKeeper,
}

View File

@ -244,7 +244,7 @@ func (suite *withdrawTestSuite) TestWithdraw_Partial() {
func (suite *withdrawTestSuite) TestWithdraw_bKava() {
vaultDenom := "bkava"
coinDenom := vaultDenom + "-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l"
coinDenom := testutil.TestBkavaDenoms[0]
startBalance := sdk.NewInt64Coin(coinDenom, 1000)
depositAmount := sdk.NewInt64Coin(coinDenom, 100)

View File

@ -28,6 +28,12 @@ import (
tmtime "github.com/tendermint/tendermint/types/time"
)
var TestBkavaDenoms = []string{
"bkava-kavavaloper15gqc744d05xacn4n6w2furuads9fu4pqn6zxlu",
"bkava-kavavaloper15qdefkmwswysgg4qxgqpqr35k3m49pkx8yhpte",
"bkava-kavavaloper1ypjp0m04pyp73hwgtc0dgkx0e9rrydeckewa42",
}
// Suite implements a test suite for the earn module integration tests
type Suite struct {
suite.Suite
@ -146,7 +152,9 @@ func (suite *Suite) SetupTest() {
savingstypes.NewParams(
[]string{
"ukava",
"bkava-kavavaloper16xyempempp92x9hyzz9wrgf94r6j9h5f2w4n2l",
TestBkavaDenoms[0],
TestBkavaDenoms[1],
TestBkavaDenoms[2],
},
),
nil,

View File

@ -25,6 +25,11 @@ type BankKeeper interface {
SendCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error
}
// LiquidKeeper defines the expected interface needed for derivative to staked token conversions.
type LiquidKeeper interface {
GetStakedTokensForDerivatives(ctx sdk.Context, derivatives sdk.Coins) (sdk.Coin, error)
}
// HardKeeper defines the expected interface needed for the hard strategy.
type HardKeeper interface {
Deposit(ctx sdk.Context, depositor sdk.AccAddress, coins sdk.Coins) error

View File

@ -111,28 +111,29 @@ func (k Keeper) IsDerivativeDenom(ctx sdk.Context, denom string) bool {
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()
// GetStakedTokensForDerivatives returns the total value of the provided derivatives
// in staked tokens, accounting for the specific share prices.
func (k Keeper) GetStakedTokensForDerivatives(ctx sdk.Context, coins sdk.Coins) (sdk.Coin, error) {
total := 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)
return sdk.Coin{}, 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)
return sdk.Coin{}, 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())
total = total.Add(valTokens.TruncateInt())
}
return totalKava, nil
totalCoin := sdk.NewCoin(k.stakingKeeper.BondDenom(ctx), total)
return totalCoin, nil
}
func (k Keeper) mintCoins(ctx sdk.Context, receiver sdk.AccAddress, amount sdk.Coins) error {

View File

@ -379,7 +379,7 @@ func (suite *KeeperTestSuite) TestIsDerivativeDenom() {
}
}
func (suite *KeeperTestSuite) TestGetKavaForDerivatives() {
func (suite *KeeperTestSuite) TestGetStakedTokensForDerivatives() {
_, addrs := app.GeneratePrivKeyAddressPairs(5)
valAccAddr1, delegator, valAccAddr2, valAccAddr3 := addrs[0], addrs[1], addrs[2], addrs[3]
valAddr1 := sdk.ValAddress(valAccAddr1)
@ -458,13 +458,13 @@ func (suite *KeeperTestSuite) TestGetKavaForDerivatives() {
for _, tc := range testCases {
suite.Run(tc.name, func() {
kavaAmount, err := suite.Keeper.GetKavaForDerivatives(suite.Ctx, tc.derivatives)
kavaAmount, err := suite.Keeper.GetStakedTokensForDerivatives(suite.Ctx, tc.derivatives)
if tc.err != nil {
suite.Require().Error(err)
} else {
suite.Require().NoError(err)
suite.Require().Equal(tc.wantKavaAmount, kavaAmount)
suite.Require().Equal(suite.NewBondCoin(tc.wantKavaAmount), kavaAmount)
}
})
}