forked from Kava-Labs/kava
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tally handler with liquid staking support (Kava-Labs#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 <[email protected]>
- Loading branch information
Showing
4 changed files
with
263 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters