0g-chain/app/tally_handler.go
Draco 314f733cb8
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>
2022-09-27 11:49:50 -04:00

254 lines
8.2 KiB
Go

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)
}
}
}
}