diff --git a/app/app.go b/app/app.go index 4f89a711..c12a13f5 100644 --- a/app/app.go +++ b/app/app.go @@ -674,6 +674,13 @@ func NewApp( app.savingsKeeper = *savingsKeeper.SetHooks(savingstypes.NewMultiSavingsHooks(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 // must be passed by reference here.) app.mm = module.NewManager( diff --git a/app/tally_handler.go b/app/tally_handler.go new file mode 100644 index 00000000..4e68e73e --- /dev/null +++ b/app/tally_handler.go @@ -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) + } + } + } +} diff --git a/go.mod b/go.mod index d81fbe0a..d343c32d 100644 --- a/go.mod +++ b/go.mod @@ -157,7 +157,7 @@ replace ( // Use the cosmos keyring code 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 - 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 github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.7.0 // Use the cosmos modified protobufs diff --git a/go.sum b/go.sum index bfdd2cbf..7032ba04 100644 --- a/go.sum +++ b/go.sum @@ -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/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/kava-labs/cosmos-sdk v0.45.4-kava.3 h1:U4esIl4rzu9sApLFYGwbccPQTQdRg6C9exUVrokqSHY= -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 h1:zAxlDE8VtAPnVjC4hwMqEzFA0H18o+pQlP4aviLKOYc= +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/go.mod h1:HVZfZzWXuqWseXQVplxsWXK6kLHLkk3kQB6c+nuSZvk= github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d h1:Z+RDyXzjKE0i2sTjZ/b1uxiGtPhFy34Ou/Tk0qwN0kM=