Skip to content

Commit

Permalink
feat(community): add AnnualizedRewards grpc query (#1751)
Browse files Browse the repository at this point in the history
* add annualized_reward query proto

* use sdkmath.LegacyDec to match RPS param...

* add AnnualizedRewards grpc query

* add changelog entry

* simplify calculation & expand test cases
  • Loading branch information
pirtleshell committed Oct 24, 2023
1 parent 1d36429 commit 0efe7f2
Show file tree
Hide file tree
Showing 13 changed files with 931 additions and 38 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
- (community) [#1704] Add module params
- (community) [#1706] Add disable inflation upgrade
- (community) [#1745] Enable params update via governance with `MsgUpdateParams`
- (community) [#1751] Add `AnnualizedRewards` query endpoint

### Bug Fixes

Expand Down Expand Up @@ -296,8 +297,9 @@ the [changelog](https://github.com/cosmos/cosmos-sdk/blob/v0.38.4/CHANGELOG.md).
large-scale simulations remotely using aws-batch

[#1752]: https://github.com/Kava-Labs/kava/pull/1752
[#1729]: https://github.com/Kava-Labs/kava/pull/1729
[#1751]: https://github.com/Kava-Labs/kava/pull/1751
[#1745]: https://github.com/Kava-Labs/kava/pull/1745
[#1729]: https://github.com/Kava-Labs/kava/pull/1729
[#1707]: https://github.com/Kava-Labs/kava/pull/1707
[#1706]: https://github.com/Kava-Labs/kava/pull/1706
[#1704]: https://github.com/Kava-Labs/kava/pull/1704
Expand Down
57 changes: 57 additions & 0 deletions client/docs/swagger-ui/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12991,6 +12991,52 @@ paths:
format: byte
tags:
- Savings
/kava/community/v1beta1/annualized_rewards:
get:
summary: >-
AnnualizedRewards calculates and returns the current annualized reward
percentages,

like staking rewards, for the chain.
operationId: CommunityAnnualizedRewards
responses:
'200':
description: A successful response.
schema:
type: object
properties:
staking_rewards:
type: string
title: >-
staking_rewards is the calculated annualized staking rewards
percentage rate
description: >-
QueryAnnualizedRewardsResponse defines the response type for
querying the annualized rewards.
default:
description: An unexpected error response.
schema:
type: object
properties:
error:
type: string
code:
type: integer
format: int32
message:
type: string
details:
type: array
items:
type: object
properties:
type_url:
type: string
value:
type: string
format: byte
tags:
- Community
/kava/community/v1beta1/balance:
get:
summary: Balance queries the balance of all coins of x/community module.
Expand Down Expand Up @@ -57043,6 +57089,17 @@ definitions:

and use when the disable inflation time is reached
description: Params defines the parameters of the community module.
kava.community.v1beta1.QueryAnnualizedRewardsResponse:
type: object
properties:
staking_rewards:
type: string
title: >-
staking_rewards is the calculated annualized staking rewards
percentage rate
description: >-
QueryAnnualizedRewardsResponse defines the response type for querying the
annualized rewards.
kava.community.v1beta1.QueryBalanceResponse:
type: object
properties:
Expand Down
28 changes: 28 additions & 0 deletions docs/core/proto-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@
- [CommunityPoolLendWithdrawProposal](#kava.community.v1beta1.CommunityPoolLendWithdrawProposal)

- [kava/community/v1beta1/query.proto](#kava/community/v1beta1/query.proto)
- [QueryAnnualizedRewardsRequest](#kava.community.v1beta1.QueryAnnualizedRewardsRequest)
- [QueryAnnualizedRewardsResponse](#kava.community.v1beta1.QueryAnnualizedRewardsResponse)
- [QueryBalanceRequest](#kava.community.v1beta1.QueryBalanceRequest)
- [QueryBalanceResponse](#kava.community.v1beta1.QueryBalanceResponse)
- [QueryParamsRequest](#kava.community.v1beta1.QueryParamsRequest)
Expand Down Expand Up @@ -3087,6 +3089,31 @@ CommunityPoolLendWithdrawProposal withdraws a lend position back to the communit



<a name="kava.community.v1beta1.QueryAnnualizedRewardsRequest"></a>

### QueryAnnualizedRewardsRequest
QueryAnnualizedRewardsRequest defines the request type for querying the annualized rewards.






<a name="kava.community.v1beta1.QueryAnnualizedRewardsResponse"></a>

### QueryAnnualizedRewardsResponse
QueryAnnualizedRewardsResponse defines the response type for querying the annualized rewards.


| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| `staking_rewards` | [string](#string) | | staking_rewards is the calculated annualized staking rewards percentage rate |






<a name="kava.community.v1beta1.QueryBalanceRequest"></a>

### QueryBalanceRequest
Expand Down Expand Up @@ -3179,6 +3206,7 @@ Query defines the gRPC querier service for x/community.
| `Params` | [QueryParamsRequest](#kava.community.v1beta1.QueryParamsRequest) | [QueryParamsResponse](#kava.community.v1beta1.QueryParamsResponse) | Params queires the module params. | GET|/kava/community/v1beta1/params|
| `Balance` | [QueryBalanceRequest](#kava.community.v1beta1.QueryBalanceRequest) | [QueryBalanceResponse](#kava.community.v1beta1.QueryBalanceResponse) | Balance queries the balance of all coins of x/community module. | GET|/kava/community/v1beta1/balance|
| `TotalBalance` | [QueryTotalBalanceRequest](#kava.community.v1beta1.QueryTotalBalanceRequest) | [QueryTotalBalanceResponse](#kava.community.v1beta1.QueryTotalBalanceResponse) | TotalBalance queries the balance of all coins, including x/distribution, x/community, and supplied balances. | GET|/kava/community/v1beta1/total_balance|
| `AnnualizedRewards` | [QueryAnnualizedRewardsRequest](#kava.community.v1beta1.QueryAnnualizedRewardsRequest) | [QueryAnnualizedRewardsResponse](#kava.community.v1beta1.QueryAnnualizedRewardsResponse) | AnnualizedRewards calculates and returns the current annualized reward percentages, like staking rewards, for the chain. | GET|/kava/community/v1beta1/annualized_rewards|

<!-- end services -->

Expand Down
20 changes: 20 additions & 0 deletions proto/kava/community/v1beta1/query.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ syntax = "proto3";
package kava.community.v1beta1;

import "cosmos/base/v1beta1/coin.proto";
import "cosmos_proto/cosmos.proto";
import "gogoproto/gogo.proto";
import "google/api/annotations.proto";
import "kava/community/v1beta1/params.proto";
Expand All @@ -25,6 +26,12 @@ service Query {
rpc TotalBalance(QueryTotalBalanceRequest) returns (QueryTotalBalanceResponse) {
option (google.api.http).get = "/kava/community/v1beta1/total_balance";
}

// AnnualizedRewards calculates and returns the current annualized reward percentages,
// like staking rewards, for the chain.
rpc AnnualizedRewards(QueryAnnualizedRewardsRequest) returns (QueryAnnualizedRewardsResponse) {
option (google.api.http).get = "/kava/community/v1beta1/annualized_rewards";
}
}

// QueryParams defines the request type for querying x/community params.
Expand Down Expand Up @@ -59,3 +66,16 @@ message QueryTotalBalanceResponse {
(gogoproto.nullable) = false
];
}

// QueryAnnualizedRewardsRequest defines the request type for querying the annualized rewards.
message QueryAnnualizedRewardsRequest {}

// QueryAnnualizedRewardsResponse defines the response type for querying the annualized rewards.
message QueryAnnualizedRewardsResponse {
// staking_rewards is the calculated annualized staking rewards percentage rate
string staking_rewards = 1 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}
35 changes: 35 additions & 0 deletions x/community/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"

sdkmath "cosmossdk.io/math"
sdk "github.com/cosmos/cosmos-sdk/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
Expand Down Expand Up @@ -62,3 +63,37 @@ func (s queryServer) TotalBalance(
Pool: totalBalance,
}, nil
}

// AnnualizedRewards calculates the annualized rewards for the chain.
func (s queryServer) AnnualizedRewards(
c context.Context,
req *types.QueryAnnualizedRewardsRequest,
) (*types.QueryAnnualizedRewardsResponse, error) {
ctx := sdk.UnwrapSDKContext(c)

// staking rewards come from one of two sources depending on if inflation is enabled or not.
// at any given time, only one source will contribute to the staking rewards. the other will be zero.
// this method adds both sources together so it is accurate in both cases.

params := s.keeper.mustGetParams(ctx)
bondDenom := s.keeper.stakingKeeper.BondDenom(ctx)

totalSupply := s.keeper.bankKeeper.GetSupply(ctx, bondDenom).Amount
totalBonded := s.keeper.stakingKeeper.TotalBondedTokens(ctx)
rewardsPerSecond := params.StakingRewardsPerSecond
// need to convert these from sdk.Dec to sdkmath.LegacyDec
inflationRate := convertDecToLegacyDec(s.keeper.mintKeeper.GetMinter(ctx).Inflation)
communityTax := convertDecToLegacyDec(s.keeper.distrKeeper.GetCommunityTax(ctx))

return &types.QueryAnnualizedRewardsResponse{
StakingRewards: CalculateStakingAnnualPercentage(totalSupply, totalBonded, inflationRate, communityTax, rewardsPerSecond),
}, nil
}

// convertDecToLegacyDec is a helper method for converting between new and old Dec types
// current version of cosmos-sdk in this repo uses sdk.Dec
// this module uses sdkmath.LegacyDec in its parameters
// TODO: remove me after upgrade to cosmos-sdk v50 (LegacyDec is everywhere)
func convertDecToLegacyDec(in sdk.Dec) sdkmath.LegacyDec {
return sdkmath.LegacyNewDecFromBigIntWithPrec(in.BigInt(), sdk.Precision)
}
131 changes: 129 additions & 2 deletions x/community/keeper/grpc_query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,20 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/suite"

"github.com/kava-labs/kava/app"
"github.com/kava-labs/kava/x/community/keeper"
"github.com/kava-labs/kava/x/community/testutil"
"github.com/kava-labs/kava/x/community/types"
)

type grpcQueryTestSuite struct {
KeeperTestSuite
testutil.Suite

queryClient types.QueryClient
}

func (suite *grpcQueryTestSuite) SetupTest() {
suite.KeeperTestSuite.SetupTest()
suite.Suite.SetupTest()

queryHelper := baseapp.NewQueryServerTestHelper(suite.Ctx, suite.App.InterfaceRegistry())
types.RegisterQueryServer(queryHelper, keeper.NewQueryServerImpl(suite.Keeper))
Expand Down Expand Up @@ -163,3 +165,128 @@ func (suite *grpcQueryTestSuite) TestGrpcQueryTotalBalance() {
})
}
}

// NOTE: this test makes use of the fact that there is always an initial 1e6 bonded tokens
// To adjust the bonded ratio, it adjusts the total supply by minting tokens.
func (suite *grpcQueryTestSuite) TestGrpcQueryAnnualizedRewards() {
bondedTokens := sdkmath.NewInt(1e6)
testCases := []struct {
name string
bondedRatio sdk.Dec
inflation sdk.Dec
rewardsPerSec sdkmath.LegacyDec
communityTax sdk.Dec
expectedRate sdkmath.LegacyDec
}{
{
name: "sanity check: no inflation, no rewards => 0%",
bondedRatio: sdk.MustNewDecFromStr("0.3456"),
inflation: sdk.ZeroDec(),
rewardsPerSec: sdkmath.LegacyZeroDec(),
expectedRate: sdkmath.LegacyZeroDec(),
},
{
name: "inflation sanity check: 100% inflation, 100% bonded => 100%",
bondedRatio: sdk.OneDec(),
inflation: sdk.OneDec(),
rewardsPerSec: sdkmath.LegacyZeroDec(),
expectedRate: sdkmath.LegacyOneDec(),
},
{
name: "inflation sanity check: 100% community tax => 0%",
bondedRatio: sdk.OneDec(),
inflation: sdk.OneDec(),
communityTax: sdk.OneDec(),
rewardsPerSec: sdkmath.LegacyZeroDec(),
expectedRate: sdkmath.LegacyZeroDec(),
},
{
name: "rewards per second sanity check: (totalBonded/SecondsPerYear) rps => 100%",
bondedRatio: sdk.OneDec(), // bonded tokens are constant in this test. ratio has no affect.
inflation: sdk.ZeroDec(),
rewardsPerSec: sdkmath.LegacyNewDecFromInt(bondedTokens).QuoInt(sdkmath.NewInt(keeper.SecondsPerYear)),
// expect ~100%
expectedRate: sdkmath.LegacyMustNewDecFromStr("0.999999999999999984"),
},
{
name: "inflation enabled: realistic example",
bondedRatio: sdk.MustNewDecFromStr("0.148"),
inflation: sdk.MustNewDecFromStr("0.595"),
communityTax: sdk.MustNewDecFromStr("0.9495"),
rewardsPerSec: sdkmath.LegacyZeroDec(),
// expect ~20.23%
expectedRate: sdkmath.LegacyMustNewDecFromStr("0.203023625910000000"),
},
{
name: "inflation disabled: simple example",
bondedRatio: sdk.OneDec(), // bonded tokens are constant in this test. ratio has no affect.
inflation: sdk.ZeroDec(),
rewardsPerSec: sdkmath.LegacyMustNewDecFromStr("0.01"),
// 1e6 bonded tokens => seconds per year / bonded tokens = 31.536
// expect 31.536%
expectedRate: sdkmath.LegacyMustNewDecFromStr("0.31536"),
},
}

for _, tc := range testCases {
suite.Run(tc.name, func() {
suite.SetupTest()

// set inflation
mk := suite.App.GetMintKeeper()
minter := mk.GetMinter(suite.Ctx)
minter.Inflation = tc.inflation
mk.SetMinter(suite.Ctx, minter)

// set community tax
communityTax := sdk.ZeroDec()
if !tc.communityTax.IsNil() {
communityTax = tc.communityTax
}
dk := suite.App.GetDistrKeeper()
distParams := dk.GetParams(suite.Ctx)
distParams.CommunityTax = communityTax
dk.SetParams(suite.Ctx, distParams)

// set staking rewards per second
ck := suite.App.GetCommunityKeeper()
commParams, _ := ck.GetParams(suite.Ctx)
commParams.StakingRewardsPerSecond = tc.rewardsPerSec
ck.SetParams(suite.Ctx, commParams)

// set bonded tokens
suite.adjustBondedRatio(tc.bondedRatio)

// query for annualized rewards
res, err := suite.queryClient.AnnualizedRewards(suite.Ctx, &types.QueryAnnualizedRewardsRequest{})
// verify results match expected
suite.Require().NoError(err)
suite.Equal(tc.expectedRate, res.StakingRewards)
})
}
}

// adjustBondRatio changes the ratio of bonded coins
// it leverages the fact that there is a constant number of bonded tokens
// and adjusts the total supply to make change the bonded ratio.
// returns the new total supply of the bond denom
func (suite *grpcQueryTestSuite) adjustBondedRatio(desiredRatio sdk.Dec) sdkmath.Int {
// from the InitGenesis validator
bondedTokens := sdkmath.NewInt(1e6)
bondDenom := suite.App.GetStakingKeeper().BondDenom(suite.Ctx)

// first, burn all non-delegated coins (bonded ratio = 100%)
suite.App.DeleteGenesisValidatorCoins(suite.T(), suite.Ctx)

if desiredRatio.Equal(sdk.OneDec()) {
return bondedTokens
}

// mint new tokens to adjust the bond ratio
newTotalSupply := sdk.NewDecFromInt(bondedTokens).Quo(desiredRatio).TruncateInt()
coinsToMint := newTotalSupply.Sub(bondedTokens)
err := suite.App.FundAccount(suite.Ctx, app.RandomAddress(), sdk.NewCoins(sdk.NewCoin(bondDenom, coinsToMint)))
suite.Require().NoError(err)

return newTotalSupply
}

0 comments on commit 0efe7f2

Please sign in to comment.