package app import ( sdkmath "cosmossdk.io/math" 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" govv1 "github.com/cosmos/cosmos-sdk/x/gov/types/v1" 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 _ govv1.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 govv1.Proposal, ) (passes bool, burnDeposits bool, tallyResults govv1.TallyResult) { results := make(map[govv1.VoteOption]sdk.Dec) results[govv1.OptionYes] = sdk.ZeroDec() results[govv1.OptionAbstain] = sdk.ZeroDec() results[govv1.OptionNo] = sdk.ZeroDec() results[govv1.OptionNoWithVeto] = sdk.ZeroDec() totalVotingPower := sdk.ZeroDec() currValidators := make(map[string]govv1.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()] = govv1.NewValidatorGovInfo( validator.GetOperator(), validator.GetBondedTokens(), validator.GetDelegatorShares(), sdk.ZeroDec(), govv1.WeightedVoteOptions{}, ) return false }) th.gk.IterateVotes(ctx, proposal.Id, func(vote govv1.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(sdk.MustNewDecFromStr(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(sdk.NewDecFromInt(coin.Amount)) 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 := sdk.NewDecFromInt(stakedCoins.Amount) for _, option := range vote.Options { subPower := votingPower.Mul(sdk.MustNewDecFromStr(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(sdk.MustNewDecFromStr(option.Weight)) results[option.Option] = results[option.Option].Add(subPower) } totalVotingPower = totalVotingPower.Add(votingPower) } tallyParams := th.gk.GetParams(ctx) tallyResults = govv1.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(sdk.NewDecFromInt(th.stk.TotalBondedTokens(ctx))) if percentVoting.LT(sdk.MustNewDecFromStr(tallyParams.Quorum)) { return false, tallyParams.BurnVoteQuorum, tallyResults } // If no one votes (everyone abstains), proposal fails if totalVotingPower.Sub(results[govv1.OptionAbstain]).Equal(sdk.ZeroDec()) { return false, false, tallyResults } // If more than 1/3 of voters veto, proposal fails if results[govv1.OptionNoWithVeto].Quo(totalVotingPower).GT(sdk.MustNewDecFromStr(tallyParams.VetoThreshold)) { return false, tallyParams.BurnVoteVeto, tallyResults } // If more than 1/2 of non-abstaining voters vote Yes, proposal passes if results[govv1.OptionYes].Quo(totalVotingPower.Sub(results[govv1.OptionAbstain])).GT(sdk.MustNewDecFromStr(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]sdkmath.Int func (bkavaMap bkavaByDenom) add(coin sdk.Coin) { _, found := bkavaMap[coin.Denom] if !found { bkavaMap[coin.Denom] = sdk.ZeroInt() } bkavaMap[coin.Denom] = bkavaMap[coin.Denom].Add(coin.Amount) } func (bkavaMap bkavaByDenom) toCoins() sdk.Coins { coins := sdk.Coins{} for denom, amt := range bkavaMap { coins = 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) } } } }