diff --git a/PENDING.md b/PENDING.md index aee683108fb7..1cc345581a6c 100644 --- a/PENDING.md +++ b/PENDING.md @@ -52,6 +52,8 @@ FEATURES * [\#2182] [x/staking] Added querier for querying a single redelegation * SDK + - \#3099 Implement F1 fee distribution + - [\#2926](https://github.com/cosmos/cosmos-sdk/issues/2926) Add TxEncoder to client TxBuilder. * \#2694 Vesting account implementation. * \#2996 Update the `AccountKeeper` to contain params used in the context of the ante handler. diff --git a/baseapp/baseapp_test.go b/baseapp/baseapp_test.go index d088b841fc45..acccd1fbf6fe 100644 --- a/baseapp/baseapp_test.go +++ b/baseapp/baseapp_test.go @@ -4,10 +4,11 @@ import ( "bytes" "encoding/binary" "fmt" - "github.com/cosmos/cosmos-sdk/store" "os" "testing" + "github.com/cosmos/cosmos-sdk/store" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" diff --git a/client/utils/utils_test.go b/client/utils/utils_test.go index cd754d9c1d3f..d5ed6205ebdb 100644 --- a/client/utils/utils_test.go +++ b/client/utils/utils_test.go @@ -5,11 +5,12 @@ import ( "errors" "testing" - "github.com/cosmos/cosmos-sdk/codec" - "github.com/cosmos/cosmos-sdk/x/auth" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/x/auth" + "github.com/stretchr/testify/assert" "github.com/tendermint/tendermint/libs/common" diff --git a/cmd/gaia/app/app.go b/cmd/gaia/app/app.go index 7945a8bc655d..64c78893b3bc 100644 --- a/cmd/gaia/app/app.go +++ b/cmd/gaia/app/app.go @@ -238,6 +238,9 @@ func (app *GaiaApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisSt app.accountKeeper.SetAccount(ctx, acc) } + // initialize distribution (must happen before staking) + distr.InitGenesis(ctx, app.distrKeeper, genesisState.DistrData) + // load the initial staking information validators, err := staking.InitGenesis(ctx, app.stakingKeeper, genesisState.StakingData) if err != nil { @@ -249,7 +252,6 @@ func (app *GaiaApp) initFromGenesisState(ctx sdk.Context, genesisState GenesisSt slashing.InitGenesis(ctx, app.slashingKeeper, genesisState.SlashingData, genesisState.StakingData) gov.InitGenesis(ctx, app.govKeeper, genesisState.GovData) mint.InitGenesis(ctx, app.mintKeeper, genesisState.MintData) - distr.InitGenesis(ctx, app.distrKeeper, genesisState.DistrData) // validate genesis state err = GaiaValidateGenesisState(genesisState) @@ -370,3 +372,11 @@ func (h StakingHooks) BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAd h.dh.BeforeDelegationRemoved(ctx, delAddr, valAddr) h.sh.BeforeDelegationRemoved(ctx, delAddr, valAddr) } +func (h StakingHooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { + h.dh.AfterDelegationModified(ctx, delAddr, valAddr) + h.sh.AfterDelegationModified(ctx, delAddr, valAddr) +} +func (h StakingHooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) { + h.dh.BeforeValidatorSlashed(ctx, valAddr, fraction) + h.sh.BeforeValidatorSlashed(ctx, valAddr, fraction) +} diff --git a/cmd/gaia/app/export.go b/cmd/gaia/app/export.go index 18413041a5d4..a6e6ce9cd708 100644 --- a/cmd/gaia/app/export.go +++ b/cmd/gaia/app/export.go @@ -2,7 +2,6 @@ package app import ( "encoding/json" - "fmt" abci "github.com/tendermint/tendermint/abci/types" tmtypes "github.com/tendermint/tendermint/types" @@ -14,7 +13,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/gov" "github.com/cosmos/cosmos-sdk/x/mint" "github.com/cosmos/cosmos-sdk/x/slashing" - staking "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/staking" ) // export the state of gaia for a genesis file @@ -62,55 +61,41 @@ func (app *GaiaApp) prepForZeroHeightGenesis(ctx sdk.Context) { /* Handle fee distribution state. */ - // withdraw all delegator & validator rewards - vdiIter := func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { - err := app.distrKeeper.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr) - if err != nil { - panic(err) - } + // withdraw all validator commission + app.stakingKeeper.IterateValidators(ctx, func(_ int64, val sdk.Validator) (stop bool) { + _ = app.distrKeeper.WithdrawValidatorCommission(ctx, val.GetOperator()) return false - } - app.distrKeeper.IterateValidatorDistInfos(ctx, vdiIter) + }) - ddiIter := func(_ int64, distInfo distr.DelegationDistInfo) (stop bool) { - err := app.distrKeeper.WithdrawDelegationReward( - ctx, distInfo.DelegatorAddr, distInfo.ValOperatorAddr) - if err != nil { - panic(err) - } - return false + // withdraw all delegator rewards + dels := app.stakingKeeper.GetAllDelegations(ctx) + for _, delegation := range dels { + _ = app.distrKeeper.WithdrawDelegationRewards(ctx, delegation.DelegatorAddr, delegation.ValidatorAddr) } - app.distrKeeper.IterateDelegationDistInfos(ctx, ddiIter) - app.assertRuntimeInvariantsOnContext(ctx) + // clear validator slash events + app.distrKeeper.DeleteValidatorSlashEvents(ctx) - // set distribution info withdrawal heights to 0 - app.distrKeeper.IterateDelegationDistInfos(ctx, func(_ int64, delInfo distr.DelegationDistInfo) (stop bool) { - delInfo.DelPoolWithdrawalHeight = 0 - app.distrKeeper.SetDelegationDistInfo(ctx, delInfo) - return false - }) - app.distrKeeper.IterateValidatorDistInfos(ctx, func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { - valInfo.FeePoolWithdrawalHeight = 0 - valInfo.DelAccum.UpdateHeight = 0 - app.distrKeeper.SetValidatorDistInfo(ctx, valInfo) + // clear validator historical rewards + app.distrKeeper.DeleteValidatorHistoricalRewards(ctx) + + // set context height to zero + height := ctx.BlockHeight() + ctx = ctx.WithBlockHeight(0) + + // reinitialize all validators + app.stakingKeeper.IterateValidators(ctx, func(_ int64, val sdk.Validator) (stop bool) { + app.distrKeeper.Hooks().AfterValidatorCreated(ctx, val.GetOperator()) return false }) - // assert that the fee pool is empty - feePool := app.distrKeeper.GetFeePool(ctx) - if !feePool.TotalValAccum.Accum.IsZero() { - panic("unexpected leftover validator accum") - } - bondDenom := app.stakingKeeper.GetParams(ctx).BondDenom - if !feePool.ValPool.AmountOf(bondDenom).IsZero() { - panic(fmt.Sprintf("unexpected leftover validator pool coins: %v", - feePool.ValPool.AmountOf(bondDenom).String())) + // reinitialize all delegations + for _, del := range dels { + app.distrKeeper.Hooks().BeforeDelegationCreated(ctx, del.DelegatorAddr, del.ValidatorAddr) } - // reset fee pool height, save fee pool - feePool.TotalValAccum = distr.NewTotalAccum(0) - app.distrKeeper.SetFeePool(ctx, feePool) + // reset context height + ctx = ctx.WithBlockHeight(height) /* Handle staking state. */ diff --git a/cmd/gaia/app/invariants.go b/cmd/gaia/app/invariants.go index bc5a7c5f30dd..cb53f1f9cfeb 100644 --- a/cmd/gaia/app/invariants.go +++ b/cmd/gaia/app/invariants.go @@ -16,7 +16,7 @@ import ( func (app *GaiaApp) runtimeInvariants() []simulation.Invariant { return []simulation.Invariant{ banksim.NonnegativeBalanceInvariant(app.accountKeeper), - distrsim.ValAccumInvariants(app.distrKeeper, app.stakingKeeper), + distrsim.NonNegativeOutstandingInvariant(app.distrKeeper), stakingsim.SupplyInvariants(app.bankKeeper, app.stakingKeeper, app.feeCollectionKeeper, app.distrKeeper, app.accountKeeper), stakingsim.NonNegativePowerInvariant(app.stakingKeeper), diff --git a/cmd/gaia/app/sim_test.go b/cmd/gaia/app/sim_test.go index da72937415eb..6f14cb6fdbed 100644 --- a/cmd/gaia/app/sim_test.go +++ b/cmd/gaia/app/sim_test.go @@ -29,7 +29,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/mock/simulation" "github.com/cosmos/cosmos-sdk/x/slashing" slashingsim "github.com/cosmos/cosmos-sdk/x/slashing/simulation" - staking "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/staking" stakingsim "github.com/cosmos/cosmos-sdk/x/staking/simulation" stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" ) @@ -120,8 +120,8 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { Params: slashing.Params{ MaxEvidenceAge: stakingGenesis.Params.UnbondingTime, SignedBlocksWindow: int64(randIntBetween(r, 10, 1000)), - DowntimeJailDuration: time.Duration(randIntBetween(r, 60, 60*60*24)) * time.Second, MinSignedPerWindow: sdk.NewDecWithPrec(int64(r.Intn(10)), 1), + DowntimeJailDuration: time.Duration(randIntBetween(r, 60, 60*60*24)) * time.Second, SlashFractionDoubleSign: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(50) + 1))), SlashFractionDowntime: sdk.NewDec(1).Quo(sdk.NewDec(int64(r.Intn(200) + 1))), }, @@ -160,12 +160,20 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { stakingGenesis.Validators = validators stakingGenesis.Bonds = delegations + distrGenesis := distr.GenesisState{ + FeePool: distr.InitialFeePool(), + CommunityTax: sdk.NewDecWithPrec(1, 2).Add(sdk.NewDecWithPrec(int64(r.Intn(30)), 2)), + BaseProposerReward: sdk.NewDecWithPrec(1, 2).Add(sdk.NewDecWithPrec(int64(r.Intn(30)), 2)), + BonusProposerReward: sdk.NewDecWithPrec(1, 2).Add(sdk.NewDecWithPrec(int64(r.Intn(30)), 2)), + } + fmt.Printf("Selected randomly generated distribution parameters:\n\t%+v\n", distrGenesis) + genesis := GenesisState{ Accounts: genesisAccounts, AuthData: authGenesis, StakingData: stakingGenesis, MintData: mintGenesis, - DistrData: distr.DefaultGenesisWithValidators(valAddrs), + DistrData: distrGenesis, SlashingData: slashingGenesis, GovData: govGenesis, } @@ -188,9 +196,8 @@ func testAndRunTxs(app *GaiaApp) []simulation.WeightedOperation { {5, authsim.SimulateDeductFee(app.accountKeeper, app.feeCollectionKeeper)}, {100, banksim.SingleInputSendMsg(app.accountKeeper, app.bankKeeper)}, {50, distrsim.SimulateMsgSetWithdrawAddress(app.accountKeeper, app.distrKeeper)}, - {50, distrsim.SimulateMsgWithdrawDelegatorRewardsAll(app.accountKeeper, app.distrKeeper)}, {50, distrsim.SimulateMsgWithdrawDelegatorReward(app.accountKeeper, app.distrKeeper)}, - {50, distrsim.SimulateMsgWithdrawValidatorRewardsAll(app.accountKeeper, app.distrKeeper)}, + {50, distrsim.SimulateMsgWithdrawValidatorCommission(app.accountKeeper, app.distrKeeper)}, {5, govsim.SimulateSubmittingVotingAndSlashingForProposal(app.govKeeper, app.stakingKeeper)}, {100, govsim.SimulateMsgDeposit(app.govKeeper)}, {100, stakingsim.SimulateMsgCreateValidator(app.accountKeeper, app.stakingKeeper)}, diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 1c8f080c1738..d5e775f73211 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -56,7 +56,7 @@ func TestGaiaCLIMinimumFees(t *testing.T) { txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(feeDenom, 23)) success, _, _ = f.TxSend(keyFoo, barAddr, sdk.NewInt64Coin(feeDenom, 10), txFees) require.True(f.T, success) - tests.WaitForNextNBlocksTM(1, f.Port) + tests.WaitForNextNBlocksTM(2, f.Port) // Ensure tx w/ improper fees (footoken) fails txFees = fmt.Sprintf("--fees=%s", sdk.NewInt64Coin(fooDenom, 23)) diff --git a/cmd/gaia/cmd/gaiad/main.go b/cmd/gaia/cmd/gaiad/main.go index 6cb2d4eebe72..5f2d64de2573 100644 --- a/cmd/gaia/cmd/gaiad/main.go +++ b/cmd/gaia/cmd/gaiad/main.go @@ -2,9 +2,10 @@ package main import ( "encoding/json" - "github.com/cosmos/cosmos-sdk/store" "io" + "github.com/cosmos/cosmos-sdk/store" + "github.com/cosmos/cosmos-sdk/baseapp" "github.com/spf13/cobra" diff --git a/cmd/gaia/cmd/gaiareplay/main.go b/cmd/gaia/cmd/gaiareplay/main.go index cf946edce3c1..ef6bcb139a4a 100644 --- a/cmd/gaia/cmd/gaiareplay/main.go +++ b/cmd/gaia/cmd/gaiareplay/main.go @@ -2,12 +2,13 @@ package main import ( "fmt" - "github.com/cosmos/cosmos-sdk/store" "io" "os" "path/filepath" "time" + "github.com/cosmos/cosmos-sdk/store" + cpm "github.com/otiai10/copy" "github.com/spf13/cobra" diff --git a/docs/examples/democoin/mock/validator.go b/docs/examples/democoin/mock/validator.go index b8f2b6c0749d..7a3691e2bc06 100644 --- a/docs/examples/democoin/mock/validator.go +++ b/docs/examples/democoin/mock/validator.go @@ -69,6 +69,11 @@ func (v Validator) GetMoniker() string { return "" } +// Implements sdk.Validator +func (v Validator) GetDelegatorShareExRate() sdk.Dec { + return sdk.ZeroDec() +} + // Implements sdk.Validator type ValidatorSet struct { Validators []Validator diff --git a/docs/examples/democoin/x/simplestaking/client/cli/commands.go b/docs/examples/democoin/x/simplestaking/client/cli/commands.go index 5a2b819f3f62..37b1c20ded7d 100644 --- a/docs/examples/democoin/x/simplestaking/client/cli/commands.go +++ b/docs/examples/democoin/x/simplestaking/client/cli/commands.go @@ -3,6 +3,7 @@ package cli import ( "encoding/hex" "fmt" + "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/utils" "github.com/cosmos/cosmos-sdk/codec" diff --git a/x/distribution/types/dec_coin.go b/types/dec_coin.go similarity index 82% rename from x/distribution/types/dec_coin.go rename to types/dec_coin.go index 8a9e9f0d0be1..62b0bea1dceb 100644 --- a/x/distribution/types/dec_coin.go +++ b/types/dec_coin.go @@ -3,34 +3,32 @@ package types import ( "fmt" "strings" - - sdk "github.com/cosmos/cosmos-sdk/types" ) // Coins which can have additional decimal points type DecCoin struct { - Denom string `json:"denom"` - Amount sdk.Dec `json:"amount"` + Denom string `json:"denom"` + Amount Dec `json:"amount"` } func NewDecCoin(denom string, amount int64) DecCoin { return DecCoin{ Denom: denom, - Amount: sdk.NewDec(amount), + Amount: NewDec(amount), } } -func NewDecCoinFromDec(denom string, amount sdk.Dec) DecCoin { +func NewDecCoinFromDec(denom string, amount Dec) DecCoin { return DecCoin{ Denom: denom, Amount: amount, } } -func NewDecCoinFromCoin(coin sdk.Coin) DecCoin { +func NewDecCoinFromCoin(coin Coin) DecCoin { return DecCoin{ Denom: coin.Denom, - Amount: sdk.NewDecFromInt(coin.Amount), + Amount: NewDecFromInt(coin.Amount), } } @@ -51,10 +49,10 @@ func (coin DecCoin) Minus(coinB DecCoin) DecCoin { } // return the decimal coins with trunctated decimals, and return the change -func (coin DecCoin) TruncateDecimal() (sdk.Coin, DecCoin) { +func (coin DecCoin) TruncateDecimal() (Coin, DecCoin) { truncated := coin.Amount.TruncateInt() - change := coin.Amount.Sub(sdk.NewDecFromInt(truncated)) - return sdk.NewCoin(coin.Denom, truncated), DecCoin{coin.Denom, change} + change := coin.Amount.Sub(NewDecFromInt(truncated)) + return NewCoin(coin.Denom, truncated), DecCoin{coin.Denom, change} } //_______________________________________________________________________ @@ -62,7 +60,7 @@ func (coin DecCoin) TruncateDecimal() (sdk.Coin, DecCoin) { // coins with decimal type DecCoins []DecCoin -func NewDecCoins(coins sdk.Coins) DecCoins { +func NewDecCoins(coins Coins) DecCoins { dcs := make(DecCoins, len(coins)) for i, coin := range coins { dcs[i] = NewDecCoinFromCoin(coin) @@ -71,9 +69,9 @@ func NewDecCoins(coins sdk.Coins) DecCoins { } // return the coins with trunctated decimals, and return the change -func (coins DecCoins) TruncateDecimal() (sdk.Coins, DecCoins) { +func (coins DecCoins) TruncateDecimal() (Coins, DecCoins) { changeSum := DecCoins{} - out := make(sdk.Coins, len(coins)) + out := make(Coins, len(coins)) for i, coin := range coins { truncated, change := coin.TruncateDecimal() out[i] = truncated @@ -135,7 +133,7 @@ func (coins DecCoins) Minus(coinsB DecCoins) DecCoins { } // multiply all the coins by a decimal -func (coins DecCoins) MulDec(d sdk.Dec) DecCoins { +func (coins DecCoins) MulDec(d Dec) DecCoins { res := make([]DecCoin, len(coins)) for i, coin := range coins { product := DecCoin{ @@ -148,7 +146,7 @@ func (coins DecCoins) MulDec(d sdk.Dec) DecCoins { } // divide all the coins by a decimal -func (coins DecCoins) QuoDec(d sdk.Dec) DecCoins { +func (coins DecCoins) QuoDec(d Dec) DecCoins { res := make([]DecCoin, len(coins)) for i, coin := range coins { quotient := DecCoin{ @@ -161,16 +159,16 @@ func (coins DecCoins) QuoDec(d sdk.Dec) DecCoins { } // returns the amount of a denom from deccoins -func (coins DecCoins) AmountOf(denom string) sdk.Dec { +func (coins DecCoins) AmountOf(denom string) Dec { switch len(coins) { case 0: - return sdk.ZeroDec() + return ZeroDec() case 1: coin := coins[0] if coin.Denom == denom { return coin.Amount } - return sdk.ZeroDec() + return ZeroDec() default: midIdx := len(coins) / 2 // binary search coin := coins[midIdx] diff --git a/x/distribution/types/dec_coin_test.go b/types/dec_coin_test.go similarity index 77% rename from x/distribution/types/dec_coin_test.go rename to types/dec_coin_test.go index 9b942f07cebc..fe6907395527 100644 --- a/x/distribution/types/dec_coin_test.go +++ b/types/dec_coin_test.go @@ -5,14 +5,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" ) func TestPlusDecCoin(t *testing.T) { - decCoinA1 := DecCoin{"A", sdk.NewDecWithPrec(11, 1)} - decCoinA2 := DecCoin{"A", sdk.NewDecWithPrec(22, 1)} - decCoinB1 := DecCoin{"B", sdk.NewDecWithPrec(11, 1)} + decCoinA1 := DecCoin{"A", NewDecWithPrec(11, 1)} + decCoinA2 := DecCoin{"A", NewDecWithPrec(22, 1)} + decCoinB1 := DecCoin{"B", NewDecWithPrec(11, 1)} // regular add res := decCoinA1.Plus(decCoinA1) @@ -25,11 +23,11 @@ func TestPlusDecCoin(t *testing.T) { } -func TestPlusCoins(t *testing.T) { - one := sdk.NewDec(1) - zero := sdk.NewDec(0) - negone := sdk.NewDec(-1) - two := sdk.NewDec(2) +func TestPlusDecCoins(t *testing.T) { + one := NewDec(1) + zero := NewDec(0) + negone := NewDec(-1) + two := NewDec(2) cases := []struct { inputOne DecCoins diff --git a/types/stake.go b/types/stake.go index c95bb2192395..25c02d8489d4 100644 --- a/types/stake.go +++ b/types/stake.go @@ -45,8 +45,9 @@ type Validator interface { GetPower() Int // validation power GetTokens() Int // validation tokens GetCommission() Dec // validator commission rate - GetDelegatorShares() Dec // Total out standing delegator shares + GetDelegatorShares() Dec // total outstanding delegator shares GetBondHeight() int64 // height in which the validator became active + GetDelegatorShareExRate() Dec // tokens per delegator share exchange rate } // validator which fulfills abci validator interface for use in Tendermint @@ -126,4 +127,6 @@ type StakingHooks interface { BeforeDelegationCreated(ctx Context, delAddr AccAddress, valAddr ValAddress) // Must be called when a delegation is created BeforeDelegationSharesModified(ctx Context, delAddr AccAddress, valAddr ValAddress) // Must be called when a delegation's shares are modified BeforeDelegationRemoved(ctx Context, delAddr AccAddress, valAddr ValAddress) // Must be called when a delegation is removed + AfterDelegationModified(ctx Context, delAddr AccAddress, valAddr ValAddress) + BeforeValidatorSlashed(ctx Context, valAddr ValAddress, fraction Dec) } diff --git a/x/auth/client/rest/sign.go b/x/auth/client/rest/sign.go index 754a07b44e60..a104c7fa14cf 100644 --- a/x/auth/client/rest/sign.go +++ b/x/auth/client/rest/sign.go @@ -36,7 +36,7 @@ func SignTxRequestHandlerFn(cdc *codec.Codec, cliCtx context.CLIContext) http.Ha } // validate tx - // discard error if it's CodeNoSignatures as the tx comes with no signatures + // discard error if it's CodeNoSignatures as the tx comes with no signatures if err := m.Tx.ValidateBasic(); err != nil && err.Code() != sdk.CodeNoSignatures { utils.WriteErrorResponse(w, http.StatusBadRequest, err.Error()) return diff --git a/x/distribution/abci_app.go b/x/distribution/abci_app.go index 800ede928d88..aae399062320 100644 --- a/x/distribution/abci_app.go +++ b/x/distribution/abci_app.go @@ -10,21 +10,8 @@ import ( // set the proposer for determining distribution during endblock func BeginBlocker(ctx sdk.Context, req abci.RequestBeginBlock, k keeper.Keeper) { - if ctx.BlockHeight() > 1 { - previousPercentPrecommitVotes := getPreviousPercentPrecommitVotes(req) - previousProposer := k.GetPreviousProposerConsAddr(ctx) - k.AllocateTokens(ctx, previousPercentPrecommitVotes, previousProposer) - } - - consAddr := sdk.ConsAddress(req.Header.ProposerAddress) - k.SetPreviousProposerConsAddr(ctx, consAddr) -} - -// percent precommit votes for the previous block -func getPreviousPercentPrecommitVotes(req abci.RequestBeginBlock) sdk.Dec { - - // determine the total number of signed power - totalPower, sumPrecommitPower := int64(0), int64(0) + // determine the total power signing the block + var totalPower, sumPrecommitPower int64 for _, voteInfo := range req.LastCommitInfo.GetVotes() { totalPower += voteInfo.Validator.Power if voteInfo.SignedLastBlock { @@ -32,8 +19,15 @@ func getPreviousPercentPrecommitVotes(req abci.RequestBeginBlock) sdk.Dec { } } - if totalPower == 0 { - return sdk.ZeroDec() + // TODO this is Tendermint-dependent + // ref https://github.com/cosmos/cosmos-sdk/issues/3095 + if ctx.BlockHeight() > 1 { + previousProposer := k.GetPreviousProposerConsAddr(ctx) + k.AllocateTokens(ctx, sumPrecommitPower, totalPower, previousProposer, req.LastCommitInfo.GetVotes()) } - return sdk.NewDec(sumPrecommitPower).Quo(sdk.NewDec(totalPower)) + + // record the proposer for when we payout on the next block + consAddr := sdk.ConsAddress(req.Header.ProposerAddress) + k.SetPreviousProposerConsAddr(ctx, consAddr) + } diff --git a/x/distribution/alias.go b/x/distribution/alias.go index 2ca6d8e91cca..a38c2e431d76 100644 --- a/x/distribution/alias.go +++ b/x/distribution/alias.go @@ -11,18 +11,9 @@ type ( Keeper = keeper.Keeper Hooks = keeper.Hooks - DelegatorWithdrawInfo = types.DelegatorWithdrawInfo - DelegationDistInfo = types.DelegationDistInfo - ValidatorDistInfo = types.ValidatorDistInfo - TotalAccum = types.TotalAccum - FeePool = types.FeePool - DecCoin = types.DecCoin - DecCoins = types.DecCoins - MsgSetWithdrawAddress = types.MsgSetWithdrawAddress - MsgWithdrawDelegatorRewardsAll = types.MsgWithdrawDelegatorRewardsAll MsgWithdrawDelegatorReward = types.MsgWithdrawDelegatorReward - MsgWithdrawValidatorRewardsAll = types.MsgWithdrawValidatorRewardsAll + MsgWithdrawValidatorCommission = types.MsgWithdrawValidatorCommission GenesisState = types.GenesisState @@ -32,39 +23,6 @@ type ( FeeCollectionKeeper = types.FeeCollectionKeeper ) -var ( - NewKeeper = keeper.NewKeeper - - GetValidatorDistInfoKey = keeper.GetValidatorDistInfoKey - GetDelegationDistInfoKey = keeper.GetDelegationDistInfoKey - GetDelegationDistInfosKey = keeper.GetDelegationDistInfosKey - GetDelegatorWithdrawAddrKey = keeper.GetDelegatorWithdrawAddrKey - FeePoolKey = keeper.FeePoolKey - ValidatorDistInfoKey = keeper.ValidatorDistInfoKey - DelegationDistInfoKey = keeper.DelegationDistInfoKey - DelegatorWithdrawInfoKey = keeper.DelegatorWithdrawInfoKey - ProposerKey = keeper.ProposerKey - DefaultParamspace = keeper.DefaultParamspace - - InitialFeePool = types.InitialFeePool - - NewGenesisState = types.NewGenesisState - ValidateGenesis = types.ValidateGenesis - DefaultGenesisState = types.DefaultGenesisState - DefaultGenesisWithValidators = types.DefaultGenesisWithValidators - - RegisterCodec = types.RegisterCodec - - NewMsgSetWithdrawAddress = types.NewMsgSetWithdrawAddress - NewMsgWithdrawDelegatorRewardsAll = types.NewMsgWithdrawDelegatorRewardsAll - NewMsgWithdrawDelegatorReward = types.NewMsgWithdrawDelegatorReward - NewMsgWithdrawValidatorRewardsAll = types.NewMsgWithdrawValidatorRewardsAll - - NewDecCoins = types.NewDecCoins - - NewTotalAccum = types.NewTotalAccum -) - const ( DefaultCodespace = types.DefaultCodespace CodeInvalidInput = types.CodeInvalidInput @@ -81,4 +39,16 @@ var ( TagValidator = tags.Validator TagDelegator = tags.Delegator + + NewMsgSetWithdrawAddress = types.NewMsgSetWithdrawAddress + NewMsgWithdrawDelegatorReward = types.NewMsgWithdrawDelegatorReward + NewMsgWithdrawValidatorCommission = types.NewMsgWithdrawValidatorCommission + + NewKeeper = keeper.NewKeeper + DefaultParamspace = keeper.DefaultParamspace + + RegisterCodec = types.RegisterCodec + DefaultGenesisState = types.DefaultGenesisState + ValidateGenesis = types.ValidateGenesis + InitialFeePool = types.InitialFeePool ) diff --git a/x/distribution/client/cli/tx.go b/x/distribution/client/cli/tx.go index 2e5cac6a611f..67113c2b92b3 100644 --- a/x/distribution/client/cli/tx.go +++ b/x/distribution/client/cli/tx.go @@ -43,7 +43,7 @@ func GetTxCmd(storeKey string, cdc *amino.Codec) *cobra.Command { func GetCmdWithdrawRewards(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ Use: "withdraw-rewards", - Short: "withdraw rewards for either: all-delegations, a delegation, or a validator", + Short: "withdraw rewards for either a delegation or a validator", Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { @@ -68,8 +68,8 @@ func GetCmdWithdrawRewards(cdc *codec.Codec) *cobra.Command { return err } valAddr := sdk.ValAddress(addr.Bytes()) - msg = types.NewMsgWithdrawValidatorRewardsAll(valAddr) - case onlyFromVal != "": + msg = types.NewMsgWithdrawValidatorCommission(valAddr) + default: delAddr, err := cliCtx.GetFromAddress() if err != nil { return err @@ -81,12 +81,6 @@ func GetCmdWithdrawRewards(cdc *codec.Codec) *cobra.Command { } msg = types.NewMsgWithdrawDelegatorReward(delAddr, valAddr) - default: - delAddr, err := cliCtx.GetFromAddress() - if err != nil { - return err - } - msg = types.NewMsgWithdrawDelegatorRewardsAll(delAddr) } if cliCtx.GenerateOnly { diff --git a/x/distribution/genesis.go b/x/distribution/genesis.go index 34680891629b..cfebd03816ee 100644 --- a/x/distribution/genesis.go +++ b/x/distribution/genesis.go @@ -11,30 +11,97 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) { keeper.SetCommunityTax(ctx, data.CommunityTax) keeper.SetBaseProposerReward(ctx, data.BaseProposerReward) keeper.SetBonusProposerReward(ctx, data.BonusProposerReward) - - for _, vdi := range data.ValidatorDistInfos { - keeper.SetValidatorDistInfo(ctx, vdi) + for _, dwi := range data.DelegatorWithdrawInfos { + keeper.SetDelegatorWithdrawAddr(ctx, dwi.DelegatorAddr, dwi.WithdrawAddr) } - for _, ddi := range data.DelegationDistInfos { - keeper.SetDelegationDistInfo(ctx, ddi) + keeper.SetPreviousProposerConsAddr(ctx, data.PreviousProposer) + keeper.SetOutstandingRewards(ctx, data.OutstandingRewards) + for _, acc := range data.ValidatorAccumulatedCommissions { + keeper.SetValidatorAccumulatedCommission(ctx, acc.ValidatorAddr, acc.Accumulated) } - for _, dw := range data.DelegatorWithdrawInfos { - keeper.SetDelegatorWithdrawAddr(ctx, dw.DelegatorAddr, dw.WithdrawAddr) + for _, his := range data.ValidatorHistoricalRewards { + keeper.SetValidatorHistoricalRewards(ctx, his.ValidatorAddr, his.Period, his.Rewards) + } + for _, cur := range data.ValidatorCurrentRewards { + keeper.SetValidatorCurrentRewards(ctx, cur.ValidatorAddr, cur.Rewards) + } + for _, del := range data.DelegatorStartingInfos { + keeper.SetDelegatorStartingInfo(ctx, del.ValidatorAddr, del.DelegatorAddr, del.StartingInfo) + } + for _, evt := range data.ValidatorSlashEvents { + keeper.SetValidatorSlashEvent(ctx, evt.ValidatorAddr, evt.Height, evt.Event) } - keeper.SetPreviousProposerConsAddr(ctx, data.PreviousProposer) } -// ExportGenesis returns a GenesisState for a given context and keeper. The -// GenesisState will contain the pool, and validator/delegator distribution info's +// ExportGenesis returns a GenesisState for a given context and keeper. func ExportGenesis(ctx sdk.Context, keeper Keeper) types.GenesisState { feePool := keeper.GetFeePool(ctx) communityTax := keeper.GetCommunityTax(ctx) baseProposerRewards := keeper.GetBaseProposerReward(ctx) bonusProposerRewards := keeper.GetBonusProposerReward(ctx) - vdis := keeper.GetAllValidatorDistInfos(ctx) - ddis := keeper.GetAllDelegationDistInfos(ctx) - dwis := keeper.GetAllDelegatorWithdrawInfos(ctx) + dwi := make([]types.DelegatorWithdrawInfo, 0) + keeper.IterateDelegatorWithdrawAddrs(ctx, func(del sdk.AccAddress, addr sdk.AccAddress) (stop bool) { + dwi = append(dwi, types.DelegatorWithdrawInfo{ + DelegatorAddr: del, + WithdrawAddr: addr, + }) + return false + }) pp := keeper.GetPreviousProposerConsAddr(ctx) - return NewGenesisState(feePool, communityTax, baseProposerRewards, - bonusProposerRewards, vdis, ddis, dwis, pp) + outstanding := keeper.GetOutstandingRewards(ctx) + acc := make([]types.ValidatorAccumulatedCommissionRecord, 0) + keeper.IterateValidatorAccumulatedCommissions(ctx, + func(addr sdk.ValAddress, commission types.ValidatorAccumulatedCommission) (stop bool) { + acc = append(acc, types.ValidatorAccumulatedCommissionRecord{ + ValidatorAddr: addr, + Accumulated: commission, + }) + return false + }, + ) + his := make([]types.ValidatorHistoricalRewardsRecord, 0) + keeper.IterateValidatorHistoricalRewards(ctx, + func(val sdk.ValAddress, period uint64, rewards types.ValidatorHistoricalRewards) (stop bool) { + his = append(his, types.ValidatorHistoricalRewardsRecord{ + ValidatorAddr: val, + Period: period, + Rewards: rewards, + }) + return false + }, + ) + cur := make([]types.ValidatorCurrentRewardsRecord, 0) + keeper.IterateValidatorCurrentRewards(ctx, + func(val sdk.ValAddress, rewards types.ValidatorCurrentRewards) (stop bool) { + cur = append(cur, types.ValidatorCurrentRewardsRecord{ + ValidatorAddr: val, + Rewards: rewards, + }) + return false + }, + ) + dels := make([]types.DelegatorStartingInfoRecord, 0) + keeper.IterateDelegatorStartingInfos(ctx, + func(val sdk.ValAddress, del sdk.AccAddress, info types.DelegatorStartingInfo) (stop bool) { + dels = append(dels, types.DelegatorStartingInfoRecord{ + ValidatorAddr: val, + DelegatorAddr: del, + StartingInfo: info, + }) + return false + }, + ) + slashes := make([]types.ValidatorSlashEventRecord, 0) + keeper.IterateValidatorSlashEvents(ctx, + func(val sdk.ValAddress, height uint64, event types.ValidatorSlashEvent) (stop bool) { + slashes = append(slashes, types.ValidatorSlashEventRecord{ + ValidatorAddr: val, + Height: height, + Event: event, + }) + return false + }, + ) + return types.NewGenesisState(feePool, communityTax, baseProposerRewards, bonusProposerRewards, dwi, pp, outstanding, + acc, his, cur, dels, slashes) } diff --git a/x/distribution/handler.go b/x/distribution/handler.go index acd93d26a7a1..5b29cb8456ea 100644 --- a/x/distribution/handler.go +++ b/x/distribution/handler.go @@ -13,36 +13,21 @@ func NewHandler(k keeper.Keeper) sdk.Handler { switch msg := msg.(type) { case types.MsgSetWithdrawAddress: return handleMsgModifyWithdrawAddress(ctx, msg, k) - case types.MsgWithdrawDelegatorRewardsAll: - return handleMsgWithdrawDelegatorRewardsAll(ctx, msg, k) case types.MsgWithdrawDelegatorReward: return handleMsgWithdrawDelegatorReward(ctx, msg, k) - case types.MsgWithdrawValidatorRewardsAll: - return handleMsgWithdrawValidatorRewardsAll(ctx, msg, k) + case types.MsgWithdrawValidatorCommission: + return handleMsgWithdrawValidatorCommission(ctx, msg, k) default: return sdk.ErrTxDecode("invalid message parse in distribution module").Result() } } } -//_____________________________________________________________________ - -// These functions assume everything has been authenticated, -// now we just perform action and save +// These functions assume everything has been authenticated (ValidateBasic passed, and signatures checked) func handleMsgModifyWithdrawAddress(ctx sdk.Context, msg types.MsgSetWithdrawAddress, k keeper.Keeper) sdk.Result { - k.SetDelegatorWithdrawAddr(ctx, msg.DelegatorAddr, msg.WithdrawAddr) - tags := sdk.NewTags( - tags.Delegator, []byte(msg.DelegatorAddr.String()), - ) - return sdk.Result{ - Tags: tags, - } -} - -func handleMsgWithdrawDelegatorRewardsAll(ctx sdk.Context, msg types.MsgWithdrawDelegatorRewardsAll, k keeper.Keeper) sdk.Result { - k.WithdrawDelegationRewardsAll(ctx, msg.DelegatorAddr) + k.SetDelegatorWithdrawAddr(ctx, msg.DelegatorAddr, msg.WithdrawAddr) tags := sdk.NewTags( tags.Delegator, []byte(msg.DelegatorAddr.String()), @@ -53,7 +38,8 @@ func handleMsgWithdrawDelegatorRewardsAll(ctx sdk.Context, msg types.MsgWithdraw } func handleMsgWithdrawDelegatorReward(ctx sdk.Context, msg types.MsgWithdrawDelegatorReward, k keeper.Keeper) sdk.Result { - err := k.WithdrawDelegationReward(ctx, msg.DelegatorAddr, msg.ValidatorAddr) + + err := k.WithdrawDelegationRewards(ctx, msg.DelegatorAddr, msg.ValidatorAddr) if err != nil { return err.Result() } @@ -67,8 +53,9 @@ func handleMsgWithdrawDelegatorReward(ctx sdk.Context, msg types.MsgWithdrawDele } } -func handleMsgWithdrawValidatorRewardsAll(ctx sdk.Context, msg types.MsgWithdrawValidatorRewardsAll, k keeper.Keeper) sdk.Result { - err := k.WithdrawValidatorRewardsAll(ctx, msg.ValidatorAddr) +func handleMsgWithdrawValidatorCommission(ctx sdk.Context, msg types.MsgWithdrawValidatorCommission, k keeper.Keeper) sdk.Result { + + err := k.WithdrawValidatorCommission(ctx, msg.ValidatorAddr) if err != nil { return err.Result() } diff --git a/x/distribution/keeper/allocation.go b/x/distribution/keeper/allocation.go index 4468f7d3c4fc..f43b018ff453 100644 --- a/x/distribution/keeper/allocation.go +++ b/x/distribution/keeper/allocation.go @@ -1,57 +1,85 @@ package keeper import ( + abci "github.com/tendermint/tendermint/abci/types" + sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/distribution/types" ) -// Allocate fees handles distribution of the collected fees -func (k Keeper) AllocateTokens(ctx sdk.Context, percentVotes sdk.Dec, proposer sdk.ConsAddress) { - - // get the proposer of this block - proposerValidator := k.stakingKeeper.ValidatorByConsAddr(ctx, proposer) +// allocate fees handles distribution of the collected fees +func (k Keeper) AllocateTokens(ctx sdk.Context, sumPrecommitPower, totalPower int64, proposer sdk.ConsAddress, votes []abci.VoteInfo) { - proposerDist := k.GetValidatorDistInfo(ctx, proposerValidator.GetOperator()) + // fetch collected fees & fee pool + feesCollectedInt := k.feeCollectionKeeper.GetCollectedFees(ctx) + feesCollected := sdk.NewDecCoins(feesCollectedInt) + feePool := k.GetFeePool(ctx) - // get the fees which have been getting collected through all the - // transactions in the block - feesCollected := k.feeCollectionKeeper.GetCollectedFees(ctx) - feesCollectedDec := types.NewDecCoins(feesCollected) + // clear collected fees, which will now be distributed + k.feeCollectionKeeper.ClearCollectedFees(ctx) - feePool := k.GetFeePool(ctx) - // Temporary workaround to keep CanWithdrawInvariant happy. - // General discussions here: https://github.com/cosmos/cosmos-sdk/issues/2906#issuecomment-441867634 - if k.stakingKeeper.GetLastTotalPower(ctx).IsZero() { - feePool.CommunityPool = feePool.CommunityPool.Plus(feesCollectedDec) + // temporary workaround to keep CanWithdrawInvariant happy + // general discussions here: https://github.com/cosmos/cosmos-sdk/issues/2906#issuecomment-441867634 + if totalPower == 0 { + feePool.CommunityPool = feePool.CommunityPool.Plus(feesCollected) k.SetFeePool(ctx, feePool) - k.feeCollectionKeeper.ClearCollectedFees(ctx) return } - // allocated rewards to proposer + // calculate fraction votes + fractionVotes := sdk.NewDec(sumPrecommitPower).Quo(sdk.NewDec(totalPower)) + + // calculate proposer reward baseProposerReward := k.GetBaseProposerReward(ctx) bonusProposerReward := k.GetBonusProposerReward(ctx) - proposerMultiplier := baseProposerReward.Add(bonusProposerReward.Mul(percentVotes)) - proposerReward := feesCollectedDec.MulDec(proposerMultiplier) + proposerMultiplier := baseProposerReward.Add(bonusProposerReward.Mul(fractionVotes)) + proposerReward := feesCollected.MulDec(proposerMultiplier) - // apply commission - commission := proposerReward.MulDec(proposerValidator.GetCommission()) - remaining := proposerReward.Minus(commission) - proposerDist.ValCommission = proposerDist.ValCommission.Plus(commission) - proposerDist.DelPool = proposerDist.DelPool.Plus(remaining) + // pay proposer + proposerValidator := k.stakingKeeper.ValidatorByConsAddr(ctx, proposer) + k.AllocateTokensToValidator(ctx, proposerValidator, proposerReward) + remaining := feesCollected.Minus(proposerReward) - // allocate community funding + // calculate fraction allocated to validators communityTax := k.GetCommunityTax(ctx) - communityFunding := feesCollectedDec.MulDec(communityTax) - feePool.CommunityPool = feePool.CommunityPool.Plus(communityFunding) + voteMultiplier := sdk.OneDec().Sub(proposerMultiplier).Sub(communityTax) - // set the global pool within the distribution module - poolReceived := feesCollectedDec.Minus(proposerReward).Minus(communityFunding) - feePool.ValPool = feePool.ValPool.Plus(poolReceived) + // allocate tokens proportionally to voting power + // TODO consider parallelizing later, ref https://github.com/cosmos/cosmos-sdk/pull/3099#discussion_r246276376 + for _, vote := range votes { + validator := k.stakingKeeper.ValidatorByConsAddr(ctx, vote.Validator.Address) - k.SetValidatorDistInfo(ctx, proposerDist) + // TODO likely we should only reward validators who actually signed the block. + // ref https://github.com/cosmos/cosmos-sdk/issues/2525#issuecomment-430838701 + powerFraction := sdk.NewDec(vote.Validator.Power).Quo(sdk.NewDec(totalPower)) + reward := feesCollected.MulDec(voteMultiplier).MulDec(powerFraction) + k.AllocateTokensToValidator(ctx, validator, reward) + remaining = remaining.Minus(reward) + } + + // allocate community funding + feePool.CommunityPool = feePool.CommunityPool.Plus(remaining) k.SetFeePool(ctx, feePool) - // clear the now distributed fees - k.feeCollectionKeeper.ClearCollectedFees(ctx) + // update outstanding rewards + outstanding := k.GetOutstandingRewards(ctx) + outstanding = outstanding.Plus(feesCollected.Minus(remaining)) + k.SetOutstandingRewards(ctx, outstanding) + +} + +// allocate tokens to a particular validator, splitting according to commission +func (k Keeper) AllocateTokensToValidator(ctx sdk.Context, val sdk.Validator, tokens sdk.DecCoins) { + // split tokens between validator and delegators according to commission + commission := tokens.MulDec(val.GetCommission()) + shared := tokens.Minus(commission) + + // update current commission + currentCommission := k.GetValidatorAccumulatedCommission(ctx, val.GetOperator()) + currentCommission = currentCommission.Plus(commission) + k.SetValidatorAccumulatedCommission(ctx, val.GetOperator(), currentCommission) + + // update current rewards + currentRewards := k.GetValidatorCurrentRewards(ctx, val.GetOperator()) + currentRewards.Rewards = currentRewards.Rewards.Plus(shared) + k.SetValidatorCurrentRewards(ctx, val.GetOperator(), currentRewards) } diff --git a/x/distribution/keeper/allocation_test.go b/x/distribution/keeper/allocation_test.go index dae7beee79ef..704e37de54fe 100644 --- a/x/distribution/keeper/allocation_test.go +++ b/x/distribution/keeper/allocation_test.go @@ -3,109 +3,106 @@ package keeper import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/staking" ) -func TestAllocateTokensBasic(t *testing.T) { - - // no community tax on inputs - ctx, _, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - totalPower := int64(10) - totalPowerInt := sdk.NewInt(totalPower) - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, totalPower) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // verify everything has been set in staking correctly - validator, found := sk.GetValidator(ctx, valOpAddr1) - require.True(t, found) - require.Equal(t, sdk.Bonded, validator.Status) - assert.True(sdk.IntEq(t, totalPowerInt, validator.Tokens)) - assert.True(sdk.DecEq(t, sdk.NewDec(totalPower), validator.DelegatorShares)) - bondedTokens := sk.TotalPower(ctx) - assert.True(sdk.IntEq(t, totalPowerInt, bondedTokens)) - - // initial fee pool should be empty - feePool := keeper.GetFeePool(ctx) - require.Nil(t, feePool.ValPool) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // verify that these fees have been received by the feePool - percentProposer := sdk.NewDecWithPrec(5, 2) - percentRemaining := sdk.OneDec().Sub(percentProposer) - feePool = keeper.GetFeePool(ctx) - expRes := sdk.NewDecFromInt(feeInputs).Mul(percentRemaining) - require.Equal(t, 1, len(feePool.ValPool)) - require.True(sdk.DecEq(t, expRes, feePool.ValPool[0].Amount)) +func TestAllocateTokensToValidatorWithCommission(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + val := sk.Validator(ctx, valOpAddr1) + + // allocate tokens + tokens := sdk.DecCoins{ + {staking.DefaultBondDenom, sdk.NewDec(10)}, + } + k.AllocateTokensToValidator(ctx, val, tokens) + + // check commission + expected := sdk.DecCoins{ + {staking.DefaultBondDenom, sdk.NewDec(5)}, + } + require.Equal(t, expected, k.GetValidatorAccumulatedCommission(ctx, val.GetOperator())) + + // check current rewards + require.Equal(t, expected, k.GetValidatorCurrentRewards(ctx, val.GetOperator()).Rewards) } -func TestAllocateTokensWithCommunityTax(t *testing.T) { - communityTax := sdk.NewDecWithPrec(1, 2) //1% - ctx, _, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, communityTax) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - totalPower := int64(10) - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, totalPower) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // verify that these fees have been received by the feePool - feePool := keeper.GetFeePool(ctx) - // 5% goes to proposer, 1% community tax - percentProposer := sdk.NewDecWithPrec(5, 2) - percentRemaining := sdk.OneDec().Sub(communityTax.Add(percentProposer)) - expRes := sdk.NewDecFromInt(feeInputs).Mul(percentRemaining) - require.Equal(t, 1, len(feePool.ValPool)) - require.True(sdk.DecEq(t, expRes, feePool.ValPool[0].Amount)) -} - -func TestAllocateTokensWithPartialPrecommitPower(t *testing.T) { - communityTax := sdk.NewDecWithPrec(1, 2) - ctx, _, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, communityTax) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - totalPower := int64(100) - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, totalPower) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - percentPrecommitVotes := sdk.NewDecWithPrec(25, 2) - keeper.AllocateTokens(ctx, percentPrecommitVotes, valConsAddr1) - - // verify that these fees have been received by the feePool - feePool := keeper.GetFeePool(ctx) - // 1% + 4%*0.25 to proposer + 1% community tax = 97% - percentProposer := sdk.NewDecWithPrec(1, 2).Add(sdk.NewDecWithPrec(4, 2).Mul(percentPrecommitVotes)) - percentRemaining := sdk.OneDec().Sub(communityTax.Add(percentProposer)) - expRes := sdk.NewDecFromInt(feeInputs).Mul(percentRemaining) - require.Equal(t, 1, len(feePool.ValPool)) - require.True(sdk.DecEq(t, expRes, feePool.ValPool[0].Amount)) +func TestAllocateTokensToManyValidators(t *testing.T) { + ctx, _, k, sk, fck := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // create second validator with 0% commission + commission = staking.NewCommissionMsg(sdk.NewDec(0), sdk.NewDec(0), sdk.NewDec(0)) + msg = staking.NewMsgCreateValidator(valOpAddr2, valConsPk2, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + abciValA := abci.Validator{ + Address: valConsPk1.Address(), + Power: 100, + } + abciValB := abci.Validator{ + Address: valConsPk2.Address(), + Power: 100, + } + + // assert initial state: zero outstanding rewards, zero community pool, zero commission, zero current rewards + require.True(t, k.GetOutstandingRewards(ctx).IsZero()) + require.True(t, k.GetFeePool(ctx).CommunityPool.IsZero()) + require.True(t, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1).IsZero()) + require.True(t, k.GetValidatorAccumulatedCommission(ctx, valOpAddr2).IsZero()) + require.True(t, k.GetValidatorCurrentRewards(ctx, valOpAddr1).Rewards.IsZero()) + require.True(t, k.GetValidatorCurrentRewards(ctx, valOpAddr2).Rewards.IsZero()) + + // allocate tokens as if both had voted and second was proposer + fees := sdk.Coins{ + {staking.DefaultBondDenom, sdk.NewInt(100)}, + } + fck.SetCollectedFees(fees) + votes := []abci.VoteInfo{ + { + Validator: abciValA, + SignedLastBlock: true, + }, + { + Validator: abciValB, + SignedLastBlock: true, + }, + } + k.AllocateTokens(ctx, 200, 200, valConsAddr2, votes) + + // 98 outstanding rewards (100 less 2 to community pool) + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(98)}}, k.GetOutstandingRewards(ctx)) + // 2 community pool coins + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(2)}}, k.GetFeePool(ctx).CommunityPool) + // 50% commission for first proposer, (0.5 * 93%) * 100 / 2 = 23.25 + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDecWithPrec(2325, 2)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) + // zero commission for second proposer + require.True(t, k.GetValidatorAccumulatedCommission(ctx, valOpAddr2).IsZero()) + // just staking.proportional for first proposer less commission = (0.5 * 93%) * 100 / 2 = 23.25 + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDecWithPrec(2325, 2)}}, k.GetValidatorCurrentRewards(ctx, valOpAddr1).Rewards) + // proposer reward + staking.proportional for second proposer = (5 % + 0.5 * (93%)) * 100 = 51.5 + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDecWithPrec(515, 1)}}, k.GetValidatorCurrentRewards(ctx, valOpAddr2).Rewards) } diff --git a/x/distribution/keeper/delegation.go b/x/distribution/keeper/delegation.go index 93e068cec831..2d8e21ce83ab 100644 --- a/x/distribution/keeper/delegation.go +++ b/x/distribution/keeper/delegation.go @@ -2,229 +2,88 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" ) -// check whether a delegator distribution info exists -func (k Keeper) HasDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress, - valOperatorAddr sdk.ValAddress) (has bool) { - store := ctx.KVStore(k.storeKey) - return store.Has(GetDelegationDistInfoKey(delAddr, valOperatorAddr)) -} - -// get the delegator distribution info -func (k Keeper) GetDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress, - valOperatorAddr sdk.ValAddress) (ddi types.DelegationDistInfo) { - - store := ctx.KVStore(k.storeKey) - - b := store.Get(GetDelegationDistInfoKey(delAddr, valOperatorAddr)) - if b == nil { - panic("Stored delegation-distribution info should not have been nil") +// initialize starting info for a new delegation +func (k Keeper) initializeDelegation(ctx sdk.Context, val sdk.ValAddress, del sdk.AccAddress) { + // period has already been incremented + period := k.GetValidatorCurrentRewards(ctx, val).Period + validator := k.stakingKeeper.Validator(ctx, val) + delegation := k.stakingKeeper.Delegation(ctx, del, val) + // calculate delegation stake in tokens + // we don't store directly, so multiply delegation shares * (tokens per share) + stake := delegation.GetShares().Mul(validator.GetDelegatorShareExRate()) + k.SetDelegatorStartingInfo(ctx, val, del, types.NewDelegatorStartingInfo(period-1, stake, uint64(ctx.BlockHeight()))) +} + +// calculate the rewards accrued by a delegation between two periods +func (k Keeper) calculateDelegationRewardsBetween(ctx sdk.Context, val sdk.Validator, + startingPeriod, endingPeriod uint64, staking sdk.Dec) (rewards sdk.DecCoins) { + // sanity check + if startingPeriod > endingPeriod { + panic("startingPeriod cannot be greater than endingPeriod") } - k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &ddi) + // return staking * (ending - starting) + starting := k.GetValidatorHistoricalRewards(ctx, val.GetOperator(), startingPeriod) + ending := k.GetValidatorHistoricalRewards(ctx, val.GetOperator(), endingPeriod) + difference := ending.Minus(starting) + rewards = difference.MulDec(staking) return } -// set the delegator distribution info -func (k Keeper) SetDelegationDistInfo(ctx sdk.Context, ddi types.DelegationDistInfo) { - store := ctx.KVStore(k.storeKey) - b := k.cdc.MustMarshalBinaryLengthPrefixed(ddi) - store.Set(GetDelegationDistInfoKey(ddi.DelegatorAddr, ddi.ValOperatorAddr), b) -} - -// remove a delegator distribution info -func (k Keeper) RemoveDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress, - valOperatorAddr sdk.ValAddress) { - - store := ctx.KVStore(k.storeKey) - store.Delete(GetDelegationDistInfoKey(delAddr, valOperatorAddr)) -} - -// remove all delegation distribution infos -func (k Keeper) RemoveDelegationDistInfos(ctx sdk.Context) { - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - store.Delete(iter.Key()) - } -} - -// iterate over all the validator distribution infos -func (k Keeper) IterateDelegationDistInfos(ctx sdk.Context, - fn func(index int64, distInfo types.DelegationDistInfo) (stop bool)) { - - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) - defer iter.Close() - index := int64(0) - for ; iter.Valid(); iter.Next() { - var ddi types.DelegationDistInfo - k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &ddi) - if fn(index, ddi) { - return - } - index++ +// calculate the total rewards accrued by a delegation +func (k Keeper) calculateDelegationRewards(ctx sdk.Context, val sdk.Validator, del sdk.Delegation, endingPeriod uint64) (rewards sdk.DecCoins) { + // fetch starting info for delegation + startingInfo := k.GetDelegatorStartingInfo(ctx, del.GetValidatorAddr(), del.GetDelegatorAddr()) + startingPeriod := startingInfo.PreviousPeriod + stake := startingInfo.Stake + + // iterate through slashes and withdraw with calculated staking for sub-intervals + // these offsets are dependent on *when* slashes happen - namely, in BeginBlock, after rewards are allocated... + // ... so we don't reduce stake for slashes which happened in the *first* block, because the delegation wouldn't have existed + startingHeight := startingInfo.Height + 1 + // ... or slashes which happened in *this* block, since they would have happened after reward allocation + endingHeight := uint64(ctx.BlockHeight()) - 1 + if endingHeight >= startingHeight { + k.IterateValidatorSlashEventsBetween(ctx, del.GetValidatorAddr(), startingHeight, endingHeight, + func(height uint64, event types.ValidatorSlashEvent) (stop bool) { + endingPeriod := event.ValidatorPeriod - 1 + rewards = rewards.Plus(k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)) + stake = stake.Mul(sdk.OneDec().Sub(event.Fraction)) + startingPeriod = endingPeriod + return false + }, + ) } -} - -//___________________________________________________________________________________________ - -// get the delegator withdraw address, return the delegator address if not set -func (k Keeper) GetDelegatorWithdrawAddr(ctx sdk.Context, delAddr sdk.AccAddress) sdk.AccAddress { - store := ctx.KVStore(k.storeKey) - - b := store.Get(GetDelegatorWithdrawAddrKey(delAddr)) - if b == nil { - return delAddr - } - return sdk.AccAddress(b) -} - -// set the delegator withdraw address -func (k Keeper) SetDelegatorWithdrawAddr(ctx sdk.Context, delAddr, withdrawAddr sdk.AccAddress) { - store := ctx.KVStore(k.storeKey) - store.Set(GetDelegatorWithdrawAddrKey(delAddr), withdrawAddr.Bytes()) -} - -// remove a delegator withdraw info -func (k Keeper) RemoveDelegatorWithdrawAddr(ctx sdk.Context, delAddr, withdrawAddr sdk.AccAddress) { - store := ctx.KVStore(k.storeKey) - store.Delete(GetDelegatorWithdrawAddrKey(delAddr)) -} - -//___________________________________________________________________________________________ - -// return all rewards for a delegation -func (k Keeper) withdrawDelegationReward(ctx sdk.Context, - delAddr sdk.AccAddress, valAddr sdk.ValAddress) (types.FeePool, - types.ValidatorDistInfo, types.DelegationDistInfo, types.DecCoins) { - - wc := k.GetWithdrawContext(ctx, valAddr) - valInfo := k.GetValidatorDistInfo(ctx, valAddr) - delInfo := k.GetDelegationDistInfo(ctx, delAddr, valAddr) - validator := k.stakingKeeper.Validator(ctx, valAddr) - delegation := k.stakingKeeper.Delegation(ctx, delAddr, valAddr) - - delInfo, valInfo, feePool, withdraw := delInfo.WithdrawRewards(wc, valInfo, - validator.GetDelegatorShares(), delegation.GetShares()) - - return feePool, valInfo, delInfo, withdraw -} - -// get all rewards for all delegations of a delegator -func (k Keeper) currentDelegationReward(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) types.DecCoins { - wc := k.GetWithdrawContext(ctx, valAddr) + // calculate rewards for final period + rewards = rewards.Plus(k.calculateDelegationRewardsBetween(ctx, val, startingPeriod, endingPeriod, stake)) - valInfo := k.GetValidatorDistInfo(ctx, valAddr) - delInfo := k.GetDelegationDistInfo(ctx, delAddr, valAddr) - validator := k.stakingKeeper.Validator(ctx, valAddr) - delegation := k.stakingKeeper.Delegation(ctx, delAddr, valAddr) - - estimation := delInfo.CurrentRewards(wc, valInfo, - validator.GetDelegatorShares(), delegation.GetShares()) - - return estimation + return } -//___________________________________________________________________________________________ +func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val sdk.Validator, del sdk.Delegation) sdk.Error { -// withdraw all rewards for a single delegation -// NOTE: This gets called "onDelegationSharesModified", -// meaning any changes to bonded coins -func (k Keeper) WithdrawToDelegator(ctx sdk.Context, feePool types.FeePool, - delAddr sdk.AccAddress, amount types.DecCoins) { + // end current period and calculate rewards + endingPeriod := k.incrementValidatorPeriod(ctx, val) + rewards := k.calculateDelegationRewards(ctx, val, del, endingPeriod) - withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, delAddr) - coinsToAdd, change := amount.TruncateDecimal() - feePool.CommunityPool = feePool.CommunityPool.Plus(change) + // truncate coins, return remainder to community pool + coins, remainder := rewards.TruncateDecimal() + outstanding := k.GetOutstandingRewards(ctx) + k.SetOutstandingRewards(ctx, outstanding.Minus(rewards)) + feePool := k.GetFeePool(ctx) + feePool.CommunityPool = feePool.CommunityPool.Plus(remainder) k.SetFeePool(ctx, feePool) - _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coinsToAdd) - if err != nil { - panic(err) - } -} - -//___________________________________________________________________________________________ - -// withdraw all rewards for a single delegation -// NOTE: This gets called "onDelegationSharesModified", -// meaning any changes to bonded coins -func (k Keeper) WithdrawDelegationReward(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) sdk.Error { - if !k.HasDelegationDistInfo(ctx, delAddr, valAddr) { - return types.ErrNoDelegationDistInfo(k.codespace) + // add coins to user account + withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr()) + if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil { + return err } - feePool, valInfo, delInfo, withdraw := k.withdrawDelegationReward(ctx, delAddr, valAddr) - - k.SetValidatorDistInfo(ctx, valInfo) - k.SetDelegationDistInfo(ctx, delInfo) - k.WithdrawToDelegator(ctx, feePool, delAddr, withdraw) - return nil } - -// current rewards for a single delegation -func (k Keeper) CurrentDelegationReward(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) (sdk.Coins, sdk.Error) { - - if !k.HasDelegationDistInfo(ctx, delAddr, valAddr) { - return sdk.Coins{}, types.ErrNoDelegationDistInfo(k.codespace) - } - estCoins := k.currentDelegationReward(ctx, delAddr, valAddr) - trucate, _ := estCoins.TruncateDecimal() - return trucate, nil -} - -//___________________________________________________________________________________________ - -// return all rewards for all delegations of a delegator -func (k Keeper) WithdrawDelegationRewardsAll(ctx sdk.Context, delAddr sdk.AccAddress) { - withdraw := k.withdrawDelegationRewardsAll(ctx, delAddr) - feePool := k.GetFeePool(ctx) - k.WithdrawToDelegator(ctx, feePool, delAddr, withdraw) -} - -func (k Keeper) withdrawDelegationRewardsAll(ctx sdk.Context, - delAddr sdk.AccAddress) types.DecCoins { - - withdraw := types.DecCoins{} - - // iterate over all the delegations - operationAtDelegation := func(_ int64, del sdk.Delegation) (stop bool) { - valAddr := del.GetValidatorAddr() - feePool, valInfo, delInfo, diWithdraw := k.withdrawDelegationReward(ctx, delAddr, valAddr) - withdraw = withdraw.Plus(diWithdraw) - - k.SetFeePool(ctx, feePool) - k.SetValidatorDistInfo(ctx, valInfo) - k.SetDelegationDistInfo(ctx, delInfo) - - return false - } - - k.stakingKeeper.IterateDelegations(ctx, delAddr, operationAtDelegation) - return withdraw -} - -// get all rewards for all delegations of a delegator -func (k Keeper) CurrentDelegationRewardsAll(ctx sdk.Context, - delAddr sdk.AccAddress) types.DecCoins { - - // iterate over all the delegations - total := types.DecCoins{} - operationAtDelegation := func(_ int64, del sdk.Delegation) (stop bool) { - valAddr := del.GetValidatorAddr() - est := k.currentDelegationReward(ctx, delAddr, valAddr) - total = total.Plus(est) - return false - } - k.stakingKeeper.IterateDelegations(ctx, delAddr, operationAtDelegation) - return total -} diff --git a/x/distribution/keeper/delegation_test.go b/x/distribution/keeper/delegation_test.go index 114ec1b633b1..0ebe068ba9b7 100644 --- a/x/distribution/keeper/delegation_test.go +++ b/x/distribution/keeper/delegation_test.go @@ -9,246 +9,530 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking" ) -func TestWithdrawDelegationRewardBasic(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, 10) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw delegation - ctx = ctx.WithBlockHeight(1) - sk.SetLastTotalPower(ctx, sdk.NewInt(10)) - sk.SetLastValidatorPower(ctx, valOpAddr1, sdk.NewInt(10)) - keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - expRes := sdk.NewDec(90).Add(sdk.NewDec(100).Quo(sdk.NewDec(2))).TruncateInt() // 90 + 100 tokens * 10/20 - require.True(sdk.IntEq(t, expRes, amt)) +func TestCalculateRewardsBasic(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) } -func TestWithdrawDelegationRewardWithCommission(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator with 10% commission - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.NewDecWithPrec(1, 1)) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw delegation - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - expRes := sdk.NewDec(90).Add(sdk.NewDec(90).Quo(sdk.NewDec(2))).TruncateInt() // 90 + 100*90% tokens * 10/20 - require.True(sdk.IntEq(t, expRes, amt)) +func TestCalculateRewardsAfterSlash(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // slash the validator by 50% + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 100, sdk.NewDecWithPrec(5, 1)) + + // retrieve validator + val = sk.Validator(ctx, valOpAddr1) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) } -func TestWithdrawDelegationRewardTwoDelegators(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator with 10% commission - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.NewDecWithPrec(1, 1)) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - msgDelegate = staking.NewTestMsgDelegate(delAddr2, valOpAddr1, 20) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt = accMapper.GetAccount(ctx, delAddr2).GetCoins().AmountOf(denom) - require.Equal(t, int64(80), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // delegator 1 withdraw delegation - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - expRes := sdk.NewDec(90).Add(sdk.NewDec(90).Quo(sdk.NewDec(4))).TruncateInt() // 90 + 100*90% tokens * 10/40 - require.True(sdk.IntEq(t, expRes, amt)) +func TestCalculateRewardsAfterManySlashes(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // slash the validator by 50% + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 100, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = sk.Validator(ctx, valOpAddr1) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator by 50% again + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 50, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = sk.Validator(ctx, valOpAddr1) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) } -// this test demonstrates how two delegators with the same power can end up -// with different rewards in the end -func TestWithdrawDelegationRewardTwoDelegatorsUneven(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator with no commission - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.ZeroDec()) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - msgDelegate = staking.NewTestMsgDelegate(delAddr2, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt = accMapper.GetAccount(ctx, delAddr2).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(90) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - ctx = ctx.WithBlockHeight(1) - - // delegator 1 withdraw delegation early, delegator 2 just keeps it's accum - keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - expRes1 := sdk.NewDec(90).Add(sdk.NewDec(90).Quo(sdk.NewDec(3))).TruncateInt() // 90 + 100 * 10/30 - require.True(sdk.IntEq(t, expRes1, amt)) - - // allocate 200 denom of fees - feeInputs = sdk.NewInt(180) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - ctx = ctx.WithBlockHeight(2) - - // delegator 2 now withdraws everything it's entitled to - keeper.WithdrawDelegationReward(ctx, delAddr2, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr2).GetCoins().AmountOf(denom) - // existingTokens + (100+200 * (10/(20+30)) - withdrawnFromVal := sdk.NewDec(60 + 180).Mul(sdk.NewDec(2)).Quo(sdk.NewDec(5)) - expRes2 := sdk.NewDec(90).Add(withdrawnFromVal).TruncateInt() - require.True(sdk.IntEq(t, expRes2, amt)) - - // finally delegator 1 withdraws the remainder of its reward - keeper.WithdrawDelegationReward(ctx, delAddr1, valOpAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - remainingInVal := sdk.NewDec(60 + 180).Sub(withdrawnFromVal) - expRes3 := sdk.NewDecFromInt(expRes1).Add(remainingInVal.Mul(sdk.NewDec(1)).Quo(sdk.NewDec(3))).TruncateInt() - require.True(sdk.IntEq(t, expRes3, amt)) - - // verify the final withdraw amounts are different - require.True(t, expRes2.GT(expRes3)) +func TestCalculateRewardsMultiDelegator(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del1 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // allocate some rewards + initial := int64(20) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // second delegation + msg2 := staking.NewMsgDelegate(sdk.AccAddress(valOpAddr2), valOpAddr1, sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100))) + require.True(t, sh(ctx, msg2).IsOK()) + del2 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr2), valOpAddr1) + + // fetch updated validator + val = sk.Validator(ctx, valOpAddr1) + + // end block + staking.EndBlocker(ctx, sk) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := k.calculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 3/4 initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial * 3 / 4)}}, rewards) + + // calculate delegation rewards for del2 + rewards = k.calculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial * 1 / 4)}}, rewards) + + // commission should be equal to initial (50% twice) + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) } -func TestWithdrawDelegationRewardsAll(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //make some validators with different commissions - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.NewDecWithPrec(1, 1)) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - msgCreateValidator = staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr2, valConsPk2, 50, sdk.NewDecWithPrec(2, 1)) - got = stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - msgCreateValidator = staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr3, valConsPk3, 40, sdk.NewDecWithPrec(3, 1)) - got = stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - // delegate to all the validators - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - require.True(t, stakingHandler(ctx, msgDelegate).IsOK()) - msgDelegate = staking.NewTestMsgDelegate(delAddr1, valOpAddr2, 20) - require.True(t, stakingHandler(ctx, msgDelegate).IsOK()) - msgDelegate = staking.NewTestMsgDelegate(delAddr1, valOpAddr3, 30) - require.True(t, stakingHandler(ctx, msgDelegate).IsOK()) - - // Update sk's LastValidatorPower/LastTotalPowers. - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // 40 tokens left after delegating 60 of them - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(40), amt.Int64()) - - // total power of each validator: - // validator 1: 10 (self) + 10 (delegator) = 20 - // validator 2: 50 (self) + 20 (delegator) = 70 - // validator 3: 40 (self) + 30 (delegator) = 70 - // grand total: 160 - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(1000) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw delegation - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawDelegationRewardsAll(ctx, delAddr1) - amt = accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - - // orig-amount + fees *(1-proposerReward)* (val1Portion * delegatorPotion * (1-val1Commission) ... etc) - // + fees *(proposerReward) * (delegatorPotion * (1-val1Commission)) - // 40 + 1000 *(1- 0.95)* (20/160 * 10/20 * 0.9 + 70/160 * 20/70 * 0.8 + 70/160 * 30/70 * 0.7) - // 40 + 1000 *( 0.05) * (10/20 * 0.9) - feesInNonProposer := sdk.NewDecFromInt(feeInputs).Mul(sdk.NewDecWithPrec(95, 2)) - feesInProposer := sdk.NewDecFromInt(feeInputs).Mul(sdk.NewDecWithPrec(5, 2)) - feesInVal1 := feesInNonProposer.Mul(sdk.NewDec(10).Quo(sdk.NewDec(160))).Mul(sdk.NewDecWithPrec(9, 1)) - feesInVal2 := feesInNonProposer.Mul(sdk.NewDec(20).Quo(sdk.NewDec(160))).Mul(sdk.NewDecWithPrec(8, 1)) - feesInVal3 := feesInNonProposer.Mul(sdk.NewDec(30).Quo(sdk.NewDec(160))).Mul(sdk.NewDecWithPrec(7, 1)) - feesInVal1Proposer := feesInProposer.Mul(sdk.NewDec(10).Quo(sdk.NewDec(20))).Mul(sdk.NewDecWithPrec(9, 1)) - expRes := sdk.NewDec(40).Add(feesInVal1).Add(feesInVal2).Add(feesInVal3).Add(feesInVal1Proposer).TruncateInt() - require.True(sdk.IntEq(t, expRes, amt)) +func TestWithdrawDelegationRewardsBasic(t *testing.T) { + balance := int64(1000) + ctx, ak, k, sk, _ := CreateTestInputDefault(t, false, balance) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + bond := int64(100) + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(bond)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // assert correct initial balance + require.Equal(t, sdk.Coins{{staking.DefaultBondDenom, sdk.NewInt(balance - bond)}}, ak.GetAccount(ctx, sdk.AccAddress(valOpAddr1)).GetCoins()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // set zero outstanding rewards + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // withdraw rewards + require.Nil(t, k.WithdrawDelegationRewards(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1)) + + // assert correct balance + require.Equal(t, sdk.Coins{{staking.DefaultBondDenom, sdk.NewInt(balance - bond + (initial / 2))}}, ak.GetAccount(ctx, sdk.AccAddress(valOpAddr1)).GetCoins()) + + // withdraw commission + require.Nil(t, k.WithdrawValidatorCommission(ctx, valOpAddr1)) + + // assert correct balance + require.Equal(t, sdk.Coins{{staking.DefaultBondDenom, sdk.NewInt(balance - bond + initial)}}, ak.GetAccount(ctx, sdk.AccAddress(valOpAddr1)).GetCoins()) +} + +func TestCalculateRewardsAfterManySlashesInSameBlock(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards := k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be zero + require.True(t, rewards.IsZero()) + + // start out block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some rewards + initial := int64(10) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator by 50% + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 100, sdk.NewDecWithPrec(5, 1)) + + // slash the validator by 50% again + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 50, sdk.NewDecWithPrec(5, 1)) + + // fetch the validator again + val = sk.Validator(ctx, valOpAddr1) + + // increase block height + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards + rewards = k.calculateDelegationRewards(ctx, val, del, endingPeriod) + + // rewards should be half the tokens + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, rewards) + + // commission should be the other half + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) +} + +func TestCalculateRewardsMultiDelegatorMultiSlash(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del1 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // allocate some rewards + initial := int64(30) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 100, sdk.NewDecWithPrec(5, 1)) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // second delegation + msg2 := staking.NewMsgDelegate(sdk.AccAddress(valOpAddr2), valOpAddr1, sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100))) + require.True(t, sh(ctx, msg2).IsOK()) + del2 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr2), valOpAddr1) + + // end block + staking.EndBlocker(ctx, sk) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // slash the validator again + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + sk.Slash(ctx, valConsAddr1, ctx.BlockHeight(), 100, sdk.NewDecWithPrec(5, 1)) + ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 3) + + // fetch updated validator + val = sk.Validator(ctx, valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := k.calculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 2/3 initial (half initial first period, 1/6 initial second period) + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec((initial / 2) + (initial / 6))}}, rewards) + + // calculate delegation rewards for del2 + rewards = k.calculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be initial / 3 + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 3)}}, rewards) + + // commission should be equal to initial (twice 50% commission, unaffected by slashing) + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) +} + +func TestCalculateRewardsMultiDelegatorMultWithdraw(t *testing.T) { + ctx, _, k, sk, _ := CreateTestInputDefault(t, false, 1000) + sh := staking.NewHandler(sk) + + // initialize state + k.SetOutstandingRewards(ctx, sdk.DecCoins{}) + + // create validator with 50% commission + commission := staking.NewCommissionMsg(sdk.NewDecWithPrec(5, 1), sdk.NewDecWithPrec(5, 1), sdk.NewDec(0)) + msg := staking.NewMsgCreateValidator(valOpAddr1, valConsPk1, + sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100)), staking.Description{}, commission) + require.True(t, sh(ctx, msg).IsOK()) + + // end block to bond validator + staking.EndBlocker(ctx, sk) + + // fetch validator and delegation + val := sk.Validator(ctx, valOpAddr1) + del1 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // allocate some rewards + initial := int64(20) + tokens := sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial)}} + k.AllocateTokensToValidator(ctx, val, tokens) + + // second delegation + msg2 := staking.NewMsgDelegate(sdk.AccAddress(valOpAddr2), valOpAddr1, sdk.NewCoin(staking.DefaultBondDenom, sdk.NewInt(100))) + require.True(t, sh(ctx, msg2).IsOK()) + + // fetch updated validator + val = sk.Validator(ctx, valOpAddr1) + del2 := sk.Delegation(ctx, sdk.AccAddress(valOpAddr2), valOpAddr1) + + // end block + staking.EndBlocker(ctx, sk) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // first delegator withdraws + k.WithdrawDelegationRewards(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // second delegator withdraws + k.WithdrawDelegationRewards(ctx, sdk.AccAddress(valOpAddr2), valOpAddr1) + + // validator withdraws commission + k.WithdrawValidatorCommission(ctx, valOpAddr1) + + // end period + endingPeriod := k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards := k.calculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be zero + require.True(t, rewards.IsZero()) + + // calculate delegation rewards for del2 + rewards = k.calculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be zero + require.True(t, rewards.IsZero()) + + // commission should be zero + require.True(t, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1).IsZero()) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // first delegator withdraws again + k.WithdrawDelegationRewards(ctx, sdk.AccAddress(valOpAddr1), valOpAddr1) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards = k.calculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be zero + require.True(t, rewards.IsZero()) + + // calculate delegation rewards for del2 + rewards = k.calculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 4)}}, rewards) + + // commission should be half initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1)) + + // allocate some more rewards + k.AllocateTokensToValidator(ctx, val, tokens) + + // withdraw commission + k.WithdrawValidatorCommission(ctx, valOpAddr1) + + // end period + endingPeriod = k.incrementValidatorPeriod(ctx, val) + + // calculate delegation rewards for del1 + rewards = k.calculateDelegationRewards(ctx, val, del1, endingPeriod) + + // rewards for del1 should be 1/4 initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 4)}}, rewards) + + // calculate delegation rewards for del2 + rewards = k.calculateDelegationRewards(ctx, val, del2, endingPeriod) + + // rewards for del2 should be 1/2 initial + require.Equal(t, sdk.DecCoins{{staking.DefaultBondDenom, sdk.NewDec(initial / 2)}}, rewards) + + // commission should be zero + require.True(t, k.GetValidatorAccumulatedCommission(ctx, valOpAddr1).IsZero()) } diff --git a/x/distribution/keeper/genesis.go b/x/distribution/keeper/genesis.go deleted file mode 100644 index 8e5a37abe2f4..000000000000 --- a/x/distribution/keeper/genesis.go +++ /dev/null @@ -1,50 +0,0 @@ -package keeper - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/distribution/types" -) - -// Get the set of all validator-distribution-info's with no limits, used during genesis dump -func (k Keeper) GetAllValidatorDistInfos(ctx sdk.Context) (vdis []types.ValidatorDistInfo) { - store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, ValidatorDistInfoKey) - defer iterator.Close() - - for ; iterator.Valid(); iterator.Next() { - var vdi types.ValidatorDistInfo - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &vdi) - vdis = append(vdis, vdi) - } - return vdis -} - -// Get the set of all delegator-distribution-info's with no limits, used during genesis dump -func (k Keeper) GetAllDelegationDistInfos(ctx sdk.Context) (ddis []types.DelegationDistInfo) { - store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) - defer iterator.Close() - - for ; iterator.Valid(); iterator.Next() { - var ddi types.DelegationDistInfo - k.cdc.MustUnmarshalBinaryLengthPrefixed(iterator.Value(), &ddi) - ddis = append(ddis, ddi) - } - return ddis -} - -// Get the set of all delegator-withdraw addresses with no limits, used during genesis dump -func (k Keeper) GetAllDelegatorWithdrawInfos(ctx sdk.Context) (dwis []types.DelegatorWithdrawInfo) { - store := ctx.KVStore(k.storeKey) - iterator := sdk.KVStorePrefixIterator(store, DelegatorWithdrawInfoKey) - defer iterator.Close() - - for ; iterator.Valid(); iterator.Next() { - dw := types.DelegatorWithdrawInfo{ - DelegatorAddr: GetDelegatorWithdrawInfoAddress(iterator.Key()), - WithdrawAddr: sdk.AccAddress(iterator.Value()), - } - dwis = append(dwis, dw) - } - return dwis -} diff --git a/x/distribution/keeper/hooks.go b/x/distribution/keeper/hooks.go index 10d4f059b1c1..a3ed466e2f37 100644 --- a/x/distribution/keeper/hooks.go +++ b/x/distribution/keeper/hooks.go @@ -1,109 +1,9 @@ package keeper import ( - "fmt" - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/distribution/types" ) -// Create a new validator distribution record -func (k Keeper) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) { - - // defensive check for existence - if k.HasValidatorDistInfo(ctx, valAddr) { - panic("validator dist info already exists (not cleaned up properly)") - } - - height := ctx.BlockHeight() - vdi := types.ValidatorDistInfo{ - OperatorAddr: valAddr, - FeePoolWithdrawalHeight: height, - DelAccum: types.NewTotalAccum(height), - DelPool: types.DecCoins{}, - ValCommission: types.DecCoins{}, - } - k.SetValidatorDistInfo(ctx, vdi) -} - -// Withdraw all validator rewards -func (k Keeper) BeforeValidatorModified(ctx sdk.Context, valAddr sdk.ValAddress) { - // Move the validator's rewards from the global pool to the validator's pools - // (dist info), but without actually withdrawing the rewards. This does not - // need to happen during the genesis block. - if ctx.BlockHeight() > 0 { - if err := k.takeValidatorFeePoolRewards(ctx, valAddr); err != nil { - panic(err) - } - } -} - -// Withdraw all validator rewards -func (k Keeper) AfterValidatorBonded(ctx sdk.Context, valAddr sdk.ValAddress) { - lastPower := k.stakingKeeper.GetLastValidatorPower(ctx, valAddr) - if !lastPower.Equal(sdk.ZeroInt()) { - panic("expected last power to be 0 for validator entering bonded state") - } - k.BeforeValidatorModified(ctx, valAddr) -} - -// Sanity check, very useful! -func (k Keeper) AfterValidatorPowerDidChange(ctx sdk.Context, valAddr sdk.ValAddress) { - vi := k.GetValidatorDistInfo(ctx, valAddr) - if vi.FeePoolWithdrawalHeight != ctx.BlockHeight() { - panic(fmt.Sprintf("expected validator (%v) dist info FeePoolWithdrawalHeight to be updated to %v, but was %v.", - valAddr.String(), ctx.BlockHeight(), vi.FeePoolWithdrawalHeight)) - } -} - -// Withdrawal all validator distribution rewards and cleanup the distribution record -func (k Keeper) AfterValidatorRemoved(ctx sdk.Context, valAddr sdk.ValAddress) { - k.RemoveValidatorDistInfo(ctx, valAddr) -} - -//_________________________________________________________________________________________ - -// Create a new delegator distribution record -func (k Keeper) BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) { - - ddi := types.DelegationDistInfo{ - DelegatorAddr: delAddr, - ValOperatorAddr: valAddr, - DelPoolWithdrawalHeight: ctx.BlockHeight(), - } - k.SetDelegationDistInfo(ctx, ddi) -} - -// Withdrawal all validator rewards -func (k Keeper) BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) { - - if err := k.WithdrawDelegationReward(ctx, delAddr, valAddr); err != nil { - panic(err) - } -} - -// Withdrawal all validator distribution rewards and cleanup the distribution record -func (k Keeper) BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, - valAddr sdk.ValAddress) { - // Withdraw validator commission when validator self-bond is removed. - // Because we maintain the invariant that all delegations must be removed - // before a validator is deleted, this ensures that commission will be withdrawn - // before the validator is deleted (and the corresponding ValidatorDistInfo removed). - // If we change other parts of the code such that a self-delegation might remain after - // a validator is deleted, this logic will no longer be safe. - // TODO: Consider instead implementing this in a "BeforeValidatorRemoved" hook. - if valAddr.Equals(sdk.ValAddress(delAddr)) { - feePool, commission := k.withdrawValidatorCommission(ctx, valAddr) - k.WithdrawToDelegator(ctx, feePool, delAddr, commission) - } - - k.RemoveDelegationDistInfo(ctx, delAddr, valAddr) -} - -//_________________________________________________________________________________________ - // Wrapper struct type Hooks struct { k Keeper @@ -111,36 +11,50 @@ type Hooks struct { var _ sdk.StakingHooks = Hooks{} -// New Validator Hooks +// Create new distribution hooks func (k Keeper) Hooks() Hooks { return Hooks{k} } // nolint func (h Hooks) AfterValidatorCreated(ctx sdk.Context, valAddr sdk.ValAddress) { - h.k.AfterValidatorCreated(ctx, valAddr) + val := h.k.stakingKeeper.Validator(ctx, valAddr) + h.k.initializeValidator(ctx, val) } func (h Hooks) BeforeValidatorModified(ctx sdk.Context, valAddr sdk.ValAddress) { - h.k.BeforeValidatorModified(ctx, valAddr) + val := h.k.stakingKeeper.Validator(ctx, valAddr) + // increment period + h.k.incrementValidatorPeriod(ctx, val) } func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr sdk.ValAddress) { - h.k.AfterValidatorRemoved(ctx, valAddr) } func (h Hooks) BeforeDelegationCreated(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { - h.k.BeforeValidatorModified(ctx, valAddr) - h.k.BeforeDelegationCreated(ctx, delAddr, valAddr) + val := h.k.stakingKeeper.Validator(ctx, valAddr) + + // increment period + h.k.incrementValidatorPeriod(ctx, val) } func (h Hooks) BeforeDelegationSharesModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { - h.k.BeforeValidatorModified(ctx, valAddr) - h.k.BeforeDelegationSharesModified(ctx, delAddr, valAddr) + val := h.k.stakingKeeper.Validator(ctx, valAddr) + del := h.k.stakingKeeper.Delegation(ctx, delAddr, valAddr) + + // withdraw delegation rewards (which also increments period) + if err := h.k.withdrawDelegationRewards(ctx, val, del); err != nil { + panic(err) + } } func (h Hooks) BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { - h.k.BeforeDelegationRemoved(ctx, delAddr, valAddr) + // nothing needed here since OnDelegationSharesModified will always also be called +} +func (h Hooks) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { + // create new delegation period record + h.k.initializeDelegation(ctx, valAddr, delAddr) } func (h Hooks) AfterValidatorBeginUnbonding(ctx sdk.Context, _ sdk.ConsAddress, valAddr sdk.ValAddress) { - h.k.BeforeValidatorModified(ctx, valAddr) } func (h Hooks) AfterValidatorBonded(ctx sdk.Context, _ sdk.ConsAddress, valAddr sdk.ValAddress) { - h.k.AfterValidatorBonded(ctx, valAddr) } func (h Hooks) AfterValidatorPowerDidChange(ctx sdk.Context, _ sdk.ConsAddress, valAddr sdk.ValAddress) { - h.k.AfterValidatorPowerDidChange(ctx, valAddr) +} +func (h Hooks) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) { + // record the slash event + h.k.updateValidatorSlashFraction(ctx, valAddr, fraction) } diff --git a/x/distribution/keeper/keeper.go b/x/distribution/keeper/keeper.go index f2c68b8a0055..46d749f748eb 100644 --- a/x/distribution/keeper/keeper.go +++ b/x/distribution/keeper/keeper.go @@ -20,9 +20,9 @@ type Keeper struct { codespace sdk.CodespaceType } +// create a new keeper func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramSpace params.Subspace, ck types.BankKeeper, sk types.StakingKeeper, fck types.FeeCollectionKeeper, codespace sdk.CodespaceType) Keeper { - keeper := Keeper{ storeKey: key, cdc: cdc, @@ -35,123 +35,53 @@ func NewKeeper(cdc *codec.Codec, key sdk.StoreKey, paramSpace params.Subspace, c return keeper } -//______________________________________________________________________ - -// get the global fee pool distribution info -func (k Keeper) GetFeePool(ctx sdk.Context) (feePool types.FeePool) { - store := ctx.KVStore(k.storeKey) - b := store.Get(FeePoolKey) - if b == nil { - panic("Stored fee pool should not have been nil") +// withdraw rewards from a delegation +func (k Keeper) WithdrawDelegationRewards(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) sdk.Error { + val := k.stakingKeeper.Validator(ctx, valAddr) + if val == nil { + return types.ErrNoValidatorDistInfo(k.codespace) } - k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &feePool) - return -} - -// set the global fee pool distribution info -func (k Keeper) SetFeePool(ctx sdk.Context, feePool types.FeePool) { - store := ctx.KVStore(k.storeKey) - b := k.cdc.MustMarshalBinaryLengthPrefixed(feePool) - store.Set(FeePoolKey, b) -} - -// get the total validator accum for the ctx height -// in the fee pool -func (k Keeper) GetFeePoolValAccum(ctx sdk.Context) sdk.Dec { - // withdraw self-delegation - height := ctx.BlockHeight() - totalPower := sdk.NewDecFromInt(k.stakingKeeper.GetLastTotalPower(ctx)) - fp := k.GetFeePool(ctx) - return fp.GetTotalValAccum(height, totalPower) -} - -//______________________________________________________________________ - -// set the proposer public key for this block -func (k Keeper) GetPreviousProposerConsAddr(ctx sdk.Context) (consAddr sdk.ConsAddress) { - store := ctx.KVStore(k.storeKey) - - b := store.Get(ProposerKey) - if b == nil { - panic("Previous proposer not set") + del := k.stakingKeeper.Delegation(ctx, delAddr, valAddr) + if del == nil { + return types.ErrNoDelegationDistInfo(k.codespace) } - k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &consAddr) - return -} - -// get the proposer public key for this block -func (k Keeper) SetPreviousProposerConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) { - store := ctx.KVStore(k.storeKey) - b := k.cdc.MustMarshalBinaryLengthPrefixed(consAddr) - store.Set(ProposerKey, b) -} - -//______________________________________________________________________ - -// get context required for withdraw operations -func (k Keeper) GetWithdrawContext(ctx sdk.Context, - valOperatorAddr sdk.ValAddress) types.WithdrawContext { + // withdraw rewards + if err := k.withdrawDelegationRewards(ctx, val, del); err != nil { + return err + } - feePool := k.GetFeePool(ctx) - height := ctx.BlockHeight() - validator := k.stakingKeeper.Validator(ctx, valOperatorAddr) - lastValPower := k.stakingKeeper.GetLastValidatorPower(ctx, valOperatorAddr) - lastTotalPower := sdk.NewDecFromInt(k.stakingKeeper.GetLastTotalPower(ctx)) + // reinitialize the delegation + k.initializeDelegation(ctx, valAddr, delAddr) - return types.NewWithdrawContext( - feePool, height, lastTotalPower, sdk.NewDecFromInt(lastValPower), - validator.GetCommission()) + return nil } -//______________________________________________________________________ -// PARAM STORE +// withdraw validator commission +func (k Keeper) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddress) sdk.Error { -// Type declaration for parameters -func ParamTypeTable() params.TypeTable { - return params.NewTypeTable( - ParamStoreKeyCommunityTax, sdk.Dec{}, - ParamStoreKeyBaseProposerReward, sdk.Dec{}, - ParamStoreKeyBonusProposerReward, sdk.Dec{}, - ) -} + // fetch validator accumulated commission + commission := k.GetValidatorAccumulatedCommission(ctx, valAddr) + if commission.IsZero() { + return types.ErrNoValidatorCommission(k.codespace) + } -// Returns the current CommunityTax rate from the global param store -// nolint: errcheck -func (k Keeper) GetCommunityTax(ctx sdk.Context) sdk.Dec { - var percent sdk.Dec - k.paramSpace.Get(ctx, ParamStoreKeyCommunityTax, &percent) - return percent -} + coins, remainder := commission.TruncateDecimal() -// nolint: errcheck -func (k Keeper) SetCommunityTax(ctx sdk.Context, percent sdk.Dec) { - k.paramSpace.Set(ctx, ParamStoreKeyCommunityTax, &percent) -} + // leave remainder to withdraw later + k.SetValidatorAccumulatedCommission(ctx, valAddr, remainder) -// Returns the current BaseProposerReward rate from the global param store -// nolint: errcheck -func (k Keeper) GetBaseProposerReward(ctx sdk.Context) sdk.Dec { - var percent sdk.Dec - k.paramSpace.Get(ctx, ParamStoreKeyBaseProposerReward, &percent) - return percent -} + // update outstanding + outstanding := k.GetOutstandingRewards(ctx) + k.SetOutstandingRewards(ctx, outstanding.Minus(sdk.NewDecCoins(coins))) -// nolint: errcheck -func (k Keeper) SetBaseProposerReward(ctx sdk.Context, percent sdk.Dec) { - k.paramSpace.Set(ctx, ParamStoreKeyBaseProposerReward, &percent) -} + accAddr := sdk.AccAddress(valAddr) + withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr) -// Returns the current BaseProposerReward rate from the global param store -// nolint: errcheck -func (k Keeper) GetBonusProposerReward(ctx sdk.Context) sdk.Dec { - var percent sdk.Dec - k.paramSpace.Get(ctx, ParamStoreKeyBonusProposerReward, &percent) - return percent -} + if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil { + return err + } -// nolint: errcheck -func (k Keeper) SetBonusProposerReward(ctx sdk.Context, percent sdk.Dec) { - k.paramSpace.Set(ctx, ParamStoreKeyBonusProposerReward, &percent) + return nil } diff --git a/x/distribution/keeper/keeper_test.go b/x/distribution/keeper/keeper_test.go index a8c378424fe6..99e96e786160 100644 --- a/x/distribution/keeper/keeper_test.go +++ b/x/distribution/keeper/keeper_test.go @@ -9,30 +9,40 @@ import ( "github.com/cosmos/cosmos-sdk/x/distribution/types" ) -func TestSetGetPreviousProposerConsAddr(t *testing.T) { - ctx, _, keeper, _, _ := CreateTestInputDefault(t, false, 0) - - keeper.SetPreviousProposerConsAddr(ctx, valConsAddr1) - res := keeper.GetPreviousProposerConsAddr(ctx) - require.True(t, res.Equals(valConsAddr1), "expected: %v got: %v", valConsAddr1.String(), res.String()) -} - -func TestSetGetCommunityTax(t *testing.T) { - ctx, _, keeper, _, _ := CreateTestInputDefault(t, false, 0) - - someDec := sdk.NewDec(333) - keeper.SetCommunityTax(ctx, someDec) - res := keeper.GetCommunityTax(ctx) - require.True(sdk.DecEq(t, someDec, res)) -} - -func TestSetGetFeePool(t *testing.T) { - ctx, _, keeper, _, _ := CreateTestInputDefault(t, false, 0) - - fp := types.InitialFeePool() - fp.TotalValAccum.UpdateHeight = 777 - - keeper.SetFeePool(ctx, fp) - res := keeper.GetFeePool(ctx) - require.Equal(t, fp.TotalValAccum, res.TotalValAccum) +func TestWithdrawValidatorCommission(t *testing.T) { + ctx, ak, keeper, _, _ := CreateTestInputDefault(t, false, 1000) + + // set zero outstanding rewards + keeper.SetOutstandingRewards(ctx, types.OutstandingRewards{}) + + // check initial balance + balance := ak.GetAccount(ctx, sdk.AccAddress(valOpAddr3)).GetCoins() + require.Equal(t, balance, sdk.Coins{ + {"stake", sdk.NewInt(1000)}, + }) + + // set commission + keeper.SetValidatorAccumulatedCommission(ctx, valOpAddr3, sdk.DecCoins{ + {"mytoken", sdk.NewDec(5).Quo(sdk.NewDec(4))}, + {"stake", sdk.NewDec(3).Quo(sdk.NewDec(2))}, + }) + + // withdraw commission + keeper.WithdrawValidatorCommission(ctx, valOpAddr3) + + // check balance increase + balance = ak.GetAccount(ctx, sdk.AccAddress(valOpAddr3)).GetCoins() + require.Equal(t, balance, sdk.Coins{ + {"mytoken", sdk.NewInt(1)}, + {"stake", sdk.NewInt(1001)}, + }) + + // check remainder + remainder := keeper.GetValidatorAccumulatedCommission(ctx, valOpAddr3) + require.Equal(t, remainder, sdk.DecCoins{ + {"mytoken", sdk.NewDec(1).Quo(sdk.NewDec(4))}, + {"stake", sdk.NewDec(1).Quo(sdk.NewDec(2))}, + }) + + require.True(t, true) } diff --git a/x/distribution/keeper/key.go b/x/distribution/keeper/key.go index 443d13079e3c..5e981559720d 100644 --- a/x/distribution/keeper/key.go +++ b/x/distribution/keeper/key.go @@ -1,55 +1,136 @@ package keeper import ( + "encoding/binary" + sdk "github.com/cosmos/cosmos-sdk/types" ) -// keys/key-prefixes +const ( + // default paramspace for params keeper + DefaultParamspace = "distr" +) + +// keys var ( - FeePoolKey = []byte{0x00} // key for global distribution state - ValidatorDistInfoKey = []byte{0x01} // prefix for each key to a validator distribution - DelegationDistInfoKey = []byte{0x02} // prefix for each key to a delegation distribution - DelegatorWithdrawInfoKey = []byte{0x03} // prefix for each key to a delegator withdraw info - ProposerKey = []byte{0x04} // key for storing the proposer operator address + FeePoolKey = []byte{0x00} // key for global distribution state + ProposerKey = []byte{0x01} // key for the proposer operator address + OutstandingRewardsKey = []byte{0x02} // key for outstanding rewards + + DelegatorWithdrawAddrPrefix = []byte{0x03} // key for delegator withdraw address + DelegatorStartingInfoPrefix = []byte{0x04} // key for delegator starting info + ValidatorHistoricalRewardsPrefix = []byte{0x05} // key for historical validators rewards / stake + ValidatorCurrentRewardsPrefix = []byte{0x06} // key for current validator rewards + ValidatorAccumulatedCommissionPrefix = []byte{0x07} // key for accumulated validator commission + ValidatorSlashEventPrefix = []byte{0x08} // key for validator slash fraction - // params store ParamStoreKeyCommunityTax = []byte("communitytax") ParamStoreKeyBaseProposerReward = []byte("baseproposerreward") ParamStoreKeyBonusProposerReward = []byte("bonusproposerreward") ) -const ( - // default paramspace for params keeper - DefaultParamspace = "distr" -) - -// gets the key for the validator distribution info from address -// VALUE: distribution/types.ValidatorDistInfo -func GetValidatorDistInfoKey(operatorAddr sdk.ValAddress) []byte { - return append(ValidatorDistInfoKey, operatorAddr.Bytes()...) +// gets an address from a delegator's withdraw info key +func GetDelegatorWithdrawInfoAddress(key []byte) (delAddr sdk.AccAddress) { + addr := key[1:] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + return sdk.AccAddress(addr) } -// gets the key for delegator distribution for a validator -// VALUE: distribution/types.DelegationDistInfo -func GetDelegationDistInfoKey(delAddr sdk.AccAddress, valAddr sdk.ValAddress) []byte { - return append(GetDelegationDistInfosKey(delAddr), valAddr.Bytes()...) +// gets the addresses from a delegator starting info key +func GetDelegatorStartingInfoAddresses(key []byte) (valAddr sdk.ValAddress, delAddr sdk.AccAddress) { + addr := key[1 : 1+sdk.AddrLen] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + valAddr = sdk.ValAddress(addr) + addr = key[1+sdk.AddrLen:] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + delAddr = sdk.AccAddress(addr) + return } -// gets the prefix for a delegator's distributions across all validators -func GetDelegationDistInfosKey(delAddr sdk.AccAddress) []byte { - return append(DelegationDistInfoKey, delAddr.Bytes()...) +// gets the address & period from a validator's historical rewards key +func GetValidatorHistoricalRewardsAddressPeriod(key []byte) (valAddr sdk.ValAddress, period uint64) { + addr := key[1 : 1+sdk.AddrLen] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + valAddr = sdk.ValAddress(addr) + b := key[1+sdk.AddrLen:] + if len(b) != 8 { + panic("unexpected key length") + } + period = binary.LittleEndian.Uint64(b) + return } -// gets the prefix for a delegator's withdraw info -func GetDelegatorWithdrawAddrKey(delAddr sdk.AccAddress) []byte { - return append(DelegatorWithdrawInfoKey, delAddr.Bytes()...) +// gets the address from a validator's current rewards key +func GetValidatorCurrentRewardsAddress(key []byte) (valAddr sdk.ValAddress) { + addr := key[1:] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + return sdk.ValAddress(addr) } -// gets an address from a delegator's withdraw info key -func GetDelegatorWithdrawInfoAddress(key []byte) (delAddr sdk.AccAddress) { +// gets the address from a validator's accumulated commission key +func GetValidatorAccumulatedCommissionAddress(key []byte) (valAddr sdk.ValAddress) { addr := key[1:] if len(addr) != sdk.AddrLen { panic("unexpected key length") } - return sdk.AccAddress(addr) + return sdk.ValAddress(addr) +} + +// gets the height from a validator's slash event key +func GetValidatorSlashEventAddressHeight(key []byte) (valAddr sdk.ValAddress, height uint64) { + addr := key[1 : 1+sdk.AddrLen] + if len(addr) != sdk.AddrLen { + panic("unexpected key length") + } + valAddr = sdk.ValAddress(addr) + b := key[1+sdk.AddrLen:] + if len(b) != 8 { + panic("unexpected key length") + } + height = binary.BigEndian.Uint64(b) + return +} + +// gets the key for a delegator's withdraw addr +func GetDelegatorWithdrawAddrKey(delAddr sdk.AccAddress) []byte { + return append(DelegatorWithdrawAddrPrefix, delAddr.Bytes()...) +} + +// gets the key for a delegator's starting info +func GetDelegatorStartingInfoKey(v sdk.ValAddress, d sdk.AccAddress) []byte { + return append(append(DelegatorStartingInfoPrefix, v.Bytes()...), d.Bytes()...) +} + +// gets the key for a validator's historical rewards +func GetValidatorHistoricalRewardsKey(v sdk.ValAddress, k uint64) []byte { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, k) + return append(append(ValidatorHistoricalRewardsPrefix, v.Bytes()...), b...) +} + +// gets the key for a validator's current rewards +func GetValidatorCurrentRewardsKey(v sdk.ValAddress) []byte { + return append(ValidatorCurrentRewardsPrefix, v.Bytes()...) +} + +// gets the key for a validator's current commission +func GetValidatorAccumulatedCommissionKey(v sdk.ValAddress) []byte { + return append(ValidatorAccumulatedCommissionPrefix, v.Bytes()...) +} + +// gets the key for a validator's slash fraction +func GetValidatorSlashEventKey(v sdk.ValAddress, height uint64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, height) + return append(append(ValidatorSlashEventPrefix, v.Bytes()...), b...) } diff --git a/x/distribution/keeper/params.go b/x/distribution/keeper/params.go new file mode 100644 index 000000000000..d0faf266b432 --- /dev/null +++ b/x/distribution/keeper/params.go @@ -0,0 +1,54 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/params" +) + +// type declaration for parameters +func ParamTypeTable() params.TypeTable { + return params.NewTypeTable( + ParamStoreKeyCommunityTax, sdk.Dec{}, + ParamStoreKeyBaseProposerReward, sdk.Dec{}, + ParamStoreKeyBonusProposerReward, sdk.Dec{}, + ) +} + +// returns the current CommunityTax rate from the global param store +// nolint: errcheck +func (k Keeper) GetCommunityTax(ctx sdk.Context) sdk.Dec { + var percent sdk.Dec + k.paramSpace.Get(ctx, ParamStoreKeyCommunityTax, &percent) + return percent +} + +// nolint: errcheck +func (k Keeper) SetCommunityTax(ctx sdk.Context, percent sdk.Dec) { + k.paramSpace.Set(ctx, ParamStoreKeyCommunityTax, &percent) +} + +// returns the current BaseProposerReward rate from the global param store +// nolint: errcheck +func (k Keeper) GetBaseProposerReward(ctx sdk.Context) sdk.Dec { + var percent sdk.Dec + k.paramSpace.Get(ctx, ParamStoreKeyBaseProposerReward, &percent) + return percent +} + +// nolint: errcheck +func (k Keeper) SetBaseProposerReward(ctx sdk.Context, percent sdk.Dec) { + k.paramSpace.Set(ctx, ParamStoreKeyBaseProposerReward, &percent) +} + +// returns the current BaseProposerReward rate from the global param store +// nolint: errcheck +func (k Keeper) GetBonusProposerReward(ctx sdk.Context) sdk.Dec { + var percent sdk.Dec + k.paramSpace.Get(ctx, ParamStoreKeyBonusProposerReward, &percent) + return percent +} + +// nolint: errcheck +func (k Keeper) SetBonusProposerReward(ctx sdk.Context, percent sdk.Dec) { + k.paramSpace.Set(ctx, ParamStoreKeyBonusProposerReward, &percent) +} diff --git a/x/distribution/keeper/store.go b/x/distribution/keeper/store.go new file mode 100644 index 000000000000..94bcf668c778 --- /dev/null +++ b/x/distribution/keeper/store.go @@ -0,0 +1,288 @@ +package keeper + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" +) + +// get the delegator withdraw address, defaulting to the delegator address +func (k Keeper) GetDelegatorWithdrawAddr(ctx sdk.Context, delAddr sdk.AccAddress) sdk.AccAddress { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetDelegatorWithdrawAddrKey(delAddr)) + if b == nil { + return delAddr + } + return sdk.AccAddress(b) +} + +// set the delegator withdraw address +func (k Keeper) SetDelegatorWithdrawAddr(ctx sdk.Context, delAddr, withdrawAddr sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + store.Set(GetDelegatorWithdrawAddrKey(delAddr), withdrawAddr.Bytes()) +} + +// remove a delegator withdraw addr +func (k Keeper) RemoveDelegatorWithdrawAddr(ctx sdk.Context, delAddr, withdrawAddr sdk.AccAddress) { + store := ctx.KVStore(k.storeKey) + store.Delete(GetDelegatorWithdrawAddrKey(delAddr)) +} + +// iterate over delegator withdraw addrs +func (k Keeper) IterateDelegatorWithdrawAddrs(ctx sdk.Context, handler func(del sdk.AccAddress, addr sdk.AccAddress) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, DelegatorWithdrawAddrPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + addr := sdk.AccAddress(iter.Value()) + del := GetDelegatorWithdrawInfoAddress(iter.Key()) + if handler(del, addr) { + break + } + } +} + +// get the global fee pool distribution info +func (k Keeper) GetFeePool(ctx sdk.Context) (feePool types.FeePool) { + store := ctx.KVStore(k.storeKey) + b := store.Get(FeePoolKey) + if b == nil { + panic("Stored fee pool should not have been nil") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &feePool) + return +} + +// set the global fee pool distribution info +func (k Keeper) SetFeePool(ctx sdk.Context, feePool types.FeePool) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(feePool) + store.Set(FeePoolKey, b) +} + +// get the proposer public key for this block +func (k Keeper) GetPreviousProposerConsAddr(ctx sdk.Context) (consAddr sdk.ConsAddress) { + store := ctx.KVStore(k.storeKey) + b := store.Get(ProposerKey) + if b == nil { + panic("Previous proposer not set") + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &consAddr) + return +} + +// set the proposer public key for this block +func (k Keeper) SetPreviousProposerConsAddr(ctx sdk.Context, consAddr sdk.ConsAddress) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(consAddr) + store.Set(ProposerKey, b) +} + +// get the starting period associated with a delegator +func (k Keeper) GetDelegatorStartingInfo(ctx sdk.Context, val sdk.ValAddress, del sdk.AccAddress) (period types.DelegatorStartingInfo) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetDelegatorStartingInfoKey(val, del)) + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &period) + return +} + +// set the starting period associated with a delegator +func (k Keeper) SetDelegatorStartingInfo(ctx sdk.Context, val sdk.ValAddress, del sdk.AccAddress, period types.DelegatorStartingInfo) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(period) + store.Set(GetDelegatorStartingInfoKey(val, del), b) +} + +// iterate over delegator starting infos +func (k Keeper) IterateDelegatorStartingInfos(ctx sdk.Context, handler func(val sdk.ValAddress, del sdk.AccAddress, info types.DelegatorStartingInfo) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, DelegatorStartingInfoPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var info types.DelegatorStartingInfo + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &info) + val, del := GetDelegatorStartingInfoAddresses(iter.Key()) + if handler(val, del, info) { + break + } + } +} + +// get historical rewards for a particular period +func (k Keeper) GetValidatorHistoricalRewards(ctx sdk.Context, val sdk.ValAddress, period uint64) (rewards types.ValidatorHistoricalRewards) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetValidatorHistoricalRewardsKey(val, period)) + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &rewards) + return +} + +// set historical rewards for a particular period +func (k Keeper) SetValidatorHistoricalRewards(ctx sdk.Context, val sdk.ValAddress, period uint64, rewards types.ValidatorHistoricalRewards) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(rewards) + store.Set(GetValidatorHistoricalRewardsKey(val, period), b) +} + +// iterate over historical rewards +func (k Keeper) IterateValidatorHistoricalRewards(ctx sdk.Context, handler func(val sdk.ValAddress, period uint64, rewards types.ValidatorHistoricalRewards) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorHistoricalRewardsPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var rewards types.ValidatorHistoricalRewards + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &rewards) + addr, period := GetValidatorHistoricalRewardsAddressPeriod(iter.Key()) + if handler(addr, period, rewards) { + break + } + } +} + +// delete historical rewards +func (k Keeper) DeleteValidatorHistoricalRewards(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorHistoricalRewardsPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} + +// get current rewards for a validator +func (k Keeper) GetValidatorCurrentRewards(ctx sdk.Context, val sdk.ValAddress) (rewards types.ValidatorCurrentRewards) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetValidatorCurrentRewardsKey(val)) + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &rewards) + return +} + +// set current rewards for a validator +func (k Keeper) SetValidatorCurrentRewards(ctx sdk.Context, val sdk.ValAddress, rewards types.ValidatorCurrentRewards) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(rewards) + store.Set(GetValidatorCurrentRewardsKey(val), b) +} + +// iterate over current rewards +func (k Keeper) IterateValidatorCurrentRewards(ctx sdk.Context, handler func(val sdk.ValAddress, rewards types.ValidatorCurrentRewards) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorCurrentRewardsPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var rewards types.ValidatorCurrentRewards + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &rewards) + addr := GetValidatorCurrentRewardsAddress(iter.Key()) + if handler(addr, rewards) { + break + } + } +} + +// get accumulated commission for a validator +func (k Keeper) GetValidatorAccumulatedCommission(ctx sdk.Context, val sdk.ValAddress) (commission types.ValidatorAccumulatedCommission) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetValidatorAccumulatedCommissionKey(val)) + if b == nil { + return types.ValidatorAccumulatedCommission{} + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &commission) + return +} + +// set accumulated commission for a validator +func (k Keeper) SetValidatorAccumulatedCommission(ctx sdk.Context, val sdk.ValAddress, commission types.ValidatorAccumulatedCommission) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(commission) + store.Set(GetValidatorAccumulatedCommissionKey(val), b) +} + +// iterate over accumulated commissions +func (k Keeper) IterateValidatorAccumulatedCommissions(ctx sdk.Context, handler func(val sdk.ValAddress, commission types.ValidatorAccumulatedCommission) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorAccumulatedCommissionPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var commission types.ValidatorAccumulatedCommission + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &commission) + addr := GetValidatorAccumulatedCommissionAddress(iter.Key()) + if handler(addr, commission) { + break + } + } +} + +// get outstanding rewards +func (k Keeper) GetOutstandingRewards(ctx sdk.Context) (rewards types.OutstandingRewards) { + store := ctx.KVStore(k.storeKey) + b := store.Get(OutstandingRewardsKey) + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &rewards) + return +} + +// set outstanding rewards +func (k Keeper) SetOutstandingRewards(ctx sdk.Context, rewards types.OutstandingRewards) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(rewards) + store.Set(OutstandingRewardsKey, b) +} + +// get slash event for height +func (k Keeper) GetValidatorSlashEvent(ctx sdk.Context, val sdk.ValAddress, height uint64) (event types.ValidatorSlashEvent, found bool) { + store := ctx.KVStore(k.storeKey) + b := store.Get(GetValidatorSlashEventKey(val, height)) + if b == nil { + return types.ValidatorSlashEvent{}, false + } + k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &event) + return event, true +} + +// set slash event for height +func (k Keeper) SetValidatorSlashEvent(ctx sdk.Context, val sdk.ValAddress, height uint64, event types.ValidatorSlashEvent) { + store := ctx.KVStore(k.storeKey) + b := k.cdc.MustMarshalBinaryLengthPrefixed(event) + store.Set(GetValidatorSlashEventKey(val, height), b) +} + +// iterate over slash events between heights, inclusive +func (k Keeper) IterateValidatorSlashEventsBetween(ctx sdk.Context, val sdk.ValAddress, startingHeight uint64, endingHeight uint64, + handler func(height uint64, event types.ValidatorSlashEvent) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := store.Iterator( + GetValidatorSlashEventKey(val, startingHeight), + GetValidatorSlashEventKey(val, endingHeight+1), + ) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var event types.ValidatorSlashEvent + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &event) + _, height := GetValidatorSlashEventAddressHeight(iter.Key()) + if handler(height, event) { + break + } + } +} + +// iterate over all slash events +func (k Keeper) IterateValidatorSlashEvents(ctx sdk.Context, handler func(val sdk.ValAddress, height uint64, event types.ValidatorSlashEvent) (stop bool)) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorSlashEventPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + var event types.ValidatorSlashEvent + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &event) + val, height := GetValidatorSlashEventAddressHeight(iter.Key()) + if handler(val, height, event) { + break + } + } +} + +// delete all slash events +func (k Keeper) DeleteValidatorSlashEvents(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorSlashEventPrefix) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} diff --git a/x/distribution/keeper/validator.go b/x/distribution/keeper/validator.go index 76e5fe8d336a..15bf49f0584f 100644 --- a/x/distribution/keeper/validator.go +++ b/x/distribution/keeper/validator.go @@ -2,170 +2,70 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/distribution/types" ) -// check whether a validator has distribution info -func (k Keeper) HasValidatorDistInfo(ctx sdk.Context, - operatorAddr sdk.ValAddress) (exists bool) { - store := ctx.KVStore(k.storeKey) - return store.Has(GetValidatorDistInfoKey(operatorAddr)) -} - -// get the validator distribution info -func (k Keeper) GetValidatorDistInfo(ctx sdk.Context, - operatorAddr sdk.ValAddress) (vdi types.ValidatorDistInfo) { - - store := ctx.KVStore(k.storeKey) - - b := store.Get(GetValidatorDistInfoKey(operatorAddr)) - if b == nil { - panic("Stored validator-distribution info should not have been nil") - } +// initialize rewards for a new validator +func (k Keeper) initializeValidator(ctx sdk.Context, val sdk.Validator) { + // set initial historical rewards (period 0) + k.SetValidatorHistoricalRewards(ctx, val.GetOperator(), 0, types.ValidatorHistoricalRewards{}) - k.cdc.MustUnmarshalBinaryLengthPrefixed(b, &vdi) - return -} + // set current rewards (starting at period 1) + k.SetValidatorCurrentRewards(ctx, val.GetOperator(), types.NewValidatorCurrentRewards(sdk.DecCoins{}, 1)) -// set the validator distribution info -func (k Keeper) SetValidatorDistInfo(ctx sdk.Context, vdi types.ValidatorDistInfo) { - store := ctx.KVStore(k.storeKey) - b := k.cdc.MustMarshalBinaryLengthPrefixed(vdi) - store.Set(GetValidatorDistInfoKey(vdi.OperatorAddr), b) + // set accumulated commission + k.SetValidatorAccumulatedCommission(ctx, val.GetOperator(), types.InitialValidatorAccumulatedCommission()) } -// remove a validator distribution info -func (k Keeper) RemoveValidatorDistInfo(ctx sdk.Context, valAddr sdk.ValAddress) { - - // defensive check - vdi := k.GetValidatorDistInfo(ctx, valAddr) - if vdi.DelAccum.Accum.IsPositive() { - panic("Should not delete validator with unwithdrawn delegator accum") - } - if !vdi.ValCommission.IsZero() { - panic("Should not delete validator with unwithdrawn validator commission") +// increment validator period, returning the period just ended +func (k Keeper) incrementValidatorPeriod(ctx sdk.Context, val sdk.Validator) uint64 { + // fetch current rewards + rewards := k.GetValidatorCurrentRewards(ctx, val.GetOperator()) + + // calculate current ratio + var current sdk.DecCoins + if val.GetTokens().IsZero() { + + // can't calculate ratio for zero-token validators + // ergo we instead add to the community pool + feePool := k.GetFeePool(ctx) + outstanding := k.GetOutstandingRewards(ctx) + feePool.CommunityPool = feePool.CommunityPool.Plus(rewards.Rewards) + outstanding = outstanding.Minus(rewards.Rewards) + k.SetFeePool(ctx, feePool) + k.SetOutstandingRewards(ctx, outstanding) + + current = sdk.DecCoins{} + } else { + current = rewards.Rewards.QuoDec(sdk.NewDecFromInt(val.GetTokens())) } - store := ctx.KVStore(k.storeKey) - store.Delete(GetValidatorDistInfoKey(valAddr)) -} + // fetch historical rewards for last period + historical := k.GetValidatorHistoricalRewards(ctx, val.GetOperator(), rewards.Period-1) -// remove all validator distribution infos -func (k Keeper) RemoveValidatorDistInfos(ctx sdk.Context) { - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, ValidatorDistInfoKey) - defer iter.Close() - for ; iter.Valid(); iter.Next() { - store.Delete(iter.Key()) - } -} + // fet new historical rewards + k.SetValidatorHistoricalRewards(ctx, val.GetOperator(), rewards.Period, historical.Plus(current)) -// iterate over all the validator distribution infos -func (k Keeper) IterateValidatorDistInfos(ctx sdk.Context, - fn func(index int64, distInfo types.ValidatorDistInfo) (stop bool)) { + // set current rewards, incrementing period by 1 + k.SetValidatorCurrentRewards(ctx, val.GetOperator(), types.NewValidatorCurrentRewards(sdk.DecCoins{}, rewards.Period+1)) - store := ctx.KVStore(k.storeKey) - iter := sdk.KVStorePrefixIterator(store, ValidatorDistInfoKey) - defer iter.Close() - index := int64(0) - for ; iter.Valid(); iter.Next() { - var vdi types.ValidatorDistInfo - k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &vdi) - if fn(index, vdi) { - return - } - index++ - } + return rewards.Period } -// Get the calculated accum of a validator at the current block -// without affecting the state. -func (k Keeper) GetValidatorAccum(ctx sdk.Context, operatorAddr sdk.ValAddress) (sdk.Dec, sdk.Error) { - if !k.HasValidatorDistInfo(ctx, operatorAddr) { - return sdk.Dec{}, types.ErrNoValidatorDistInfo(k.codespace) +func (k Keeper) updateValidatorSlashFraction(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) { + height := uint64(ctx.BlockHeight()) + currentFraction := sdk.ZeroDec() + currentPeriod := k.GetValidatorCurrentRewards(ctx, valAddr).Period + current, found := k.GetValidatorSlashEvent(ctx, valAddr, height) + if found { + // there has already been a slash event this height, + // and we don't need to store more than one, + // so just update the current slash fraction + currentFraction = current.Fraction } - - // withdraw self-delegation - height := ctx.BlockHeight() - lastValPower := k.stakingKeeper.GetLastValidatorPower(ctx, operatorAddr) - valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) - accum := valInfo.GetValAccum(height, sdk.NewDecFromInt(lastValPower)) - - return accum, nil -} - -// takeValidatorFeePoolRewards updates the validator's distribution info -// from the global fee pool without withdrawing any rewards. This will be called -// from a onValidatorModified hook. -func (k Keeper) takeValidatorFeePoolRewards(ctx sdk.Context, operatorAddr sdk.ValAddress) sdk.Error { - if !k.HasValidatorDistInfo(ctx, operatorAddr) { - return types.ErrNoValidatorDistInfo(k.codespace) - } - accAddr := sdk.AccAddress(operatorAddr.Bytes()) - - // withdraw reward for self-delegation - if k.HasDelegationDistInfo(ctx, accAddr, operatorAddr) { - fp, vi, di, withdraw := - k.withdrawDelegationReward(ctx, accAddr, operatorAddr) - k.SetFeePool(ctx, fp) - k.SetValidatorDistInfo(ctx, vi) - k.SetDelegationDistInfo(ctx, di) - k.WithdrawToDelegator(ctx, fp, accAddr, withdraw) - } - - // withdrawal validator commission rewards - valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) - wc := k.GetWithdrawContext(ctx, operatorAddr) - valInfo, feePool := valInfo.TakeFeePoolRewards(wc) - k.SetFeePool(ctx, feePool) - k.SetValidatorDistInfo(ctx, valInfo) - return nil -} - -func (k Keeper) withdrawValidatorCommission(ctx sdk.Context, operatorAddr sdk.ValAddress) (types.FeePool, types.DecCoins) { - valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) - wc := k.GetWithdrawContext(ctx, operatorAddr) - valInfo, feePool, commission := valInfo.WithdrawCommission(wc) - k.SetValidatorDistInfo(ctx, valInfo) - - return feePool, commission -} - -// withdrawal all the validator rewards including the commission -func (k Keeper) WithdrawValidatorRewardsAll(ctx sdk.Context, operatorAddr sdk.ValAddress) sdk.Error { - if !k.HasValidatorDistInfo(ctx, operatorAddr) { - return types.ErrNoValidatorDistInfo(k.codespace) - } - - // withdraw self-delegation - accAddr := sdk.AccAddress(operatorAddr.Bytes()) - withdraw := k.withdrawDelegationRewardsAll(ctx, accAddr) - - // withdrawal validator commission rewards - feePool, commission := k.withdrawValidatorCommission(ctx, operatorAddr) - withdraw = withdraw.Plus(commission) - - k.WithdrawToDelegator(ctx, feePool, accAddr, withdraw) - return nil -} - -// get all the validator rewards including the commission -func (k Keeper) CurrentValidatorRewardsAll(ctx sdk.Context, operatorAddr sdk.ValAddress) (sdk.Coins, sdk.Error) { - - if !k.HasValidatorDistInfo(ctx, operatorAddr) { - return sdk.Coins{}, types.ErrNoValidatorDistInfo(k.codespace) - } - - // withdraw self-delegation - accAddr := sdk.AccAddress(operatorAddr.Bytes()) - withdraw := k.CurrentDelegationRewardsAll(ctx, accAddr) - - // withdrawal validator commission rewards - valInfo := k.GetValidatorDistInfo(ctx, operatorAddr) - - wc := k.GetWithdrawContext(ctx, operatorAddr) - commission := valInfo.CurrentCommissionRewards(wc) - withdraw = withdraw.Plus(commission) - truncated, _ := withdraw.TruncateDecimal() - return truncated, nil + currentMultiplicand := sdk.OneDec().Sub(currentFraction) + newMultiplicand := sdk.OneDec().Sub(fraction) + updatedFraction := sdk.OneDec().Sub(currentMultiplicand.Mul(newMultiplicand)) + k.SetValidatorSlashEvent(ctx, valAddr, height, types.NewValidatorSlashEvent(currentPeriod, updatedFraction)) } diff --git a/x/distribution/keeper/validator_test.go b/x/distribution/keeper/validator_test.go deleted file mode 100644 index a6956fcc0cdc..000000000000 --- a/x/distribution/keeper/validator_test.go +++ /dev/null @@ -1,201 +0,0 @@ -package keeper - -import ( - "testing" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" - "github.com/cosmos/cosmos-sdk/x/staking" -) - -func TestWithdrawValidatorRewardsAllNoDelegator(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - // first make a validator - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, 10) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw self-delegation reward - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawValidatorRewardsAll(ctx, valOpAddr1) - amt := accMapper.GetAccount(ctx, valAccAddr1).GetCoins().AmountOf(denom) - expRes := sdk.NewDec(90).Add(sdk.NewDec(100)).TruncateInt() - require.True(sdk.IntEq(t, expRes, amt)) -} - -func TestWithdrawValidatorRewardsAllDelegatorNoCommission(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - msgCreateValidator := staking.NewTestMsgCreateValidator(valOpAddr1, valConsPk1, 10) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw self-delegation reward - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawValidatorRewardsAll(ctx, valOpAddr1) - amt = accMapper.GetAccount(ctx, valAccAddr1).GetCoins().AmountOf(denom) - expRes := sdk.NewDec(90).Add(sdk.NewDec(100).Quo(sdk.NewDec(2))).TruncateInt() // 90 + 100 tokens * 10/20 - require.True(sdk.IntEq(t, expRes, amt)) -} - -func TestWithdrawValidatorRewardsAllDelegatorWithCommission(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator - commissionRate := sdk.NewDecWithPrec(1, 1) - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, commissionRate) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw validator reward - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawValidatorRewardsAll(ctx, valOpAddr1) - amt = accMapper.GetAccount(ctx, valAccAddr1).GetCoins().AmountOf(denom) - commissionTaken := sdk.NewDec(100).Mul(commissionRate) - afterCommission := sdk.NewDec(100).Sub(commissionTaken) - selfDelegationReward := afterCommission.Quo(sdk.NewDec(2)) - expRes := sdk.NewDec(90).Add(commissionTaken).Add(selfDelegationReward).TruncateInt() // 90 + 100 tokens * 10/20 - require.True(sdk.IntEq(t, expRes, amt)) -} - -func TestWithdrawValidatorRewardsAllMultipleValidator(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - // Make some validators with different commissions. - // Bond 10 of 100 with 0.1 commission. - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.NewDecWithPrec(1, 1)) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - // Bond 50 of 100 with 0.2 commission. - msgCreateValidator = staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr2, valConsPk2, 50, sdk.NewDecWithPrec(2, 1)) - got = stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - // Bond 40 of 100 with 0.3 commission. - msgCreateValidator = staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr3, valConsPk3, 40, sdk.NewDecWithPrec(3, 1)) - got = stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // Allocate 1000 denom of fees. - feeInputs := sdk.NewInt(1000) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - // Collect proposer reward for 100% of votes. - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // Withdraw validator reward. - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawValidatorRewardsAll(ctx, valOpAddr1) - amt := accMapper.GetAccount(ctx, valAccAddr1).GetCoins().AmountOf(denom) - - feesInNonProposer := sdk.NewDecFromInt(feeInputs).Mul(sdk.NewDecWithPrec(95, 2)) - feesInProposer := sdk.NewDecFromInt(feeInputs).Mul(sdk.NewDecWithPrec(5, 2)) - // NOTE: the non-proposer rewards (95) and proposer rewards (50) add up to - // 145. During computation, this is further split into 130.5 and 14.5, - // which is the non-commission and commission respectively, but the - // commission is for self so the result is just 145. - expRes := sdk.NewDec(90). // orig tokens (100) - bonded (10) - Add(feesInNonProposer.Quo(sdk.NewDec(10))). // validator 1 has 1/10 total power (non-proposer rewards = 95) - Add(feesInProposer). // (proposer rewards = 50) - TruncateInt() - require.True(sdk.IntEq(t, expRes, amt)) -} - -func TestWithdrawValidatorRewardsAllMultipleDelegator(t *testing.T) { - ctx, accMapper, keeper, sk, fck := CreateTestInputAdvanced(t, false, 100, sdk.ZeroDec()) - stakingHandler := staking.NewHandler(sk) - denom := sk.GetParams(ctx).BondDenom - - //first make a validator with 10% commission - commissionRate := sdk.NewDecWithPrec(1, 1) - msgCreateValidator := staking.NewTestMsgCreateValidatorWithCommission( - valOpAddr1, valConsPk1, 10, sdk.NewDecWithPrec(1, 1)) - got := stakingHandler(ctx, msgCreateValidator) - require.True(t, got.IsOK(), "expected msg to be ok, got %v", got) - _ = sk.ApplyAndReturnValidatorSetUpdates(ctx) - - // delegate - msgDelegate := staking.NewTestMsgDelegate(delAddr1, valOpAddr1, 10) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt := accMapper.GetAccount(ctx, delAddr1).GetCoins().AmountOf(denom) - require.Equal(t, int64(90), amt.Int64()) - - msgDelegate = staking.NewTestMsgDelegate(delAddr2, valOpAddr1, 20) - got = stakingHandler(ctx, msgDelegate) - require.True(t, got.IsOK()) - amt = accMapper.GetAccount(ctx, delAddr2).GetCoins().AmountOf(denom) - require.Equal(t, int64(80), amt.Int64()) - - // allocate 100 denom of fees - feeInputs := sdk.NewInt(100) - fck.SetCollectedFees(sdk.Coins{sdk.NewCoin(denom, feeInputs)}) - require.Equal(t, feeInputs, fck.GetCollectedFees(ctx).AmountOf(denom)) - keeper.AllocateTokens(ctx, sdk.OneDec(), valConsAddr1) - - // withdraw validator reward - ctx = ctx.WithBlockHeight(1) - keeper.WithdrawValidatorRewardsAll(ctx, valOpAddr1) - amt = accMapper.GetAccount(ctx, valAccAddr1).GetCoins().AmountOf(denom) - - commissionTaken := sdk.NewDec(100).Mul(commissionRate) - afterCommission := sdk.NewDec(100).Sub(commissionTaken) - expRes := sdk.NewDec(90). - Add(afterCommission.Quo(sdk.NewDec(4))). - Add(commissionTaken). - TruncateInt() // 90 + 100*90% tokens * 10/40 - require.True(sdk.IntEq(t, expRes, amt)) -} diff --git a/x/distribution/simulation/invariants.go b/x/distribution/simulation/invariants.go index cdebc98d2c81..4c46d67dbb6b 100644 --- a/x/distribution/simulation/invariants.go +++ b/x/distribution/simulation/invariants.go @@ -10,19 +10,13 @@ import ( ) // AllInvariants runs all invariants of the distribution module -// Currently: total supply, positive power func AllInvariants(d distr.Keeper, stk staking.Keeper) simulation.Invariant { - sk := distr.StakingKeeper(stk) return func(ctx sdk.Context) error { - err := ValAccumInvariants(d, sk)(ctx) + err := CanWithdrawInvariant(d, stk)(ctx) if err != nil { return err } - err = DelAccumInvariants(d, sk)(ctx) - if err != nil { - return err - } - err = CanWithdrawInvariant(d, stk)(ctx) + err = NonNegativeOutstandingInvariant(d)(ctx) if err != nil { return err } @@ -30,104 +24,13 @@ func AllInvariants(d distr.Keeper, stk staking.Keeper) simulation.Invariant { } } -// ValAccumInvariants checks that the fee pool accum == sum all validators' accum -func ValAccumInvariants(k distr.Keeper, sk distr.StakingKeeper) simulation.Invariant { - +// NonNegativeOutstandingInvariant checks that outstanding unwithdrawn fees are never negative +func NonNegativeOutstandingInvariant(k distr.Keeper) simulation.Invariant { return func(ctx sdk.Context) error { - height := ctx.BlockHeight() - - valAccum := sdk.ZeroDec() - k.IterateValidatorDistInfos(ctx, func(_ int64, vdi distr.ValidatorDistInfo) bool { - lastValPower := sk.GetLastValidatorPower(ctx, vdi.OperatorAddr) - valAccum = valAccum.Add(vdi.GetValAccum(height, sdk.NewDecFromInt(lastValPower))) - return false - }) - - lastTotalPower := sdk.NewDecFromInt(sk.GetLastTotalPower(ctx)) - totalAccum := k.GetFeePool(ctx).GetTotalValAccum(height, lastTotalPower) - - if !totalAccum.Equal(valAccum) { - return fmt.Errorf("validator accum invariance: \n\tfee pool totalAccum: %v"+ - "\n\tvalidator accum \t%v\n", totalAccum.String(), valAccum.String()) - } - - return nil - } -} - -// DelAccumInvariants checks that each validator del accum == sum all delegators' accum -func DelAccumInvariants(k distr.Keeper, sk distr.StakingKeeper) simulation.Invariant { - - return func(ctx sdk.Context) error { - height := ctx.BlockHeight() - - totalDelAccumFromVal := make(map[string]sdk.Dec) // key is the valOpAddr string - totalDelAccum := make(map[string]sdk.Dec) - - // iterate the validators - iterVal := func(_ int64, vdi distr.ValidatorDistInfo) bool { - key := vdi.OperatorAddr.String() - validator := sk.Validator(ctx, vdi.OperatorAddr) - totalDelAccumFromVal[key] = vdi.GetTotalDelAccum(height, - validator.GetDelegatorShares()) - - // also initialize the delegation map - totalDelAccum[key] = sdk.ZeroDec() - - return false - } - k.IterateValidatorDistInfos(ctx, iterVal) - - // iterate the delegations - iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { - key := ddi.ValOperatorAddr.String() - delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) - totalDelAccum[key] = totalDelAccum[key].Add( - ddi.GetDelAccum(height, delegation.GetShares())) - return false + outstanding := k.GetOutstandingRewards(ctx) + if outstanding.HasNegative() { + return fmt.Errorf("Negative outstanding coins: %v", outstanding) } - k.IterateDelegationDistInfos(ctx, iterDel) - - // compare - for key, delAccumFromVal := range totalDelAccumFromVal { - sumDelAccum := totalDelAccum[key] - - if !sumDelAccum.Equal(delAccumFromVal) { - - logDelAccums := "" - iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { - keyLog := ddi.ValOperatorAddr.String() - if keyLog == key { - delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) - accum := ddi.GetDelAccum(height, delegation.GetShares()) - if accum.IsPositive() { - logDelAccums += fmt.Sprintf("\n\t\tdel: %v, accum: %v, power: %v", - ddi.DelegatorAddr.String(), - accum.String(), - delegation.GetShares()) - } - } - return false - } - k.IterateDelegationDistInfos(ctx, iterDel) - - operAddr, err := sdk.ValAddressFromBech32(key) - if err != nil { - panic(err) - } - validator := sk.Validator(ctx, operAddr) - - return fmt.Errorf("delegator accum invariance: \n"+ - "\tvalidator key: %v\n"+ - "\tvalidator: %+v\n"+ - "\tsum delegator accum: %v\n"+ - "\tvalidator's total delegator accum: %v\n"+ - "\tlog of delegations with accum: %v\n", - key, validator, sumDelAccum.String(), - delAccumFromVal.String(), logDelAccums) - } - } - return nil } } @@ -135,41 +38,28 @@ func DelAccumInvariants(k distr.Keeper, sk distr.StakingKeeper) simulation.Invar // CanWithdrawInvariant checks that current rewards can be completely withdrawn func CanWithdrawInvariant(k distr.Keeper, sk staking.Keeper) simulation.Invariant { return func(ctx sdk.Context) error { - // we don't want to write the changes + + // cache, we don't want to write changes ctx, _ = ctx.CacheContext() - // withdraw all delegator & validator rewards - vdiIter := func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { - err := k.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr) - if err != nil { - panic(err) - } + // iterate over all bonded validators, withdraw commission + sk.IterateValidators(ctx, func(_ int64, val sdk.Validator) (stop bool) { + _ = k.WithdrawValidatorCommission(ctx, val.GetOperator()) return false - } - k.IterateValidatorDistInfos(ctx, vdiIter) + }) - ddiIter := func(_ int64, distInfo distr.DelegationDistInfo) (stop bool) { - err := k.WithdrawDelegationReward( - ctx, distInfo.DelegatorAddr, distInfo.ValOperatorAddr) - if err != nil { - panic(err) - } - return false + // iterate over all current delegations, withdraw rewards + dels := sk.GetAllDelegations(ctx) + for _, delegation := range dels { + _ = k.WithdrawDelegationRewards(ctx, delegation.DelegatorAddr, delegation.ValidatorAddr) } - k.IterateDelegationDistInfos(ctx, ddiIter) - // assert that the fee pool is empty - feePool := k.GetFeePool(ctx) - if !feePool.TotalValAccum.Accum.IsZero() { - return fmt.Errorf("unexpected leftover validator accum") - } - bondDenom := sk.GetParams(ctx).BondDenom - if !feePool.ValPool.AmountOf(bondDenom).IsZero() { - return fmt.Errorf("unexpected leftover validator pool coins: %v", - feePool.ValPool.AmountOf(bondDenom).String()) + remaining := k.GetOutstandingRewards(ctx) + + if len(remaining) > 0 && remaining[0].Amount.LT(sdk.ZeroDec()) { + return fmt.Errorf("Negative remaining coins: %v", remaining) } - // all ok return nil } } diff --git a/x/distribution/simulation/msgs.go b/x/distribution/simulation/msgs.go index 62881c5f958e..9f979d86d47c 100644 --- a/x/distribution/simulation/msgs.go +++ b/x/distribution/simulation/msgs.go @@ -39,33 +39,6 @@ func SimulateMsgSetWithdrawAddress(m auth.AccountKeeper, k distribution.Keeper) } } -// SimulateMsgWithdrawDelegatorRewardsAll -func SimulateMsgWithdrawDelegatorRewardsAll(m auth.AccountKeeper, k distribution.Keeper) simulation.Operation { - handler := distribution.NewHandler(k) - return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, - accs []simulation.Account, event func(string)) ( - action string, fOp []simulation.FutureOperation, err error) { - - account := simulation.RandomAcc(r, accs) - msg := distribution.NewMsgWithdrawDelegatorRewardsAll(account.Address) - - if msg.ValidateBasic() != nil { - return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) - } - - ctx, write := ctx.CacheContext() - result := handler(ctx, msg) - if result.IsOK() { - write() - } - - event(fmt.Sprintf("distribution/MsgWithdrawDelegatorRewardsAll/%v", result.IsOK())) - - action = fmt.Sprintf("TestMsgWithdrawDelegatorRewardsAll: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) - return action, nil, nil - } -} - // SimulateMsgWithdrawDelegatorReward func SimulateMsgWithdrawDelegatorReward(m auth.AccountKeeper, k distribution.Keeper) simulation.Operation { handler := distribution.NewHandler(k) @@ -94,15 +67,15 @@ func SimulateMsgWithdrawDelegatorReward(m auth.AccountKeeper, k distribution.Kee } } -// SimulateMsgWithdrawValidatorRewardsAll -func SimulateMsgWithdrawValidatorRewardsAll(m auth.AccountKeeper, k distribution.Keeper) simulation.Operation { +// SimulateMsgWithdrawValidatorCommission +func SimulateMsgWithdrawValidatorCommission(m auth.AccountKeeper, k distribution.Keeper) simulation.Operation { handler := distribution.NewHandler(k) return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simulation.Account, event func(string)) ( action string, fOp []simulation.FutureOperation, err error) { account := simulation.RandomAcc(r, accs) - msg := distribution.NewMsgWithdrawValidatorRewardsAll(sdk.ValAddress(account.Address)) + msg := distribution.NewMsgWithdrawValidatorCommission(sdk.ValAddress(account.Address)) if msg.ValidateBasic() != nil { return "", nil, fmt.Errorf("expected msg to pass ValidateBasic: %s", msg.GetSignBytes()) @@ -114,9 +87,9 @@ func SimulateMsgWithdrawValidatorRewardsAll(m auth.AccountKeeper, k distribution write() } - event(fmt.Sprintf("distribution/MsgWithdrawValidatorRewardsAll/%v", result.IsOK())) + event(fmt.Sprintf("distribution/MsgWithdrawValidatorCommission/%v", result.IsOK())) - action = fmt.Sprintf("TestMsgWithdrawValidatorRewardsAll: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) + action = fmt.Sprintf("TestMsgWithdrawValidatorCommission: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) return action, nil, nil } } diff --git a/x/distribution/types/codec.go b/x/distribution/types/codec.go index 10b2dc2fb5ea..30ae415720f0 100644 --- a/x/distribution/types/codec.go +++ b/x/distribution/types/codec.go @@ -6,9 +6,8 @@ import ( // Register concrete types on codec codec func RegisterCodec(cdc *codec.Codec) { - cdc.RegisterConcrete(MsgWithdrawDelegatorRewardsAll{}, "cosmos-sdk/MsgWithdrawDelegationRewardsAll", nil) cdc.RegisterConcrete(MsgWithdrawDelegatorReward{}, "cosmos-sdk/MsgWithdrawDelegationReward", nil) - cdc.RegisterConcrete(MsgWithdrawValidatorRewardsAll{}, "cosmos-sdk/MsgWithdrawValidatorRewardsAll", nil) + cdc.RegisterConcrete(MsgWithdrawValidatorCommission{}, "cosmos-sdk/MsgWithdrawValidatorCommission", nil) cdc.RegisterConcrete(MsgSetWithdrawAddress{}, "cosmos-sdk/MsgModifyWithdrawAddress", nil) } diff --git a/x/distribution/types/delegation_info.go b/x/distribution/types/delegation_info.go deleted file mode 100644 index 6068752f21c3..000000000000 --- a/x/distribution/types/delegation_info.go +++ /dev/null @@ -1,122 +0,0 @@ -package types - -import ( - "fmt" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// distribution info for a delegation - used to determine entitled rewards -type DelegationDistInfo struct { - DelegatorAddr sdk.AccAddress `json:"delegator_addr"` - ValOperatorAddr sdk.ValAddress `json:"val_operator_addr"` - DelPoolWithdrawalHeight int64 `json:"del_pool_withdrawal_height"` // last time this delegation withdrew rewards -} - -func NewDelegationDistInfo(delegatorAddr sdk.AccAddress, valOperatorAddr sdk.ValAddress, - currentHeight int64) DelegationDistInfo { - - return DelegationDistInfo{ - DelegatorAddr: delegatorAddr, - ValOperatorAddr: valOperatorAddr, - DelPoolWithdrawalHeight: currentHeight, - } -} - -// Get the calculated accum of this delegator at the provided height -func (di DelegationDistInfo) GetDelAccum(height int64, delegatorShares sdk.Dec) sdk.Dec { - blocks := height - di.DelPoolWithdrawalHeight - accum := delegatorShares.MulInt(sdk.NewInt(blocks)) - - // defensive check - if accum.IsNegative() { - panic(fmt.Sprintf("negative accum: %v\n"+ - "\theight: %v\n"+ - "\tdelegation_dist_info: %v\n"+ - "\tdelegator_shares: %v\n", - accum.String(), height, di, delegatorShares)) - } - return accum -} - -// Withdraw rewards from delegator. -// Among many things, it does: -// * updates validator info's total del accum -// * calls vi.TakeFeePoolRewards, which: -// * updates validator info's FeePoolWithdrawalHeight, thus setting accum to 0 -// * updates fee pool to latest height and total val accum w/ given totalBonded -// (see comment on TakeFeePoolRewards for more info) -func (di DelegationDistInfo) WithdrawRewards(wc WithdrawContext, vi ValidatorDistInfo, - totalDelShares, delegatorShares sdk.Dec) ( - DelegationDistInfo, ValidatorDistInfo, FeePool, DecCoins) { - - fp := wc.FeePool - vi = vi.UpdateTotalDelAccum(wc.Height, totalDelShares) - - // Break out to prevent a divide by zero. - if vi.DelAccum.Accum.IsZero() { - di.DelPoolWithdrawalHeight = wc.Height - return di, vi, fp, DecCoins{} - } - - vi, fp = vi.TakeFeePoolRewards(wc) - - accum := di.GetDelAccum(wc.Height, delegatorShares) - di.DelPoolWithdrawalHeight = wc.Height - - withdrawalTokens := vi.DelPool.MulDec(accum).QuoDec(vi.DelAccum.Accum) - - // Clip withdrawal tokens by pool, due to possible rounding errors. - // This rounding error may be introduced upon multiplication since - // we're clipping decimal digits, and then when we divide by a number ~1 or - // < 1, the error doesn't get "buried", and if << 1 it'll get amplified. - // more: https://github.com/cosmos/cosmos-sdk/issues/2888#issuecomment-441387987 - for i, decCoin := range withdrawalTokens { - poolDenomAmount := vi.DelPool.AmountOf(decCoin.Denom) - if decCoin.Amount.GT(poolDenomAmount) { - withdrawalTokens[i] = NewDecCoinFromDec(decCoin.Denom, poolDenomAmount) - } - } - - // defensive check for impossible accum ratios - if accum.GT(vi.DelAccum.Accum) { - panic(fmt.Sprintf("accum > vi.DelAccum.Accum:\n"+ - "\taccum\t\t\t%v\n"+ - "\tvi.DelAccum.Accum\t%v\n", - accum, vi.DelAccum.Accum)) - } - - remDelPool := vi.DelPool.Minus(withdrawalTokens) - - // defensive check - if remDelPool.HasNegative() { - panic(fmt.Sprintf("negative remDelPool: %v\n"+ - "\tvi.DelPool\t\t%v\n"+ - "\taccum\t\t\t%v\n"+ - "\tvi.DelAccum.Accum\t%v\n"+ - "\twithdrawalTokens\t%v\n", - remDelPool, vi.DelPool, accum, - vi.DelAccum.Accum, withdrawalTokens)) - } - - vi.DelPool = remDelPool - vi.DelAccum.Accum = vi.DelAccum.Accum.Sub(accum) - - return di, vi, fp, withdrawalTokens -} - -// get the delegators rewards at this current state, -func (di DelegationDistInfo) CurrentRewards(wc WithdrawContext, vi ValidatorDistInfo, - totalDelShares, delegatorShares sdk.Dec) DecCoins { - - totalDelAccum := vi.GetTotalDelAccum(wc.Height, totalDelShares) - - if vi.DelAccum.Accum.IsZero() { - return DecCoins{} - } - - rewards := vi.CurrentPoolRewards(wc) - accum := di.GetDelAccum(wc.Height, delegatorShares) - tokens := rewards.MulDec(accum).QuoDec(totalDelAccum) - return tokens -} diff --git a/x/distribution/types/delegation_info_test.go b/x/distribution/types/delegation_info_test.go deleted file mode 100644 index e97ea80510a6..000000000000 --- a/x/distribution/types/delegation_info_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package types - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - sdk "github.com/cosmos/cosmos-sdk/types" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" -) - -func TestWithdrawRewards(t *testing.T) { - - // initialize - height := int64(0) - fp := InitialFeePool() - vi := NewValidatorDistInfo(valAddr1, height) - commissionRate := sdk.NewDecWithPrec(2, 2) - validatorTokens := sdk.NewDec(10) - validatorDelShares := sdk.NewDec(10) - totalBondedTokens := validatorTokens.Add(sdk.NewDec(90)) // validator-1 is 10% of total power - - di1 := NewDelegationDistInfo(delAddr1, valAddr1, height) - di1Shares := sdk.NewDec(5) // this delegator has half the shares in the validator - - di2 := NewDelegationDistInfo(delAddr2, valAddr1, height) - di2Shares := sdk.NewDec(5) - - // simulate adding some stake for inflation - height = 10 - fp.ValPool = DecCoins{NewDecCoin(stakingTypes.DefaultBondDenom, 1000)} - - // withdraw rewards - wc := NewWithdrawContext(fp, height, - totalBondedTokens, validatorTokens, commissionRate) - di1, vi, fp, rewardRecv1 := di1.WithdrawRewards(wc, vi, - validatorDelShares, di1Shares) - - assert.Equal(t, height, di1.DelPoolWithdrawalHeight) - assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(49), vi.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(2), vi.ValCommission[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(49), rewardRecv1[0].Amount)) - - // add more blocks and inflation - height = 20 - fp.ValPool[0].Amount = fp.ValPool[0].Amount.Add(sdk.NewDec(1000)) - - // withdraw rewards - wc = NewWithdrawContext(fp, height, - totalBondedTokens, validatorTokens, commissionRate) - di2, vi, fp, rewardRecv2 := di2.WithdrawRewards(wc, vi, - validatorDelShares, di2Shares) - - assert.Equal(t, height, di2.DelPoolWithdrawalHeight) - assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(49), vi.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(4), vi.ValCommission[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(98), rewardRecv2[0].Amount)) -} diff --git a/x/distribution/types/delegator.go b/x/distribution/types/delegator.go new file mode 100644 index 000000000000..550dab1f14a2 --- /dev/null +++ b/x/distribution/types/delegator.go @@ -0,0 +1,27 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// starting info for a delegator reward period +// tracks the previous validator period, the delegation's amount +// of staking token, and the creation height (to check later on +// if any slashes have occurred) +// NOTE that even though validators are slashed to whole staking tokens, the +// delegators within the validator may be left with less than a full token, +// thus sdk.Dec is used +type DelegatorStartingInfo struct { + PreviousPeriod uint64 `json:"previous_period"` // period at which the delegation should withdraw starting from + Stake sdk.Dec `json:"stake"` // amount of staking token delegated + Height uint64 `json:"height"` // height at which delegation was created +} + +// create a new DelegatorStartingInfo +func NewDelegatorStartingInfo(previousPeriod uint64, stake sdk.Dec, height uint64) DelegatorStartingInfo { + return DelegatorStartingInfo{ + PreviousPeriod: previousPeriod, + Stake: stake, + Height: height, + } +} diff --git a/x/distribution/types/errors.go b/x/distribution/types/errors.go index 36de11f740c8..cdb6a7233d50 100644 --- a/x/distribution/types/errors.go +++ b/x/distribution/types/errors.go @@ -8,9 +8,10 @@ import ( type CodeType = sdk.CodeType const ( - DefaultCodespace sdk.CodespaceType = "DISTR" - CodeInvalidInput CodeType = 103 - CodeNoDistributionInfo CodeType = 104 + DefaultCodespace sdk.CodespaceType = "DISTR" + CodeInvalidInput CodeType = 103 + CodeNoDistributionInfo CodeType = 104 + CodeNoValidatorCommission CodeType = 105 ) func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error { @@ -28,3 +29,6 @@ func ErrNoDelegationDistInfo(codespace sdk.CodespaceType) sdk.Error { func ErrNoValidatorDistInfo(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeNoDistributionInfo, "no validator distribution info") } +func ErrNoValidatorCommission(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeNoValidatorCommission, "no validator commission to withdraw") +} diff --git a/x/distribution/types/fee_pool.go b/x/distribution/types/fee_pool.go index 2f921220c772..1bf2083c93c1 100644 --- a/x/distribution/types/fee_pool.go +++ b/x/distribution/types/fee_pool.go @@ -8,46 +8,18 @@ import ( // global fee pool for distribution type FeePool struct { - TotalValAccum TotalAccum `json:"val_accum"` // total valdator accum held by validators - ValPool DecCoins `json:"val_pool"` // funds for all validators which have yet to be withdrawn - CommunityPool DecCoins `json:"community_pool"` // pool for community funds yet to be spent -} - -// update total validator accumulation factor -// NOTE: Do not call this except from ValidatorDistInfo.TakeFeePoolRewards(). -func (f FeePool) UpdateTotalValAccum(height int64, totalBondedTokens sdk.Dec) FeePool { - f.TotalValAccum = f.TotalValAccum.UpdateForNewHeight(height, totalBondedTokens) - return f -} - -// get the total validator accum for the fee pool without modifying the state -func (f FeePool) GetTotalValAccum(height int64, totalBondedTokens sdk.Dec) sdk.Dec { - return f.TotalValAccum.GetAccum(height, totalBondedTokens) + CommunityPool sdk.DecCoins `json:"community_pool"` // pool for community funds yet to be spent } // zero fee pool func InitialFeePool() FeePool { return FeePool{ - TotalValAccum: NewTotalAccum(0), - ValPool: DecCoins{}, - CommunityPool: DecCoins{}, + CommunityPool: sdk.DecCoins{}, } } // ValidateGenesis validates the fee pool for a genesis state func (f FeePool) ValidateGenesis() error { - if f.TotalValAccum.Accum.IsNegative() { - return fmt.Errorf("negative accum in distribution fee pool, is %v", - f.TotalValAccum.Accum.String()) - } - if f.TotalValAccum.UpdateHeight < 0 { - return fmt.Errorf("negative update height in distribution fee pool, is %v", - f.TotalValAccum.UpdateHeight) - } - if f.ValPool.HasNegative() { - return fmt.Errorf("negative ValPool in distribution fee pool, is %v", - f.ValPool) - } if f.CommunityPool.HasNegative() { return fmt.Errorf("negative CommunityPool in distribution fee pool, is %v", f.CommunityPool) @@ -55,3 +27,8 @@ func (f FeePool) ValidateGenesis() error { return nil } + +// outstanding (un-withdrawn) rewards for everyone +// excludes the community pool +// inexpensive to track, allows simple sanity checks +type OutstandingRewards = sdk.DecCoins diff --git a/x/distribution/types/fee_pool_test.go b/x/distribution/types/fee_pool_test.go index ceb908d7f70d..bc85e56df2bf 100644 --- a/x/distribution/types/fee_pool_test.go +++ b/x/distribution/types/fee_pool_test.go @@ -8,13 +8,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -func TestUpdateTotalValAccum(t *testing.T) { +func TestValidateGenesis(t *testing.T) { fp := InitialFeePool() + require.Nil(t, fp.ValidateGenesis()) - fp = fp.UpdateTotalValAccum(5, sdk.NewDec(3)) - require.True(sdk.DecEq(t, sdk.NewDec(15), fp.TotalValAccum.Accum)) + fp2 := FeePool{CommunityPool: sdk.DecCoins{{"stake", sdk.NewDec(-1)}}} + require.NotNil(t, fp2.ValidateGenesis()) - fp = fp.UpdateTotalValAccum(8, sdk.NewDec(2)) - require.True(sdk.DecEq(t, sdk.NewDec(21), fp.TotalValAccum.Accum)) } diff --git a/x/distribution/types/genesis.go b/x/distribution/types/genesis.go index a94eb7005fb6..63dd2d252afe 100644 --- a/x/distribution/types/genesis.go +++ b/x/distribution/types/genesis.go @@ -13,62 +13,92 @@ type DelegatorWithdrawInfo struct { WithdrawAddr sdk.AccAddress `json:"withdraw_addr"` } +// used for import / export via genesis json +type ValidatorAccumulatedCommissionRecord struct { + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + Accumulated ValidatorAccumulatedCommission `json:"accumulated"` +} + +// used for import / export via genesis json +type ValidatorHistoricalRewardsRecord struct { + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + Period uint64 `json:"period"` + Rewards ValidatorHistoricalRewards `json:"rewards"` +} + +// used for import / export via genesis json +type ValidatorCurrentRewardsRecord struct { + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + Rewards ValidatorCurrentRewards `json:"rewards"` +} + +// used for import / export via genesis json +type DelegatorStartingInfoRecord struct { + DelegatorAddr sdk.AccAddress `json:"delegator_addr"` + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + StartingInfo DelegatorStartingInfo `json:"starting_info"` +} + +// used for import / export via genesis json +type ValidatorSlashEventRecord struct { + ValidatorAddr sdk.ValAddress `json:"validator_addr"` + Height uint64 `json:"height"` + Event ValidatorSlashEvent `json:"validator_slash_event"` +} + // GenesisState - all distribution state that must be provided at genesis type GenesisState struct { - FeePool FeePool `json:"fee_pool"` - CommunityTax sdk.Dec `json:"community_tax"` - BaseProposerReward sdk.Dec `json:"base_proposer_reward"` - BonusProposerReward sdk.Dec `json:"bonus_proposer_reward"` - ValidatorDistInfos []ValidatorDistInfo `json:"validator_dist_infos"` - DelegationDistInfos []DelegationDistInfo `json:"delegator_dist_infos"` - DelegatorWithdrawInfos []DelegatorWithdrawInfo `json:"delegator_withdraw_infos"` - PreviousProposer sdk.ConsAddress `json:"previous_proposer"` + FeePool FeePool `json:"fee_pool"` + CommunityTax sdk.Dec `json:"community_tax"` + BaseProposerReward sdk.Dec `json:"base_proposer_reward"` + BonusProposerReward sdk.Dec `json:"bonus_proposer_reward"` + DelegatorWithdrawInfos []DelegatorWithdrawInfo `json:"delegator_withdraw_infos"` + PreviousProposer sdk.ConsAddress `json:"previous_proposer"` + OutstandingRewards sdk.DecCoins `json:"outstanding_rewards"` + ValidatorAccumulatedCommissions []ValidatorAccumulatedCommissionRecord `json:"validator_accumulated_commissions"` + ValidatorHistoricalRewards []ValidatorHistoricalRewardsRecord `json:"validator_historical_rewards"` + ValidatorCurrentRewards []ValidatorCurrentRewardsRecord `json:"validator_current_rewards"` + DelegatorStartingInfos []DelegatorStartingInfoRecord `json:"delegator_starting_infos"` + ValidatorSlashEvents []ValidatorSlashEventRecord `json:"validator_slash_events"` } func NewGenesisState(feePool FeePool, communityTax, baseProposerReward, bonusProposerReward sdk.Dec, - vdis []ValidatorDistInfo, ddis []DelegationDistInfo, dwis []DelegatorWithdrawInfo, pp sdk.ConsAddress) GenesisState { + dwis []DelegatorWithdrawInfo, pp sdk.ConsAddress, r OutstandingRewards, + acc []ValidatorAccumulatedCommissionRecord, historical []ValidatorHistoricalRewardsRecord, + cur []ValidatorCurrentRewardsRecord, dels []DelegatorStartingInfoRecord, + slashes []ValidatorSlashEventRecord) GenesisState { return GenesisState{ - FeePool: feePool, - CommunityTax: communityTax, - BaseProposerReward: baseProposerReward, - BonusProposerReward: bonusProposerReward, - ValidatorDistInfos: vdis, - DelegationDistInfos: ddis, - DelegatorWithdrawInfos: dwis, - PreviousProposer: pp, + FeePool: feePool, + CommunityTax: communityTax, + BaseProposerReward: baseProposerReward, + BonusProposerReward: bonusProposerReward, + DelegatorWithdrawInfos: dwis, + PreviousProposer: pp, + OutstandingRewards: r, + ValidatorAccumulatedCommissions: acc, + ValidatorHistoricalRewards: historical, + ValidatorCurrentRewards: cur, + DelegatorStartingInfos: dels, + ValidatorSlashEvents: slashes, } } // get raw genesis raw message for testing func DefaultGenesisState() GenesisState { return GenesisState{ - FeePool: InitialFeePool(), - CommunityTax: sdk.NewDecWithPrec(2, 2), // 2% - BaseProposerReward: sdk.NewDecWithPrec(1, 2), // 1% - BonusProposerReward: sdk.NewDecWithPrec(4, 2), // 4% - } -} - -// default genesis utility function, initialize for starting validator set -func DefaultGenesisWithValidators(valAddrs []sdk.ValAddress) GenesisState { - - vdis := make([]ValidatorDistInfo, len(valAddrs)) - ddis := make([]DelegationDistInfo, len(valAddrs)) - - for i, valAddr := range valAddrs { - vdis[i] = NewValidatorDistInfo(valAddr, 0) - accAddr := sdk.AccAddress(valAddr) - ddis[i] = NewDelegationDistInfo(accAddr, valAddr, 0) - } - - return GenesisState{ - FeePool: InitialFeePool(), - CommunityTax: sdk.NewDecWithPrec(2, 2), // 2% - BaseProposerReward: sdk.NewDecWithPrec(1, 2), // 1% - BonusProposerReward: sdk.NewDecWithPrec(4, 2), // 4% - ValidatorDistInfos: vdis, - DelegationDistInfos: ddis, + FeePool: InitialFeePool(), + CommunityTax: sdk.NewDecWithPrec(2, 2), // 2% + BaseProposerReward: sdk.NewDecWithPrec(1, 2), // 1% + BonusProposerReward: sdk.NewDecWithPrec(4, 2), // 4% + DelegatorWithdrawInfos: []DelegatorWithdrawInfo{}, + PreviousProposer: nil, + OutstandingRewards: sdk.DecCoins{}, + ValidatorAccumulatedCommissions: []ValidatorAccumulatedCommissionRecord{}, + ValidatorHistoricalRewards: []ValidatorHistoricalRewardsRecord{}, + ValidatorCurrentRewards: []ValidatorCurrentRewardsRecord{}, + DelegatorStartingInfos: []DelegatorStartingInfoRecord{}, + ValidatorSlashEvents: []ValidatorSlashEventRecord{}, } } diff --git a/x/distribution/types/keepers.go b/x/distribution/types/keepers.go index 4411ebc8de61..e734d47fcdfe 100644 --- a/x/distribution/types/keepers.go +++ b/x/distribution/types/keepers.go @@ -19,7 +19,7 @@ type BankKeeper interface { AddCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) } -// from ante handler +// expected fee collection keeper type FeeCollectionKeeper interface { GetCollectedFees(ctx sdk.Context) sdk.Coins ClearCollectedFees(ctx sdk.Context) diff --git a/x/distribution/types/msg.go b/x/distribution/types/msg.go index 1d421c56d815..8100e51562a3 100644 --- a/x/distribution/types/msg.go +++ b/x/distribution/types/msg.go @@ -5,11 +5,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// Verify interface at compile time -var _, _ sdk.Msg = &MsgSetWithdrawAddress{}, &MsgWithdrawDelegatorRewardsAll{} -var _, _ sdk.Msg = &MsgWithdrawDelegatorReward{}, &MsgWithdrawValidatorRewardsAll{} +// name to identify transaction types +const MsgRoute = "distr" -//______________________________________________________________________ +// Verify interface at compile time +var _, _, _ sdk.Msg = &MsgSetWithdrawAddress{}, &MsgWithdrawDelegatorReward{}, &MsgWithdrawValidatorCommission{} // msg struct for changing the withdraw address for a delegator (or validator self-delegation) type MsgSetWithdrawAddress struct { @@ -24,7 +24,7 @@ func NewMsgSetWithdrawAddress(delAddr, withdrawAddr sdk.AccAddress) MsgSetWithdr } } -func (msg MsgSetWithdrawAddress) Route() string { return RouterKey } +func (msg MsgSetWithdrawAddress) Route() string { return MsgRoute } func (msg MsgSetWithdrawAddress) Type() string { return "set_withdraw_address" } // Return address that must sign over msg.GetSignBytes() @@ -52,46 +52,6 @@ func (msg MsgSetWithdrawAddress) ValidateBasic() sdk.Error { return nil } -//______________________________________________________________________ - -// msg struct for delegation withdraw for all of the delegator's delegations -type MsgWithdrawDelegatorRewardsAll struct { - DelegatorAddr sdk.AccAddress `json:"delegator_addr"` -} - -func NewMsgWithdrawDelegatorRewardsAll(delAddr sdk.AccAddress) MsgWithdrawDelegatorRewardsAll { - return MsgWithdrawDelegatorRewardsAll{ - DelegatorAddr: delAddr, - } -} - -func (msg MsgWithdrawDelegatorRewardsAll) Route() string { return RouterKey } -func (msg MsgWithdrawDelegatorRewardsAll) Type() string { return "withdraw_delegation_rewards_all" } - -// Return address that must sign over msg.GetSignBytes() -func (msg MsgWithdrawDelegatorRewardsAll) GetSigners() []sdk.AccAddress { - return []sdk.AccAddress{sdk.AccAddress(msg.DelegatorAddr)} -} - -// get the bytes for the message signer to sign on -func (msg MsgWithdrawDelegatorRewardsAll) GetSignBytes() []byte { - b, err := MsgCdc.MarshalJSON(msg) - if err != nil { - panic(err) - } - return sdk.MustSortJSON(b) -} - -// quick validity check -func (msg MsgWithdrawDelegatorRewardsAll) ValidateBasic() sdk.Error { - if msg.DelegatorAddr == nil { - return ErrNilDelegatorAddr(DefaultCodespace) - } - return nil -} - -//______________________________________________________________________ - // msg struct for delegation withdraw from a single validator type MsgWithdrawDelegatorReward struct { DelegatorAddr sdk.AccAddress `json:"delegator_addr"` @@ -105,7 +65,7 @@ func NewMsgWithdrawDelegatorReward(delAddr sdk.AccAddress, valAddr sdk.ValAddres } } -func (msg MsgWithdrawDelegatorReward) Route() string { return RouterKey } +func (msg MsgWithdrawDelegatorReward) Route() string { return MsgRoute } func (msg MsgWithdrawDelegatorReward) Type() string { return "withdraw_delegation_reward" } // Return address that must sign over msg.GetSignBytes() @@ -133,29 +93,27 @@ func (msg MsgWithdrawDelegatorReward) ValidateBasic() sdk.Error { return nil } -//______________________________________________________________________ - // msg struct for validator withdraw -type MsgWithdrawValidatorRewardsAll struct { +type MsgWithdrawValidatorCommission struct { ValidatorAddr sdk.ValAddress `json:"validator_addr"` } -func NewMsgWithdrawValidatorRewardsAll(valAddr sdk.ValAddress) MsgWithdrawValidatorRewardsAll { - return MsgWithdrawValidatorRewardsAll{ +func NewMsgWithdrawValidatorCommission(valAddr sdk.ValAddress) MsgWithdrawValidatorCommission { + return MsgWithdrawValidatorCommission{ ValidatorAddr: valAddr, } } -func (msg MsgWithdrawValidatorRewardsAll) Route() string { return RouterKey } -func (msg MsgWithdrawValidatorRewardsAll) Type() string { return "withdraw_validator_rewards_all" } +func (msg MsgWithdrawValidatorCommission) Route() string { return MsgRoute } +func (msg MsgWithdrawValidatorCommission) Type() string { return "withdraw_validator_rewards_all" } // Return address that must sign over msg.GetSignBytes() -func (msg MsgWithdrawValidatorRewardsAll) GetSigners() []sdk.AccAddress { +func (msg MsgWithdrawValidatorCommission) GetSigners() []sdk.AccAddress { return []sdk.AccAddress{sdk.AccAddress(msg.ValidatorAddr.Bytes())} } // get the bytes for the message signer to sign on -func (msg MsgWithdrawValidatorRewardsAll) GetSignBytes() []byte { +func (msg MsgWithdrawValidatorCommission) GetSignBytes() []byte { b, err := MsgCdc.MarshalJSON(msg) if err != nil { panic(err) @@ -164,7 +122,7 @@ func (msg MsgWithdrawValidatorRewardsAll) GetSignBytes() []byte { } // quick validity check -func (msg MsgWithdrawValidatorRewardsAll) ValidateBasic() sdk.Error { +func (msg MsgWithdrawValidatorCommission) ValidateBasic() sdk.Error { if msg.ValidatorAddr == nil { return ErrNilValidatorAddr(DefaultCodespace) } diff --git a/x/distribution/types/msg_test.go b/x/distribution/types/msg_test.go index b0c0dd9eb4af..4fe6125a2cb8 100644 --- a/x/distribution/types/msg_test.go +++ b/x/distribution/types/msg_test.go @@ -8,7 +8,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" ) -// test ValidateBasic for MsgCreateValidator +// test ValidateBasic for MsgSetWithdrawAddress func TestMsgSetWithdrawAddress(t *testing.T) { tests := []struct { delegatorAddr sdk.AccAddress @@ -32,7 +32,7 @@ func TestMsgSetWithdrawAddress(t *testing.T) { } } -// test ValidateBasic for MsgEditValidator +// test ValidateBasic for MsgWithdrawDelegatorReward func TestMsgWithdrawDelegatorReward(t *testing.T) { tests := []struct { delegatorAddr sdk.AccAddress @@ -54,27 +54,8 @@ func TestMsgWithdrawDelegatorReward(t *testing.T) { } } -// test ValidateBasic and GetSigners for MsgCreateValidatorOnBehalfOf -func TestMsgWithdrawDelegatorRewardsAll(t *testing.T) { - tests := []struct { - delegatorAddr sdk.AccAddress - expectPass bool - }{ - {delAddr1, true}, - {emptyDelAddr, false}, - } - for i, tc := range tests { - msg := NewMsgWithdrawDelegatorRewardsAll(tc.delegatorAddr) - if tc.expectPass { - require.Nil(t, msg.ValidateBasic(), "test index: %v", i) - } else { - require.NotNil(t, msg.ValidateBasic(), "test index: %v", i) - } - } -} - -// test ValidateBasic for MsgDelegate -func TestMsgWithdrawValidatorRewardsAll(t *testing.T) { +// test ValidateBasic for MsgWithdrawValidatorCommission +func TestMsgWithdrawValidatorCommission(t *testing.T) { tests := []struct { validatorAddr sdk.ValAddress expectPass bool @@ -83,7 +64,7 @@ func TestMsgWithdrawValidatorRewardsAll(t *testing.T) { {emptyValAddr, false}, } for i, tc := range tests { - msg := NewMsgWithdrawValidatorRewardsAll(tc.validatorAddr) + msg := NewMsgWithdrawValidatorCommission(tc.validatorAddr) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test index: %v", i) } else { diff --git a/x/distribution/types/total_accum.go b/x/distribution/types/total_accum.go deleted file mode 100644 index 0915847a5ef6..000000000000 --- a/x/distribution/types/total_accum.go +++ /dev/null @@ -1,40 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// total accumulation tracker -type TotalAccum struct { - UpdateHeight int64 `json:"update_height"` - Accum sdk.Dec `json:"accum"` -} - -func NewTotalAccum(height int64) TotalAccum { - return TotalAccum{ - UpdateHeight: height, - Accum: sdk.ZeroDec(), - } -} - -// update total accumulation factor for the new height -// CONTRACT: height should be greater than the old height -func (ta TotalAccum) UpdateForNewHeight(height int64, accumCreatedPerBlock sdk.Dec) TotalAccum { - blocks := height - ta.UpdateHeight - if blocks < 0 { - panic("reverse updated for new height") - } - ta.Accum = ta.Accum.Add(accumCreatedPerBlock.MulInt(sdk.NewInt(blocks))) - ta.UpdateHeight = height - return ta -} - -// get total accumulation factor for the given height -// CONTRACT: height should be greater than the old height -func (ta TotalAccum) GetAccum(height int64, accumCreatedPerBlock sdk.Dec) sdk.Dec { - blocks := height - ta.UpdateHeight - if blocks < 0 { - panic("reverse updated for new height") - } - return ta.Accum.Add(accumCreatedPerBlock.MulInt(sdk.NewInt(blocks))) -} diff --git a/x/distribution/types/total_accum_test.go b/x/distribution/types/total_accum_test.go deleted file mode 100644 index 3984612add43..000000000000 --- a/x/distribution/types/total_accum_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package types - -import ( - "testing" - - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" -) - -func TestTotalAccumUpdateForNewHeight(t *testing.T) { - - ta := NewTotalAccum(0) - - ta = ta.UpdateForNewHeight(5, sdk.NewDec(3)) - require.True(sdk.DecEq(t, sdk.NewDec(15), ta.Accum)) - - ta = ta.UpdateForNewHeight(8, sdk.NewDec(2)) - require.True(sdk.DecEq(t, sdk.NewDec(21), ta.Accum)) -} diff --git a/x/distribution/types/validator.go b/x/distribution/types/validator.go new file mode 100644 index 000000000000..8d9c10f0cf73 --- /dev/null +++ b/x/distribution/types/validator.go @@ -0,0 +1,52 @@ +package types + +import ( + sdk "github.com/cosmos/cosmos-sdk/types" +) + +// historical rewards for a validator +// TODO add reference counter, ref https://github.com/cosmos/cosmos-sdk/pull/3099#discussion_r245747051 +// height is implicit within the store key +type ValidatorHistoricalRewards = sdk.DecCoins + +// current rewards and current period for a validator +// kept as a running counter and incremented each block +// as long as the validator's tokens remain constant +type ValidatorCurrentRewards struct { + Rewards sdk.DecCoins `json:"rewards"` // current rewards + Period uint64 `json:"period"` // current period +} + +// create a new ValidatorCurrentRewards +func NewValidatorCurrentRewards(rewards sdk.DecCoins, period uint64) ValidatorCurrentRewards { + return ValidatorCurrentRewards{ + Rewards: rewards, + Period: period, + } +} + +// accumulated commission for a validator +// kept as a running counter, can be withdrawn at any time +type ValidatorAccumulatedCommission = sdk.DecCoins + +// return the initial accumulated commission (zero) +func InitialValidatorAccumulatedCommission() ValidatorAccumulatedCommission { + return ValidatorAccumulatedCommission{} +} + +// validator slash event +// height is implicit within the store key +// needed to calculate appropriate amounts of staking token +// for delegations which withdraw after a slash has occurred +type ValidatorSlashEvent struct { + ValidatorPeriod uint64 `json:"validator_period"` // period when the slash occurred + Fraction sdk.Dec `json:"fraction"` // slash fraction +} + +// create a new ValidatorSlashEvent +func NewValidatorSlashEvent(validatorPeriod uint64, fraction sdk.Dec) ValidatorSlashEvent { + return ValidatorSlashEvent{ + ValidatorPeriod: validatorPeriod, + Fraction: fraction, + } +} diff --git a/x/distribution/types/validator_info.go b/x/distribution/types/validator_info.go deleted file mode 100644 index fe4b755d0c70..000000000000 --- a/x/distribution/types/validator_info.go +++ /dev/null @@ -1,153 +0,0 @@ -package types - -import ( - sdk "github.com/cosmos/cosmos-sdk/types" -) - -// common parameters used in withdraws from validators -type WithdrawContext struct { - FeePool FeePool - Height int64 // block height - TotalPower sdk.Dec // total bonded tokens in the network - ValPower sdk.Dec // validator's bonded tokens - CommissionRate sdk.Dec // validator commission rate -} - -func NewWithdrawContext(feePool FeePool, height int64, totalPower, - valPower, commissionRate sdk.Dec) WithdrawContext { - - return WithdrawContext{ - FeePool: feePool, - Height: height, - TotalPower: totalPower, - ValPower: valPower, - CommissionRate: commissionRate, - } -} - -//_____________________________________________________________________________ - -// distribution info for a particular validator -type ValidatorDistInfo struct { - OperatorAddr sdk.ValAddress `json:"operator_addr"` - - FeePoolWithdrawalHeight int64 `json:"fee_pool_withdrawal_height"` // last height this validator withdrew from the global pool - - DelAccum TotalAccum `json:"del_accum"` // total accumulation factor held by delegators - DelPool DecCoins `json:"del_pool"` // rewards owed to delegators, commission has already been charged (includes proposer reward) - ValCommission DecCoins `json:"val_commission"` // commission collected by this validator (pending withdrawal) -} - -func NewValidatorDistInfo(operatorAddr sdk.ValAddress, currentHeight int64) ValidatorDistInfo { - return ValidatorDistInfo{ - OperatorAddr: operatorAddr, - FeePoolWithdrawalHeight: currentHeight, - DelPool: DecCoins{}, - DelAccum: NewTotalAccum(currentHeight), - ValCommission: DecCoins{}, - } -} - -// update total delegator accumululation -func (vi ValidatorDistInfo) UpdateTotalDelAccum(height int64, totalDelShares sdk.Dec) ValidatorDistInfo { - vi.DelAccum = vi.DelAccum.UpdateForNewHeight(height, totalDelShares) - return vi -} - -// Get the total delegator accum within this validator at the provided height -func (vi ValidatorDistInfo) GetTotalDelAccum(height int64, totalDelShares sdk.Dec) sdk.Dec { - return vi.DelAccum.GetAccum(height, totalDelShares) -} - -// Get the validator accum at the provided height -func (vi ValidatorDistInfo) GetValAccum(height int64, valTokens sdk.Dec) sdk.Dec { - blocks := height - vi.FeePoolWithdrawalHeight - return valTokens.MulInt(sdk.NewInt(blocks)) -} - -// Move any available accumulated fees in the FeePool to the validator's pool -// - updates validator info's FeePoolWithdrawalHeight, thus setting accum to 0 -// - updates fee pool to latest height and total val accum w/ given totalBonded -// This is the only way to update the FeePool's validator TotalAccum. -// NOTE: This algorithm works as long as TakeFeePoolRewards is called after every power change. -// - called in ValidationDistInfo.WithdrawCommission -// - called in DelegationDistInfo.WithdrawRewards -// NOTE: When a delegator unbonds, say, onDelegationSharesModified -> -// WithdrawDelegationReward -> WithdrawRewards -func (vi ValidatorDistInfo) TakeFeePoolRewards(wc WithdrawContext) ( - ValidatorDistInfo, FeePool) { - - fp := wc.FeePool.UpdateTotalValAccum(wc.Height, wc.TotalPower) - - if fp.TotalValAccum.Accum.IsZero() { - vi.FeePoolWithdrawalHeight = wc.Height - return vi, fp - } - - // update the validators pool - accum := vi.GetValAccum(wc.Height, wc.ValPower) - vi.FeePoolWithdrawalHeight = wc.Height - - if accum.GT(fp.TotalValAccum.Accum) { - panic("individual accum should never be greater than the total") - } - withdrawalTokens := fp.ValPool.MulDec(accum).QuoDec(fp.TotalValAccum.Accum) // XXX ensure this doesn't cause problems - remValPool := fp.ValPool.Minus(withdrawalTokens) - - commission := withdrawalTokens.MulDec(wc.CommissionRate) - afterCommission := withdrawalTokens.Minus(commission) - - fp.TotalValAccum.Accum = fp.TotalValAccum.Accum.Sub(accum) - fp.ValPool = remValPool - vi.ValCommission = vi.ValCommission.Plus(commission) - vi.DelPool = vi.DelPool.Plus(afterCommission) - - return vi, fp -} - -// withdraw commission rewards -func (vi ValidatorDistInfo) WithdrawCommission(wc WithdrawContext) ( - vio ValidatorDistInfo, fpo FeePool, withdrawn DecCoins) { - - vi, fp := vi.TakeFeePoolRewards(wc) - - withdrawalTokens := vi.ValCommission - vi.ValCommission = DecCoins{} // zero - - return vi, fp, withdrawalTokens -} - -// get the validator's pool rewards at this current state, -func (vi ValidatorDistInfo) CurrentPoolRewards( - wc WithdrawContext) DecCoins { - - fp := wc.FeePool - totalValAccum := fp.GetTotalValAccum(wc.Height, wc.TotalPower) - valAccum := vi.GetValAccum(wc.Height, wc.ValPower) - - if valAccum.GT(totalValAccum) { - panic("individual accum should never be greater than the total") - } - withdrawalTokens := fp.ValPool.MulDec(valAccum).QuoDec(totalValAccum) - commission := withdrawalTokens.MulDec(wc.CommissionRate) - afterCommission := withdrawalTokens.Minus(commission) - pool := vi.DelPool.Plus(afterCommission) - return pool -} - -// get the validator's commission pool rewards at this current state, -func (vi ValidatorDistInfo) CurrentCommissionRewards( - wc WithdrawContext) DecCoins { - - fp := wc.FeePool - totalValAccum := fp.GetTotalValAccum(wc.Height, wc.TotalPower) - valAccum := vi.GetValAccum(wc.Height, wc.ValPower) - - if valAccum.GT(totalValAccum) { - panic("individual accum should never be greater than the total") - } - withdrawalTokens := fp.ValPool.MulDec(valAccum).QuoDec(totalValAccum) - commission := withdrawalTokens.MulDec(wc.CommissionRate) - commissionPool := vi.ValCommission.Plus(commission) - return commissionPool -} diff --git a/x/distribution/types/validator_info_test.go b/x/distribution/types/validator_info_test.go deleted file mode 100644 index 9982a7dc1c69..000000000000 --- a/x/distribution/types/validator_info_test.go +++ /dev/null @@ -1,92 +0,0 @@ -package types - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - sdk "github.com/cosmos/cosmos-sdk/types" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" -) - -func TestTakeFeePoolRewards(t *testing.T) { - - // initialize - height := int64(0) - fp := InitialFeePool() - vi1 := NewValidatorDistInfo(valAddr1, height) - vi2 := NewValidatorDistInfo(valAddr2, height) - vi3 := NewValidatorDistInfo(valAddr3, height) - commissionRate1 := sdk.NewDecWithPrec(2, 2) - commissionRate2 := sdk.NewDecWithPrec(3, 2) - commissionRate3 := sdk.NewDecWithPrec(4, 2) - validatorTokens1 := sdk.NewDec(10) - validatorTokens2 := sdk.NewDec(40) - validatorTokens3 := sdk.NewDec(50) - totalBondedTokens := validatorTokens1.Add(validatorTokens2).Add(validatorTokens3) - - // simulate adding some stake for inflation - height = 10 - fp.ValPool = DecCoins{NewDecCoin(stakingTypes.DefaultBondDenom, 1000)} - - vi1, fp = vi1.TakeFeePoolRewards(NewWithdrawContext( - fp, height, totalBondedTokens, validatorTokens1, commissionRate1)) - require.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(100-2), vi1.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(2), vi1.ValCommission[0].Amount)) - - vi2, fp = vi2.TakeFeePoolRewards(NewWithdrawContext( - fp, height, totalBondedTokens, validatorTokens2, commissionRate2)) - require.True(sdk.DecEq(t, sdk.NewDec(500), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(500), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(400-12), vi2.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, vi2.ValCommission[0].Amount, sdk.NewDec(12))) - - // add more blocks and inflation - height = 20 - fp.ValPool[0].Amount = fp.ValPool[0].Amount.Add(sdk.NewDec(1000)) - - vi3, fp = vi3.TakeFeePoolRewards(NewWithdrawContext( - fp, height, totalBondedTokens, validatorTokens3, commissionRate3)) - require.True(sdk.DecEq(t, sdk.NewDec(500), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(500), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(1000-40), vi3.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, vi3.ValCommission[0].Amount, sdk.NewDec(40))) -} - -func TestWithdrawCommission(t *testing.T) { - - // initialize - height := int64(0) - fp := InitialFeePool() - vi := NewValidatorDistInfo(valAddr1, height) - commissionRate := sdk.NewDecWithPrec(2, 2) - validatorTokens := sdk.NewDec(10) - totalBondedTokens := validatorTokens.Add(sdk.NewDec(90)) // validator-1 is 10% of total power - - // simulate adding some stake for inflation - height = 10 - fp.ValPool = DecCoins{NewDecCoin(stakingTypes.DefaultBondDenom, 1000)} - - // for a more fun staring condition, have an non-withdraw update - vi, fp = vi.TakeFeePoolRewards(NewWithdrawContext( - fp, height, totalBondedTokens, validatorTokens, commissionRate)) - require.True(sdk.DecEq(t, sdk.NewDec(900), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(900), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(100-2), vi.DelPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(2), vi.ValCommission[0].Amount)) - - // add more blocks and inflation - height = 20 - fp.ValPool[0].Amount = fp.ValPool[0].Amount.Add(sdk.NewDec(1000)) - - vi, fp, commissionRecv := vi.WithdrawCommission(NewWithdrawContext( - fp, height, totalBondedTokens, validatorTokens, commissionRate)) - require.True(sdk.DecEq(t, sdk.NewDec(1800), fp.TotalValAccum.Accum)) - assert.True(sdk.DecEq(t, sdk.NewDec(1800), fp.ValPool[0].Amount)) - assert.True(sdk.DecEq(t, sdk.NewDec(200-4), vi.DelPool[0].Amount)) - assert.Zero(t, len(vi.ValCommission)) - assert.True(sdk.DecEq(t, sdk.NewDec(4), commissionRecv[0].Amount)) -} diff --git a/x/gov/genesis_test.go b/x/gov/genesis_test.go index ab3810068520..87d466b5f179 100644 --- a/x/gov/genesis_test.go +++ b/x/gov/genesis_test.go @@ -3,9 +3,10 @@ package gov import ( "testing" - "github.com/cosmos/cosmos-sdk/x/mock" "github.com/stretchr/testify/require" + "github.com/cosmos/cosmos-sdk/x/mock" + abci "github.com/tendermint/tendermint/abci/types" ) diff --git a/x/slashing/hooks.go b/x/slashing/hooks.go index 8f90a5addd83..3a4a38f2a721 100644 --- a/x/slashing/hooks.go +++ b/x/slashing/hooks.go @@ -71,3 +71,5 @@ func (h Hooks) BeforeValidatorModified(_ sdk.Context, _ sdk.ValAddress) func (h Hooks) BeforeDelegationCreated(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} func (h Hooks) BeforeDelegationSharesModified(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} func (h Hooks) BeforeDelegationRemoved(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} +func (h Hooks) AfterDelegationModified(_ sdk.Context, _ sdk.AccAddress, _ sdk.ValAddress) {} +func (h Hooks) BeforeValidatorSlashed(_ sdk.Context, _ sdk.ValAddress, _ sdk.Dec) {} diff --git a/x/staking/genesis.go b/x/staking/genesis.go index 736513db29a8..294d9d40e63b 100644 --- a/x/staking/genesis.go +++ b/x/staking/genesis.go @@ -45,6 +45,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res [ for _, delegation := range data.Bonds { keeper.BeforeDelegationCreated(ctx, delegation.DelegatorAddr, delegation.ValidatorAddr) keeper.SetDelegation(ctx, delegation) + keeper.AfterDelegationModified(ctx, delegation.DelegatorAddr, delegation.ValidatorAddr) } for _, ubd := range data.UnbondingDelegations { diff --git a/x/staking/keeper/delegation.go b/x/staking/keeper/delegation.go index e4c61eb65d07..d4f35431d7cf 100644 --- a/x/staking/keeper/delegation.go +++ b/x/staking/keeper/delegation.go @@ -77,6 +77,7 @@ func (k Keeper) SetDelegation(ctx sdk.Context, delegation types.Delegation) { store := ctx.KVStore(k.storeKey) b := types.MustMarshalDelegation(k.cdc, delegation) store.Set(GetDelegationKey(delegation.DelegatorAddr, delegation.ValidatorAddr), b) + k.AfterDelegationModified(ctx, delegation.DelegatorAddr, delegation.ValidatorAddr) } // remove a delegation from store diff --git a/x/staking/keeper/hooks.go b/x/staking/keeper/hooks.go index 29c1966d7d79..4ed0a724c54d 100644 --- a/x/staking/keeper/hooks.go +++ b/x/staking/keeper/hooks.go @@ -59,3 +59,15 @@ func (k Keeper) BeforeDelegationRemoved(ctx sdk.Context, delAddr sdk.AccAddress, k.hooks.BeforeDelegationRemoved(ctx, delAddr, valAddr) } } + +func (k Keeper) AfterDelegationModified(ctx sdk.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress) { + if k.hooks != nil { + k.hooks.AfterDelegationModified(ctx, delAddr, valAddr) + } +} + +func (k Keeper) BeforeValidatorSlashed(ctx sdk.Context, valAddr sdk.ValAddress, fraction sdk.Dec) { + if k.hooks != nil { + k.hooks.BeforeValidatorSlashed(ctx, valAddr, fraction) + } +} diff --git a/x/staking/keeper/slash.go b/x/staking/keeper/slash.go index 3c698382e71a..e747d0dcf4ad 100644 --- a/x/staking/keeper/slash.go +++ b/x/staking/keeper/slash.go @@ -55,6 +55,11 @@ func (k Keeper) Slash(ctx sdk.Context, consAddr sdk.ConsAddress, infractionHeigh operatorAddress := validator.GetOperator() k.BeforeValidatorModified(ctx, operatorAddress) + // we need to calculate the *effective* slash fraction for distribution + if validator.Tokens.GT(sdk.ZeroInt()) { + k.BeforeValidatorSlashed(ctx, operatorAddress, slashAmountDec.Quo(sdk.NewDecFromInt(validator.Tokens))) + } + // Track remaining slash amount for the validator // This will decrease when we slash unbondings and // redelegations, as that stake has since unbonded diff --git a/x/staking/simulation/invariants.go b/x/staking/simulation/invariants.go index 8557a0b4febe..04871240c411 100644 --- a/x/staking/simulation/invariants.go +++ b/x/staking/simulation/invariants.go @@ -82,17 +82,8 @@ func SupplyInvariants(ck bank.Keeper, k staking.Keeper, // add community pool loose = loose.Add(feePool.CommunityPool.AmountOf(stakingTypes.DefaultBondDenom)) - // add validator distribution pool - loose = loose.Add(feePool.ValPool.AmountOf(stakingTypes.DefaultBondDenom)) - - // add validator distribution commission and yet-to-be-withdrawn-by-delegators - d.IterateValidatorDistInfos(ctx, - func(_ int64, distInfo distribution.ValidatorDistInfo) (stop bool) { - loose = loose.Add(distInfo.DelPool.AmountOf(stakingTypes.DefaultBondDenom)) - loose = loose.Add(distInfo.ValCommission.AmountOf(stakingTypes.DefaultBondDenom)) - return false - }, - ) + // add yet-to-be-withdrawn + loose = loose.Add(d.GetOutstandingRewards(ctx).AmountOf(stakingTypes.DefaultBondDenom)) // Loose tokens should equal coin supply plus unbonding delegations // plus tokens on unbonded validators diff --git a/x/staking/staking.go b/x/staking/staking.go index 67fa3bb2e62c..096c3d24ec06 100644 --- a/x/staking/staking.go +++ b/x/staking/staking.go @@ -67,6 +67,7 @@ var ( KeyBondDenom = types.KeyBondDenom DefaultParams = types.DefaultParams + DefaultBondDenom = types.DefaultBondDenom InitialPool = types.InitialPool NewValidator = types.NewValidator NewDescription = types.NewDescription diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index eddce185ea07..60cf3840e7c6 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -405,14 +405,15 @@ func (v Validator) BondedTokens() sdk.Int { var _ sdk.Validator = Validator{} // nolint - for sdk.Validator -func (v Validator) GetJailed() bool { return v.Jailed } -func (v Validator) GetMoniker() string { return v.Description.Moniker } -func (v Validator) GetStatus() sdk.BondStatus { return v.Status } -func (v Validator) GetOperator() sdk.ValAddress { return v.OperatorAddr } -func (v Validator) GetConsPubKey() crypto.PubKey { return v.ConsPubKey } -func (v Validator) GetConsAddr() sdk.ConsAddress { return sdk.ConsAddress(v.ConsPubKey.Address()) } -func (v Validator) GetPower() sdk.Int { return v.BondedTokens() } -func (v Validator) GetTokens() sdk.Int { return v.Tokens } -func (v Validator) GetCommission() sdk.Dec { return v.Commission.Rate } -func (v Validator) GetDelegatorShares() sdk.Dec { return v.DelegatorShares } -func (v Validator) GetBondHeight() int64 { return v.BondHeight } +func (v Validator) GetJailed() bool { return v.Jailed } +func (v Validator) GetMoniker() string { return v.Description.Moniker } +func (v Validator) GetStatus() sdk.BondStatus { return v.Status } +func (v Validator) GetOperator() sdk.ValAddress { return v.OperatorAddr } +func (v Validator) GetConsPubKey() crypto.PubKey { return v.ConsPubKey } +func (v Validator) GetConsAddr() sdk.ConsAddress { return sdk.ConsAddress(v.ConsPubKey.Address()) } +func (v Validator) GetPower() sdk.Int { return v.BondedTokens() } +func (v Validator) GetTokens() sdk.Int { return v.Tokens } +func (v Validator) GetCommission() sdk.Dec { return v.Commission.Rate } +func (v Validator) GetDelegatorShares() sdk.Dec { return v.DelegatorShares } +func (v Validator) GetBondHeight() int64 { return v.BondHeight } +func (v Validator) GetDelegatorShareExRate() sdk.Dec { return v.DelegatorShareExRate() }