Tally handler with liquid staking support (#1307)

* tally handler with liquid staking support

* clean up

* update for liquid keeper changes

* switch to tagged cosmos-sdk for tallying updates

Co-authored-by: Nick DeLuca <nickdeluca08@gmail.com>
This commit is contained in:
Draco 2022-09-27 11:49:50 -04:00 committed by GitHub
parent b68685af32
commit 314f733cb8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 263 additions and 3 deletions

View File

@ -674,6 +674,13 @@ func NewApp(
app.savingsKeeper = *savingsKeeper.SetHooks(savingstypes.NewMultiSavingsHooks(app.incentiveKeeper.Hooks())) app.savingsKeeper = *savingsKeeper.SetHooks(savingstypes.NewMultiSavingsHooks(app.incentiveKeeper.Hooks()))
app.earnKeeper = *earnKeeper.SetHooks(app.incentiveKeeper.Hooks()) app.earnKeeper = *earnKeeper.SetHooks(app.incentiveKeeper.Hooks())
// override x/gov tally handler with custom implementation
tallyHandler := NewTallyHandler(
app.govKeeper, app.stakingKeeper, app.savingsKeeper, app.earnKeeper,
app.liquidKeeper, app.bankKeeper,
)
app.govKeeper.SetTallyHandler(tallyHandler)
// create the module manager (Note: Any module instantiated in the module manager that is later modified // create the module manager (Note: Any module instantiated in the module manager that is later modified
// must be passed by reference here.) // must be passed by reference here.)
app.mm = module.NewManager( app.mm = module.NewManager(

253
app/tally_handler.go Normal file
View File

@ -0,0 +1,253 @@
package app
import (
sdk "github.com/cosmos/cosmos-sdk/types"
bankkeeper "github.com/cosmos/cosmos-sdk/x/bank/keeper"
govkeeper "github.com/cosmos/cosmos-sdk/x/gov/keeper"
"github.com/cosmos/cosmos-sdk/x/gov/types"
govtypes "github.com/cosmos/cosmos-sdk/x/gov/types"
stakingkeeper "github.com/cosmos/cosmos-sdk/x/staking/keeper"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"
earnkeeper "github.com/kava-labs/kava/x/earn/keeper"
liquidkeeper "github.com/kava-labs/kava/x/liquid/keeper"
liquidtypes "github.com/kava-labs/kava/x/liquid/types"
savingskeeper "github.com/kava-labs/kava/x/savings/keeper"
)
var _ govtypes.TallyHandler = TallyHandler{}
// TallyHandler is the tally handler for kava
type TallyHandler struct {
gk govkeeper.Keeper
stk stakingkeeper.Keeper
svk savingskeeper.Keeper
ek earnkeeper.Keeper
lk liquidkeeper.Keeper
bk bankkeeper.Keeper
}
// NewTallyHandler creates a new tally handler.
func NewTallyHandler(
gk govkeeper.Keeper, stk stakingkeeper.Keeper, svk savingskeeper.Keeper,
ek earnkeeper.Keeper, lk liquidkeeper.Keeper, bk bankkeeper.Keeper,
) TallyHandler {
return TallyHandler{
gk: gk,
stk: stk,
svk: svk,
ek: ek,
lk: lk,
bk: bk,
}
}
func (th TallyHandler) Tally(ctx sdk.Context, proposal types.Proposal) (passes bool, burnDeposits bool, tallyResults types.TallyResult) {
results := make(map[types.VoteOption]sdk.Dec)
results[types.OptionYes] = sdk.ZeroDec()
results[types.OptionAbstain] = sdk.ZeroDec()
results[types.OptionNo] = sdk.ZeroDec()
results[types.OptionNoWithVeto] = sdk.ZeroDec()
totalVotingPower := sdk.ZeroDec()
currValidators := make(map[string]types.ValidatorGovInfo)
// fetch all the bonded validators, insert them into currValidators
th.stk.IterateBondedValidatorsByPower(ctx, func(index int64, validator stakingtypes.ValidatorI) (stop bool) {
currValidators[validator.GetOperator().String()] = types.NewValidatorGovInfo(
validator.GetOperator(),
validator.GetBondedTokens(),
validator.GetDelegatorShares(),
sdk.ZeroDec(),
types.WeightedVoteOptions{},
)
return false
})
th.gk.IterateVotes(ctx, proposal.ProposalId, func(vote types.Vote) bool {
// if validator, just record it in the map
voter, err := sdk.AccAddressFromBech32(vote.Voter)
if err != nil {
panic(err)
}
valAddrStr := sdk.ValAddress(voter.Bytes()).String()
if val, ok := currValidators[valAddrStr]; ok {
val.Vote = vote.Options
currValidators[valAddrStr] = val
}
// iterate over all delegations from voter, deduct from any delegated-to validators
th.stk.IterateDelegations(ctx, voter, func(index int64, delegation stakingtypes.DelegationI) (stop bool) {
valAddrStr := delegation.GetValidatorAddr().String()
if val, ok := currValidators[valAddrStr]; ok {
// There is no need to handle the special case that validator address equal to voter address.
// Because voter's voting power will tally again even if there will deduct voter's voting power from validator.
val.DelegatorDeductions = val.DelegatorDeductions.Add(delegation.GetShares())
currValidators[valAddrStr] = val
// delegation shares * bonded / total shares
votingPower := delegation.GetShares().MulInt(val.BondedTokens).Quo(val.DelegatorShares)
for _, option := range vote.Options {
subPower := votingPower.Mul(option.Weight)
results[option.Option] = results[option.Option].Add(subPower)
}
totalVotingPower = totalVotingPower.Add(votingPower)
}
return false
})
// get voter bkava and update total voting power and results
addrBkava := th.getAddrBkava(ctx, voter).toCoins()
for _, coin := range addrBkava {
valAddr, err := liquidtypes.ParseLiquidStakingTokenDenom(coin.Denom)
if err != nil {
break
}
// reduce delegator shares by the amount of voter bkava for the validator
valAddrStr := valAddr.String()
if val, ok := currValidators[valAddrStr]; ok {
val.DelegatorDeductions = val.DelegatorDeductions.Add(coin.Amount.ToDec())
currValidators[valAddrStr] = val
}
// votingPower = amount of ukava coin
stakedCoins, err := th.lk.GetStakedTokensForDerivatives(ctx, sdk.NewCoins(coin))
if err != nil {
// error is returned only if the bkava denom is incorrect, which should never happen here.
panic(err)
}
votingPower := stakedCoins.Amount.ToDec()
for _, option := range vote.Options {
subPower := votingPower.Mul(option.Weight)
results[option.Option] = results[option.Option].Add(subPower)
}
totalVotingPower = totalVotingPower.Add(votingPower)
}
th.gk.DeleteVote(ctx, vote.ProposalId, voter)
return false
})
// iterate over the validators again to tally their voting power
for _, val := range currValidators {
if len(val.Vote) == 0 {
continue
}
sharesAfterDeductions := val.DelegatorShares.Sub(val.DelegatorDeductions)
votingPower := sharesAfterDeductions.MulInt(val.BondedTokens).Quo(val.DelegatorShares)
for _, option := range val.Vote {
subPower := votingPower.Mul(option.Weight)
results[option.Option] = results[option.Option].Add(subPower)
}
totalVotingPower = totalVotingPower.Add(votingPower)
}
tallyParams := th.gk.GetTallyParams(ctx)
tallyResults = types.NewTallyResultFromMap(results)
// TODO: Upgrade the spec to cover all of these cases & remove pseudocode.
// If there is no staked coins, the proposal fails
if th.stk.TotalBondedTokens(ctx).IsZero() {
return false, false, tallyResults
}
// If there is not enough quorum of votes, the proposal fails
percentVoting := totalVotingPower.Quo(th.stk.TotalBondedTokens(ctx).ToDec())
if percentVoting.LT(tallyParams.Quorum) {
return false, true, tallyResults
}
// If no one votes (everyone abstains), proposal fails
if totalVotingPower.Sub(results[types.OptionAbstain]).Equal(sdk.ZeroDec()) {
return false, false, tallyResults
}
// If more than 1/3 of voters veto, proposal fails
if results[types.OptionNoWithVeto].Quo(totalVotingPower).GT(tallyParams.VetoThreshold) {
return false, true, tallyResults
}
// If more than 1/2 of non-abstaining voters vote Yes, proposal passes
if results[types.OptionYes].Quo(totalVotingPower.Sub(results[types.OptionAbstain])).GT(tallyParams.Threshold) {
return true, false, tallyResults
}
// If more than 1/2 of non-abstaining voters vote No, proposal fails
return false, false, tallyResults
}
// bkavaByDenom a map of the bkava denom and the amount of bkava for that denom.
type bkavaByDenom map[string]sdk.Int
func (bkavaMap bkavaByDenom) add(coin sdk.Coin) {
_, found := bkavaMap[coin.Denom]
if !found {
bkavaMap[coin.Denom] = sdk.ZeroInt()
}
bkavaMap[coin.Denom].Add(coin.Amount)
}
func (bkavaMap bkavaByDenom) toCoins() sdk.Coins {
coins := sdk.Coins{}
for denom, amt := range bkavaMap {
coins.Add(sdk.NewCoin(denom, amt))
}
return coins.Sort()
}
// getAddrBkava returns a map of validator address & the amount of bkava
// of the addr for each validator.
func (th TallyHandler) getAddrBkava(ctx sdk.Context, addr sdk.AccAddress) bkavaByDenom {
results := make(bkavaByDenom)
th.addBkavaFromWallet(ctx, addr, results)
th.addBkavaFromSavings(ctx, addr, results)
th.addBkavaFromEarn(ctx, addr, results)
return results
}
// addBkavaFromWallet adds all addr balances of bkava in x/bank.
func (th TallyHandler) addBkavaFromWallet(ctx sdk.Context, addr sdk.AccAddress, bkava bkavaByDenom) {
coins := th.bk.GetAllBalances(ctx, addr)
for _, coin := range coins {
if th.lk.IsDerivativeDenom(ctx, coin.Denom) {
bkava.add(coin)
}
}
}
// addBkavaFromSavings adds all addr deposits of bkava in x/savings.
func (th TallyHandler) addBkavaFromSavings(ctx sdk.Context, addr sdk.AccAddress, bkava bkavaByDenom) {
deposit, found := th.svk.GetDeposit(ctx, addr)
if !found {
return
}
for _, coin := range deposit.Amount {
if th.lk.IsDerivativeDenom(ctx, coin.Denom) {
bkava.add(coin)
}
}
}
// addBkavaFromEarn adds all addr deposits of bkava in x/earn.
func (th TallyHandler) addBkavaFromEarn(ctx sdk.Context, addr sdk.AccAddress, bkava bkavaByDenom) {
shares, found := th.ek.GetVaultAccountShares(ctx, addr)
if !found {
return
}
for _, share := range shares {
if th.lk.IsDerivativeDenom(ctx, share.Denom) {
if coin, err := th.ek.ConvertToAssets(ctx, share); err != nil {
bkava.add(coin)
}
}
}
}

2
go.mod
View File

@ -157,7 +157,7 @@ replace (
// Use the cosmos keyring code // Use the cosmos keyring code
github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76 github.com/99designs/keyring => github.com/cosmos/keyring v1.1.7-0.20210622111912-ef00f8ac3d76
// Use cosmos-sdk fork with backported fix for unsafe-reset-all // Use cosmos-sdk fork with backported fix for unsafe-reset-all
github.com/cosmos/cosmos-sdk => github.com/kava-labs/cosmos-sdk v0.45.4-kava.3 github.com/cosmos/cosmos-sdk => github.com/kava-labs/cosmos-sdk v0.45.4-kava.4
// See https://github.com/cosmos/cosmos-sdk/pull/10401, https://github.com/cosmos/cosmos-sdk/commit/0592ba6158cd0bf49d894be1cef4faeec59e8320 // See https://github.com/cosmos/cosmos-sdk/pull/10401, https://github.com/cosmos/cosmos-sdk/commit/0592ba6158cd0bf49d894be1cef4faeec59e8320
github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.7.0 github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.7.0
// Use the cosmos modified protobufs // Use the cosmos modified protobufs

4
go.sum
View File

@ -696,8 +696,8 @@ github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E
github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0= github.com/jwilder/encoding v0.0.0-20170811194829-b4e1701a28ef/go.mod h1:Ct9fl0F6iIOGgxJ5npU/IUOhOhqlVrGjyIZc8/MagT0=
github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/karalabe/usb v0.0.0-20190919080040-51dc0efba356/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU= github.com/karalabe/usb v0.0.2/go.mod h1:Od972xHfMJowv7NGVDiWVxk2zxnWgjLlJzE+F4F7AGU=
github.com/kava-labs/cosmos-sdk v0.45.4-kava.3 h1:U4esIl4rzu9sApLFYGwbccPQTQdRg6C9exUVrokqSHY= github.com/kava-labs/cosmos-sdk v0.45.4-kava.4 h1:zAxlDE8VtAPnVjC4hwMqEzFA0H18o+pQlP4aviLKOYc=
github.com/kava-labs/cosmos-sdk v0.45.4-kava.3/go.mod h1:WOqtDxN3eCCmnYLVla10xG7lEXkFjpTaqm2a2WasgCc= github.com/kava-labs/cosmos-sdk v0.45.4-kava.4/go.mod h1:WOqtDxN3eCCmnYLVla10xG7lEXkFjpTaqm2a2WasgCc=
github.com/kava-labs/tm-db v0.6.7-kava.1 h1:7cVYlvWx1yP+gGdaAWcfm6NwMLzf4z6DxXguWn3+O3w= github.com/kava-labs/tm-db v0.6.7-kava.1 h1:7cVYlvWx1yP+gGdaAWcfm6NwMLzf4z6DxXguWn3+O3w=
github.com/kava-labs/tm-db v0.6.7-kava.1/go.mod h1:HVZfZzWXuqWseXQVplxsWXK6kLHLkk3kQB6c+nuSZvk= github.com/kava-labs/tm-db v0.6.7-kava.1/go.mod h1:HVZfZzWXuqWseXQVplxsWXK6kLHLkk3kQB6c+nuSZvk=
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM= github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=