From 9dafa3252d7a5a473bb1b5352dc5cb2281565167 Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Mon, 24 Sep 2018 22:23:58 +0000 Subject: [PATCH] Merge PR #2365: Validator Commission Model * Update validator commission fields * Remove CommissionChangeToday and update to use CommissionChangeTime * Implement commission as a first class citizen type * Implement stringer for Comission * Move commission type and logic to new file * Add new commission errors * Add commission to create validator message * Implement and call UpdateValidatorCommission * Update godoc for UpdateValidatorCommission * Add Abs to the decimal type * Implement new SetValidatorCommission * Update decimal short godocs * Move set initial commission logic * Move initial commission validation to Commission type * Update initial validator commission logic and unit tests * Remove commission update time from struct and move to validator * Update validator create handler tests * Implement commission logic for CLI * Fix make lint failure * Fix make cover failure * Update edit validator logic to handle new commission rate * Fix lint and cover * Update create/edit validator simulation to include commission params * Update MsgEditValidator godoc * Update pending log * Update staking tx docs * Fix CLI create validator test * Update variables names for commission strings * Merge UpdateTime into Commission type * Update create-validator usage in docs * Update more docs with examples * More doc updates --- PENDING.md | 4 + cmd/gaia/cli_test/cli_test.go | 3 + docs/spec/staking/transactions.md | 119 ++++++++++------- docs/validators/validator-setup.md | 19 ++- types/decimal.go | 6 +- x/gov/tally_test.go | 18 ++- x/slashing/app_test.go | 5 +- x/slashing/test_common.go | 6 +- x/stake/app_test.go | 14 +- x/stake/client/cli/flags.go | 10 ++ x/stake/client/cli/tx.go | 86 +++++------- x/stake/client/cli/utils.go | 88 ++++++++++++ x/stake/client/rest/utils.go | 3 +- x/stake/handler.go | 26 +++- x/stake/handler_test.go | 5 +- x/stake/keeper/validator.go | 17 +++ x/stake/keeper/validator_test.go | 54 ++++++++ x/stake/simulation/msgs.go | 28 +++- x/stake/stake.go | 18 ++- x/stake/types/commission.go | 128 ++++++++++++++++++ x/stake/types/errors.go | 20 +++ x/stake/types/msg.go | 34 +++-- x/stake/types/msg_test.go | 57 +++++--- x/stake/types/validator.go | 208 +++++++++++++---------------- x/stake/types/validator_test.go | 34 +++++ 25 files changed, 728 insertions(+), 282 deletions(-) create mode 100644 x/stake/client/cli/utils.go create mode 100644 x/stake/types/commission.go diff --git a/PENDING.md b/PENDING.md index 8a9fdebd5..ec3c7f930 100644 --- a/PENDING.md +++ b/PENDING.md @@ -76,6 +76,8 @@ FEATURES * [\#966](https://github.com/cosmos/cosmos-sdk/issues/966) Add --generate-only flag to build an unsigned transaction and write it to STDOUT. * [\#1953](https://github.com/cosmos/cosmos-sdk/issues/1953) New `sign` command to sign transactions generated with the --generate-only flag. * [\#1954](https://github.com/cosmos/cosmos-sdk/issues/1954) New `broadcast` command to broadcast transactions generated offline and signed with the `sign` command. + * [stake][cli] [\#1672](https://github.com/cosmos/cosmos-sdk/issues/1672) Introduced + new commission flags for validator commands `create-validator` and `edit-validator`. * Gaia * [cli] #2170 added ability to show the node's address via `gaiad tendermint show-address` @@ -88,6 +90,8 @@ FEATURES * [simulation] [\#1924](https://github.com/cosmos/cosmos-sdk/issues/1924) allow operations to specify future operations * [simulation] [\#1924](https://github.com/cosmos/cosmos-sdk/issues/1924) Add benchmarking capabilities, with makefile commands "test_sim_gaia_benchmark, test_sim_gaia_profile" * [simulation] [\#2349](https://github.com/cosmos/cosmos-sdk/issues/2349) Add time-based future scheduled operations to simulator + * [x/stake] [\#1672](https://github.com/cosmos/cosmos-sdk/issues/1672) Implement + basis for the validator commission model. * Tendermint diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 8ac8f5559..3c847dc90 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -238,6 +238,9 @@ func TestGaiaCLICreateValidator(t *testing.T) { cvStr += fmt.Sprintf(" --pubkey=%s", barCeshPubKey) cvStr += fmt.Sprintf(" --amount=%v", "2steak") cvStr += fmt.Sprintf(" --moniker=%v", "bar-vally") + cvStr += fmt.Sprintf(" --commission-rate=%v", "0.05") + cvStr += fmt.Sprintf(" --commission-max-rate=%v", "0.20") + cvStr += fmt.Sprintf(" --commission-max-change-rate=%v", "0.10") initialPool.BondedTokens = initialPool.BondedTokens.Add(sdk.NewDec(1)) diff --git a/docs/spec/staking/transactions.md b/docs/spec/staking/transactions.md index 148c1c415..6ed90343b 100644 --- a/docs/spec/staking/transactions.md +++ b/docs/spec/staking/transactions.md @@ -1,86 +1,107 @@ -## Transaction Overview +# Transaction Overview In this section we describe the processing of the transactions and the corresponding updates to the state. Transactions: - - TxCreateValidator - - TxEditValidator - - TxDelegation - - TxStartUnbonding - - TxCompleteUnbonding - - TxRedelegate - - TxCompleteRedelegation + +* TxCreateValidator +* TxEditValidator +* TxDelegation +* TxStartUnbonding +* TxCompleteUnbonding +* TxRedelegate +* TxCompleteRedelegation Other important state changes: - - Update Validators + +* Update Validators Other notes: - - `tx` denotes a reference to the transaction being processed - - `sender` denotes the address of the sender of the transaction - - `getXxx`, `setXxx`, and `removeXxx` functions are used to retrieve and - modify objects from the store - - `sdk.Dec` refers to a decimal type specified by the SDK. -### TxCreateValidator +* `tx` denotes a reference to the transaction being processed +* `sender` denotes the address of the sender of the transaction +* `getXxx`, `setXxx`, and `removeXxx` functions are used to retrieve and + modify objects from the store +* `sdk.Dec` refers to a decimal type specified by the SDK. - - triggers: `distribution.CreateValidatorDistribution` +## TxCreateValidator + +* triggers: `distribution.CreateValidatorDistribution` A validator is created using the `TxCreateValidator` transaction. ```golang type TxCreateValidator struct { - Operator sdk.Address - ConsensusPubKey crypto.PubKey - GovernancePubKey crypto.PubKey - SelfDelegation coin.Coin + Description Description + Commission Commission - Description Description - Commission sdk.Dec - CommissionMax sdk.Dec - CommissionMaxChange sdk.Dec + DelegatorAddr sdk.AccAddress + ValidatorAddr sdk.ValAddress + PubKey crypto.PubKey + Delegation sdk.Coin } - createValidator(tx TxCreateValidator): - validator = getValidator(tx.Operator) - if validator != nil return // only one validator per address + ok := validatorExists(tx.ValidatorAddr) + if ok return err // only one validator per address - validator = NewValidator(operatorAddr, ConsensusPubKey, GovernancePubKey, Description) - init validator poolShares, delegatorShares set to 0 - init validator commision fields from tx - validator.PoolShares = 0 + ok := validatorByPubKeyExists(tx.PubKey) + if ok return err // only one validator per public key + err := validateDenom(tx.Delegation.Denom) + if err != nil return err // denomination must be valid + + validator := NewValidator(tx.ValidatorAddr, tx.PubKey, tx.Description) + + err := setInitialCommission(validator, tx.Commission, blockTime) + if err != nil return err // must be able to set initial commission correctly + + // set the validator and public key setValidator(validator) + setValidatorByPubKeyIndex(validator) - txDelegate = TxDelegate(tx.Operator, tx.Operator, tx.SelfDelegation) - delegate(txDelegate, validator) // see delegate function in [TxDelegate](TxDelegate) - return + // delegate coins from tx.DelegatorAddr to the validator + err := delegate(tx.DelegatorAddr, tx.Delegation, validator) + if err != nil return err // must be able to set delegation correctly + + tags := createTags(tx) + return tags ``` -### TxEditValidator +## TxEditValidator -If either the `Description` (excluding `DateBonded` which is constant), -`Commission`, or the `GovernancePubKey` need to be updated, the -`TxEditCandidacy` transaction should be sent from the operator account: +If either the `Description`, `Commission`, or the `ValidatorAddr` need to be +updated, the `TxEditCandidacy` transaction should be sent from the operator +account: ```golang type TxEditCandidacy struct { - GovernancePubKey crypto.PubKey - Commission sdk.Dec - Description Description + Description Description + ValidatorAddr sdk.ValAddress + CommissionRate sdk.Dec } editCandidacy(tx TxEditCandidacy): - validator = getValidator(tx.ValidatorAddr) + validator, ok := getValidator(tx.ValidatorAddr) + if !ok return err // validator must exist - if tx.Commission > CommissionMax || tx.Commission < 0 then fail - if rateChange(tx.Commission) > CommissionMaxChange then fail - validator.Commission = tx.Commission + // Attempt to update the validator's description. The description provided + // must be valid. + description, err := updateDescription(validator, tx.Description) + if err != nil return err - if tx.GovernancePubKey != nil validator.GovernancePubKey = tx.GovernancePubKey - if tx.Description != nil validator.Description = tx.Description + // a validator is not required to update it's commission rate + if tx.CommissionRate != nil { + // Attempt to update a validator's commission rate. The rate provided + // must be valid. It's rate can only be updated once a day. + err := updateValidatorCommission(validator, tx.CommissionRate) + if err != nil return err + } - setValidator(store, validator) - return + // set the validator and public key + setValidator(validator) + + tags := createTags(tx) + return tags ``` ### TxDelegate diff --git a/docs/validators/validator-setup.md b/docs/validators/validator-setup.md index 8a842ea20..dbcc84b3d 100644 --- a/docs/validators/validator-setup.md +++ b/docs/validators/validator-setup.md @@ -35,9 +35,16 @@ gaiacli stake create-validator \ --address-validator= --moniker="choose a moniker" \ --chain-id= \ - --name= + --name= \ + --commission-rate="0.10" \ + --commission-max-rate="0.20" \ + --commission-max-change-rate="0.01" ``` +__Note__: When specifying commission parameters, the `commission-max-change-rate` +is used to measure % _point_ change over the `commission-rate`. E.g. 1% to 2% is +a 100% rate increase, but only 1 percentage point. + ### Edit Validator Description You can edit your validator's public description. This info is to identify your validator, and will be relied on by delegators to decide which validators to stake to. Make sure to provide input for every flag below, otherwise the field will default to empty (`--moniker` defaults to the machine name). @@ -52,9 +59,17 @@ gaiacli stake edit-validator --identity=6A0D65E29A4CBC8E --details="To infinity and beyond!" --chain-id= \ - --name= + --name= \ + --commission-rate="0.10" ``` +__Note__: The `commission-rate` value must adhere to the following invariants: + +- Must be between 0 and the validator's `commission-max-rate` +- Must not exceed the validator's `commission-max-change-rate` which is maximum + % point change rate **per day**. In other words, a validator can only change + its commission once per day and within `commission-max-change-rate` bounds. + ### View Validator Description View the validator's information with this command: diff --git a/types/decimal.go b/types/decimal.go index 8e7db1340..564d6322c 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -174,13 +174,15 @@ func NewDecFromStr(str string) (d Dec, err Error) { //______________________________________________________________________________________________ //nolint -func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // Is equal to zero -func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } +func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil +func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero +func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal func (d Dec) LT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) < 0 } // less than func (d Dec) LTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) <= 0 } // less than or equal func (d Dec) Neg() Dec { return Dec{new(big.Int).Neg(d.Int)} } // reverse the decimal sign +func (d Dec) Abs() Dec { return Dec{new(big.Int).Abs(d.Int)} } // absolute value // addition func (d Dec) Add(d2 Dec) Dec { diff --git a/x/gov/tally_test.go b/x/gov/tally_test.go index 6c6d64aa6..71983bf59 100644 --- a/x/gov/tally_test.go +++ b/x/gov/tally_test.go @@ -15,13 +15,19 @@ import ( var ( pubkeys = []crypto.PubKey{ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey(), ed25519.GenPrivKey().PubKey()} + + testDescription = stake.NewDescription("T", "E", "S", "T") + testCommissionMsg = stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) ) func createValidators(t *testing.T, stakeHandler sdk.Handler, ctx sdk.Context, addrs []sdk.ValAddress, coinAmt []int64) { require.True(t, len(addrs) <= len(pubkeys), "Not enough pubkeys specified at top of file.") - dummyDescription := stake.NewDescription("T", "E", "S", "T") + for i := 0; i < len(addrs); i++ { - valCreateMsg := stake.NewMsgCreateValidator(addrs[i], pubkeys[i], sdk.NewInt64Coin("steak", coinAmt[i]), dummyDescription) + valCreateMsg := stake.NewMsgCreateValidator( + addrs[i], pubkeys[i], sdk.NewInt64Coin("steak", coinAmt[i]), testDescription, testCommissionMsg, + ) + res := stakeHandler(ctx, valCreateMsg) require.True(t, res.IsOK()) } @@ -378,20 +384,18 @@ func TestTallyDelgatorMultipleInherit(t *testing.T) { ctx := mapp.BaseApp.NewContext(false, abci.Header{}) stakeHandler := stake.NewHandler(sk) - dummyDescription := stake.NewDescription("T", "E", "S", "T") - val1CreateMsg := stake.NewMsgCreateValidator( - sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 25), dummyDescription, + sdk.ValAddress(addrs[0]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 25), testDescription, testCommissionMsg, ) stakeHandler(ctx, val1CreateMsg) val2CreateMsg := stake.NewMsgCreateValidator( - sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 6), dummyDescription, + sdk.ValAddress(addrs[1]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 6), testDescription, testCommissionMsg, ) stakeHandler(ctx, val2CreateMsg) val3CreateMsg := stake.NewMsgCreateValidator( - sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 7), dummyDescription, + sdk.ValAddress(addrs[2]), ed25519.GenPrivKey().PubKey(), sdk.NewInt64Coin("steak", 7), testDescription, testCommissionMsg, ) stakeHandler(ctx, val3CreateMsg) diff --git a/x/slashing/app_test.go b/x/slashing/app_test.go index 497440a08..a3370b1d8 100644 --- a/x/slashing/app_test.go +++ b/x/slashing/app_test.go @@ -99,9 +99,12 @@ func TestSlashingMsgs(t *testing.T) { } accs := []auth.Account{acc1} mock.SetGenesis(mapp, accs) + description := stake.NewDescription("foo_moniker", "", "", "") + commission := stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) + createValidatorMsg := stake.NewMsgCreateValidator( - sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, + sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commission, ) mock.SignCheckDeliver(t, mapp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1) mock.CheckBalance(t, mapp, addr1, sdk.Coins{genCoin.Minus(bondCoin)}) diff --git a/x/slashing/test_common.go b/x/slashing/test_common.go index db2293a2f..7c97a8537 100644 --- a/x/slashing/test_common.go +++ b/x/slashing/test_common.go @@ -103,12 +103,14 @@ func testAddr(addr string) sdk.AccAddress { } func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt sdk.Int) stake.MsgCreateValidator { + commission := stake.NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) return stake.MsgCreateValidator{ Description: stake.Description{}, + Commission: commission, DelegatorAddr: sdk.AccAddress(address), ValidatorAddr: address, PubKey: pubKey, - Delegation: sdk.Coin{"steak", amt}, + Delegation: sdk.NewCoin("steak", amt), } } @@ -116,6 +118,6 @@ func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, delAmoun return stake.MsgDelegate{ DelegatorAddr: delAddr, ValidatorAddr: valAddr, - Delegation: sdk.Coin{"steak", delAmount}, + Delegation: sdk.NewCoin("steak", delAmount), } } diff --git a/x/stake/app_test.go b/x/stake/app_test.go index 922ce8ac3..f96408c11 100644 --- a/x/stake/app_test.go +++ b/x/stake/app_test.go @@ -21,10 +21,12 @@ var ( priv4 = ed25519.GenPrivKey() addr4 = sdk.AccAddress(priv4.PubKey().Address()) coins = sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(10))} - fee = auth.StdFee{ - sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(0))}, + fee = auth.NewStdFee( 100000, - } + sdk.Coins{sdk.NewCoin("foocoin", sdk.NewInt(0))}..., + ) + + commissionMsg = NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) ) // getMockApp returns an initialized mock application for this module. @@ -129,7 +131,7 @@ func TestStakeMsgs(t *testing.T) { // create validator description := NewDescription("foo_moniker", "", "", "") createValidatorMsg := NewMsgCreateValidator( - sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, + sdk.ValAddress(addr1), priv1.PubKey(), bondCoin, description, commissionMsg, ) mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsg}, []int64{0}, []int64{0}, true, true, priv1) @@ -143,7 +145,7 @@ func TestStakeMsgs(t *testing.T) { // addr1 create validator on behalf of addr2 createValidatorMsgOnBehalfOf := NewMsgCreateValidatorOnBehalfOf( - addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, + addr1, sdk.ValAddress(addr2), priv2.PubKey(), bondCoin, description, commissionMsg, ) mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{createValidatorMsgOnBehalfOf}, []int64{0, 1}, []int64{1, 0}, true, true, priv1, priv2) @@ -160,7 +162,7 @@ func TestStakeMsgs(t *testing.T) { // edit the validator description = NewDescription("bar_moniker", "", "", "") - editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description) + editValidatorMsg := NewMsgEditValidator(sdk.ValAddress(addr1), description, nil) mock.SignCheckDeliver(t, mApp.BaseApp, []sdk.Msg{editValidatorMsg}, []int64{0}, []int64{2}, true, true, priv1) validator = checkValidator(t, mApp, keeper, sdk.ValAddress(addr1), true) diff --git a/x/stake/client/cli/flags.go b/x/stake/client/cli/flags.go index d0e83ab3c..1228bec2a 100644 --- a/x/stake/client/cli/flags.go +++ b/x/stake/client/cli/flags.go @@ -21,6 +21,10 @@ const ( FlagIdentity = "identity" FlagWebsite = "website" FlagDetails = "details" + + FlagCommissionRate = "commission-rate" + FlagCommissionMaxRate = "commission-max-rate" + FlagCommissionMaxChangeRate = "commission-max-change-rate" ) // common flagsets to add to various functions @@ -29,6 +33,8 @@ var ( fsAmount = flag.NewFlagSet("", flag.ContinueOnError) fsShares = flag.NewFlagSet("", flag.ContinueOnError) fsDescriptionCreate = flag.NewFlagSet("", flag.ContinueOnError) + fsCommissionCreate = flag.NewFlagSet("", flag.ContinueOnError) + fsCommissionUpdate = flag.NewFlagSet("", flag.ContinueOnError) fsDescriptionEdit = flag.NewFlagSet("", flag.ContinueOnError) fsValidator = flag.NewFlagSet("", flag.ContinueOnError) fsDelegator = flag.NewFlagSet("", flag.ContinueOnError) @@ -44,6 +50,10 @@ func init() { fsDescriptionCreate.String(FlagIdentity, "", "optional identity signature (ex. UPort or Keybase)") fsDescriptionCreate.String(FlagWebsite, "", "optional website") fsDescriptionCreate.String(FlagDetails, "", "optional details") + fsCommissionUpdate.String(FlagCommissionRate, "", "The new commission rate percentage") + fsCommissionCreate.String(FlagCommissionRate, "", "The initial commission rate percentage") + fsCommissionCreate.String(FlagCommissionMaxRate, "", "The maximum commission rate percentage") + fsCommissionCreate.String(FlagCommissionMaxChangeRate, "", "The maximum commission change rate percentage (per day)") fsDescriptionEdit.String(FlagMoniker, types.DoNotModifyDesc, "validator name") fsDescriptionEdit.String(FlagIdentity, types.DoNotModifyDesc, "optional identity signature (ex. UPort or Keybase)") fsDescriptionEdit.String(FlagWebsite, types.DoNotModifyDesc, "optional website") diff --git a/x/stake/client/cli/tx.go b/x/stake/client/cli/tx.go index f91717328..74f4c6d6e 100644 --- a/x/stake/client/cli/tx.go +++ b/x/stake/client/cli/tx.go @@ -12,9 +12,7 @@ import ( authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" authtxb "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" "github.com/cosmos/cosmos-sdk/x/stake" - "github.com/cosmos/cosmos-sdk/x/stake/types" - "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -66,6 +64,15 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { Details: viper.GetString(FlagDetails), } + // get the initial validator commission parameters + rateStr := viper.GetString(FlagCommissionRate) + maxRateStr := viper.GetString(FlagCommissionMaxRate) + maxChangeRateStr := viper.GetString(FlagCommissionMaxChangeRate) + commissionMsg, err := buildCommissionMsg(rateStr, maxRateStr, maxChangeRateStr) + if err != nil { + return err + } + var msg sdk.Msg if viper.GetString(FlagAddressDelegator) != "" { delAddr, err := sdk.AccAddressFromBech32(viper.GetString(FlagAddressDelegator)) @@ -73,13 +80,19 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { return err } - msg = stake.NewMsgCreateValidatorOnBehalfOf(delAddr, sdk.ValAddress(valAddr), pk, amount, description) + msg = stake.NewMsgCreateValidatorOnBehalfOf( + delAddr, sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, + ) } else { - msg = stake.NewMsgCreateValidator(sdk.ValAddress(valAddr), pk, amount, description) + msg = stake.NewMsgCreateValidator( + sdk.ValAddress(valAddr), pk, amount, description, commissionMsg, + ) } + if cliCtx.GenerateOnly { return utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg}) } + // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txBldr, cliCtx, []sdk.Msg{msg}) }, @@ -88,6 +101,7 @@ func GetCmdCreateValidator(cdc *codec.Codec) *cobra.Command { cmd.Flags().AddFlagSet(fsPk) cmd.Flags().AddFlagSet(fsAmount) cmd.Flags().AddFlagSet(fsDescriptionCreate) + cmd.Flags().AddFlagSet(fsCommissionCreate) cmd.Flags().AddFlagSet(fsDelegator) return cmd @@ -117,17 +131,31 @@ func GetCmdEditValidator(cdc *codec.Codec) *cobra.Command { Details: viper.GetString(FlagDetails), } - msg := stake.NewMsgEditValidator(sdk.ValAddress(valAddr), description) + var newRate *sdk.Dec + + commissionRate := viper.GetString(FlagCommissionRate) + if commissionRate != "" { + rate, err := sdk.NewDecFromStr(commissionRate) + if err != nil { + return fmt.Errorf("invalid new commission rate: %v", err) + } + + newRate = &rate + } + + msg := stake.NewMsgEditValidator(sdk.ValAddress(valAddr), description, newRate) if cliCtx.GenerateOnly { return utils.PrintUnsignedStdTx(txBldr, cliCtx, []sdk.Msg{msg}) } + // build and sign the transaction, then broadcast to Tendermint return utils.SendTx(txBldr, cliCtx, []sdk.Msg{msg}) }, } cmd.Flags().AddFlagSet(fsDescriptionEdit) + cmd.Flags().AddFlagSet(fsCommissionUpdate) return cmd } @@ -247,54 +275,6 @@ func GetCmdBeginRedelegate(storeName string, cdc *codec.Codec) *cobra.Command { return cmd } -// nolint: gocyclo -// TODO: Make this pass gocyclo linting -func getShares( - storeName string, cdc *codec.Codec, sharesAmountStr, - sharesPercentStr string, delAddr sdk.AccAddress, valAddr sdk.ValAddress, -) (sharesAmount sdk.Dec, err error) { - switch { - case sharesAmountStr != "" && sharesPercentStr != "": - return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both") - case sharesAmountStr == "" && sharesPercentStr == "": - return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both") - case sharesAmountStr != "": - sharesAmount, err = sdk.NewDecFromStr(sharesAmountStr) - if err != nil { - return sharesAmount, err - } - if !sharesAmount.GT(sdk.ZeroDec()) { - return sharesAmount, errors.Errorf("shares amount must be positive number (ex. 123, 1.23456789)") - } - case sharesPercentStr != "": - var sharesPercent sdk.Dec - sharesPercent, err = sdk.NewDecFromStr(sharesPercentStr) - if err != nil { - return sharesAmount, err - } - if !sharesPercent.GT(sdk.ZeroDec()) || !sharesPercent.LTE(sdk.OneDec()) { - return sharesAmount, errors.Errorf("shares percent must be >0 and <=1 (ex. 0.01, 0.75, 1)") - } - - // make a query to get the existing delegation shares - key := stake.GetDelegationKey(delAddr, valAddr) - cliCtx := context.NewCLIContext(). - WithCodec(cdc). - WithAccountDecoder(authcmd.GetAccountDecoder(cdc)) - - resQuery, err := cliCtx.QueryStore(key, storeName) - if err != nil { - return sharesAmount, errors.Errorf("cannot find delegation to determine percent Error: %v", err) - } - delegation, err := types.UnmarshalDelegation(cdc, key, resQuery) - if err != nil { - return sdk.ZeroDec(), err - } - sharesAmount = sharesPercent.Mul(delegation.Shares) - } - return -} - // GetCmdCompleteRedelegate implements the complete redelegation command. func GetCmdCompleteRedelegate(cdc *codec.Codec) *cobra.Command { cmd := &cobra.Command{ diff --git a/x/stake/client/cli/utils.go b/x/stake/client/cli/utils.go new file mode 100644 index 000000000..9aca2d899 --- /dev/null +++ b/x/stake/client/cli/utils.go @@ -0,0 +1,88 @@ +package cli + +import ( + "github.com/cosmos/cosmos-sdk/client/context" + "github.com/cosmos/cosmos-sdk/codec" + sdk "github.com/cosmos/cosmos-sdk/types" + authcmd "github.com/cosmos/cosmos-sdk/x/auth/client/cli" + "github.com/cosmos/cosmos-sdk/x/stake" + "github.com/cosmos/cosmos-sdk/x/stake/types" + "github.com/pkg/errors" +) + +func getShares( + storeName string, cdc *codec.Codec, sharesAmountStr, + sharesPercentStr string, delAddr sdk.AccAddress, valAddr sdk.ValAddress, +) (sharesAmount sdk.Dec, err error) { + + switch { + case sharesAmountStr != "" && sharesPercentStr != "": + return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both") + + case sharesAmountStr == "" && sharesPercentStr == "": + return sharesAmount, errors.Errorf("can either specify the amount OR the percent of the shares, not both") + + case sharesAmountStr != "": + sharesAmount, err = sdk.NewDecFromStr(sharesAmountStr) + if err != nil { + return sharesAmount, err + } + if !sharesAmount.GT(sdk.ZeroDec()) { + return sharesAmount, errors.Errorf("shares amount must be positive number (ex. 123, 1.23456789)") + } + + case sharesPercentStr != "": + var sharesPercent sdk.Dec + sharesPercent, err = sdk.NewDecFromStr(sharesPercentStr) + if err != nil { + return sharesAmount, err + } + if !sharesPercent.GT(sdk.ZeroDec()) || !sharesPercent.LTE(sdk.OneDec()) { + return sharesAmount, errors.Errorf("shares percent must be >0 and <=1 (ex. 0.01, 0.75, 1)") + } + + // make a query to get the existing delegation shares + key := stake.GetDelegationKey(delAddr, valAddr) + cliCtx := context.NewCLIContext(). + WithCodec(cdc). + WithAccountDecoder(authcmd.GetAccountDecoder(cdc)) + + resQuery, err := cliCtx.QueryStore(key, storeName) + if err != nil { + return sharesAmount, errors.Errorf("cannot find delegation to determine percent Error: %v", err) + } + + delegation, err := types.UnmarshalDelegation(cdc, key, resQuery) + if err != nil { + return sdk.ZeroDec(), err + } + + sharesAmount = sharesPercent.Mul(delegation.Shares) + } + + return +} + +func buildCommissionMsg(rateStr, maxRateStr, maxChangeRateStr string) (commission types.CommissionMsg, err error) { + if rateStr == "" || maxRateStr == "" || maxChangeRateStr == "" { + return commission, errors.Errorf("must specify all validator commission parameters") + } + + rate, err := sdk.NewDecFromStr(rateStr) + if err != nil { + return commission, err + } + + maxRate, err := sdk.NewDecFromStr(maxRateStr) + if err != nil { + return commission, err + } + + maxChangeRate, err := sdk.NewDecFromStr(maxChangeRateStr) + if err != nil { + return commission, err + } + + commission = types.NewCommissionMsg(rate, maxRate, maxChangeRate) + return commission, nil +} diff --git a/x/stake/client/rest/utils.go b/x/stake/client/rest/utils.go index 8ab565500..e7b8891ea 100644 --- a/x/stake/client/rest/utils.go +++ b/x/stake/client/rest/utils.go @@ -3,11 +3,12 @@ package rest import ( "fmt" + "github.com/cosmos/cosmos-sdk/client/context" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/x/stake/tags" + rpcclient "github.com/tendermint/tendermint/rpc/client" - "github.com/cosmos/cosmos-sdk/client/context" ) // contains checks if the a given query contains one of the tx types diff --git a/x/stake/handler.go b/x/stake/handler.go index e7641393d..aaf372af2 100644 --- a/x/stake/handler.go +++ b/x/stake/handler.go @@ -62,27 +62,38 @@ func EndBlocker(ctx sdk.Context, k keeper.Keeper) (ValidatorUpdates []abci.Valid // now we just perform action and save func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k keeper.Keeper) sdk.Result { - // check to see if the pubkey or sender has been registered before _, found := k.GetValidator(ctx, msg.ValidatorAddr) if found { return ErrValidatorOwnerExists(k.Codespace()).Result() } + _, found = k.GetValidatorByPubKey(ctx, msg.PubKey) if found { return ErrValidatorPubKeyExists(k.Codespace()).Result() } + if msg.Delegation.Denom != k.GetParams(ctx).BondDenom { return ErrBadDenom(k.Codespace()).Result() } validator := NewValidator(msg.ValidatorAddr, msg.PubKey, msg.Description) + commission := NewCommissionWithTime( + msg.Commission.Rate, msg.Commission.MaxChangeRate, + msg.Commission.MaxChangeRate, ctx.BlockHeader().Time, + ) + + validator, err := validator.SetInitialCommission(commission) + if err != nil { + return err.Result() + } + k.SetValidator(ctx, validator) k.SetValidatorByPubKeyIndex(ctx, validator) // move coins from the msg.Address account to a (self-delegation) delegator account // the validator account and global shares are updated within here - _, err := k.Delegate(ctx, msg.DelegatorAddr, msg.Delegation, validator, true) + _, err = k.Delegate(ctx, msg.DelegatorAddr, msg.Delegation, validator, true) if err != nil { return err.Result() } @@ -93,13 +104,13 @@ func handleMsgCreateValidator(ctx sdk.Context, msg types.MsgCreateValidator, k k tags.Moniker, []byte(msg.Description.Moniker), tags.Identity, []byte(msg.Description.Identity), ) + return sdk.Result{ Tags: tags, } } func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keeper.Keeper) sdk.Result { - // validator must already be registered validator, found := k.GetValidator(ctx, msg.ValidatorAddr) if !found { @@ -111,17 +122,26 @@ func handleMsgEditValidator(ctx sdk.Context, msg types.MsgEditValidator, k keepe if err != nil { return err.Result() } + validator.Description = description + if msg.CommissionRate != nil { + if err := k.UpdateValidatorCommission(ctx, validator, *msg.CommissionRate); err != nil { + return err.Result() + } + } + // We don't need to run through all the power update logic within k.UpdateValidator // We just need to override the entry in state, since only the description has changed. k.SetValidator(ctx, validator) + tags := sdk.NewTags( tags.Action, tags.ActionEditValidator, tags.DstValidator, []byte(msg.ValidatorAddr.String()), tags.Moniker, []byte(description.Moniker), tags.Identity, []byte(description.Identity), ) + return sdk.Result{ Tags: tags, } diff --git a/x/stake/handler_test.go b/x/stake/handler_test.go index 70a21c171..845844fe2 100644 --- a/x/stake/handler_test.go +++ b/x/stake/handler_test.go @@ -17,7 +17,9 @@ import ( //______________________________________________________________________ func newTestMsgCreateValidator(address sdk.ValAddress, pubKey crypto.PubKey, amt int64) MsgCreateValidator { - return types.NewMsgCreateValidator(address, pubKey, sdk.NewCoin("steak", sdk.NewInt(amt)), Description{}) + return types.NewMsgCreateValidator( + address, pubKey, sdk.NewCoin("steak", sdk.NewInt(amt)), Description{}, commissionMsg, + ) } func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt int64) MsgDelegate { @@ -31,6 +33,7 @@ func newTestMsgDelegate(delAddr sdk.AccAddress, valAddr sdk.ValAddress, amt int6 func newTestMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.ValAddress, valPubKey crypto.PubKey, amt int64) MsgCreateValidator { return MsgCreateValidator{ Description: Description{}, + Commission: commissionMsg, DelegatorAddr: delAddr, ValidatorAddr: valAddr, PubKey: valPubKey, diff --git a/x/stake/keeper/validator.go b/x/stake/keeper/validator.go index 996bf06bf..de4c4a187 100644 --- a/x/stake/keeper/validator.go +++ b/x/stake/keeper/validator.go @@ -693,6 +693,23 @@ func (k Keeper) RemoveValidator(ctx sdk.Context, address sdk.ValAddress) { tstore.Set(GetTendermintUpdatesTKey(address), bz) } +// UpdateValidatorCommission attempts to update a validator's commission rate. +// An error is returned if the new commission rate is invalid. +func (k Keeper) UpdateValidatorCommission(ctx sdk.Context, validator types.Validator, newRate sdk.Dec) sdk.Error { + commission := validator.Commission + blockTime := ctx.BlockHeader().Time + + if err := commission.ValidateNewRate(newRate, blockTime); err != nil { + return err + } + + validator.Commission.Rate = newRate + validator.Commission.UpdateTime = blockTime + + k.SetValidator(ctx, validator) + return nil +} + //__________________________________________________________________________ // get the current validator on the cliff diff --git a/x/stake/keeper/validator_test.go b/x/stake/keeper/validator_test.go index e23d3b0aa..1a5cc3fee 100644 --- a/x/stake/keeper/validator_test.go +++ b/x/stake/keeper/validator_test.go @@ -3,9 +3,11 @@ package keeper import ( "fmt" "testing" + "time" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/stake/types" + abci "github.com/tendermint/tendermint/abci/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -1043,3 +1045,55 @@ func TestGetValidTendermintUpdatesBondTransition(t *testing.T) { clearTendermintUpdates(ctx, keeper) require.Equal(t, 0, len(keeper.GetValidTendermintUpdates(ctx))) } + +func TestUpdateValidatorCommission(t *testing.T) { + ctx, _, keeper := CreateTestInput(t, false, 1000) + ctx = ctx.WithBlockHeader(abci.Header{Time: time.Now().UTC()}) + + commission1 := types.NewCommissionWithTime( + sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(3, 1), + sdk.NewDecWithPrec(1, 1), time.Now().UTC().Add(time.Duration(-1)*time.Hour), + ) + commission2 := types.NewCommission(sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(3, 1), sdk.NewDecWithPrec(1, 1)) + + val1 := types.NewValidator(addrVals[0], PKs[0], types.Description{}) + val2 := types.NewValidator(addrVals[1], PKs[1], types.Description{}) + + val1, _ = val1.SetInitialCommission(commission1) + val2, _ = val2.SetInitialCommission(commission2) + + testCases := []struct { + validator types.Validator + newRate sdk.Dec + expectedErr bool + }{ + {val1, sdk.ZeroDec(), true}, + {val2, sdk.NewDecWithPrec(-1, 1), true}, + {val2, sdk.NewDecWithPrec(4, 1), true}, + {val2, sdk.NewDecWithPrec(3, 1), true}, + {val2, sdk.NewDecWithPrec(2, 1), false}, + } + + for i, tc := range testCases { + err := keeper.UpdateValidatorCommission(ctx, tc.validator, tc.newRate) + + if tc.expectedErr { + require.Error(t, err, "expected error for test case #%d with rate: %s", i, tc.newRate) + } else { + val, found := keeper.GetValidator(ctx, tc.validator.OperatorAddr) + + require.True(t, found, + "expected to find validator for test case #%d with rate: %s", i, tc.newRate, + ) + require.NoError(t, err, + "unexpected error for test case #%d with rate: %s", i, tc.newRate, + ) + require.Equal(t, tc.newRate, val.Commission.Rate, + "expected new validator commission rate for test case #%d with rate: %s", i, tc.newRate, + ) + require.Equal(t, ctx.BlockHeader().Time, val.Commission.UpdateTime, + "expected new validator commission update time for test case #%d with rate: %s", i, tc.newRate, + ) + } + } +} diff --git a/x/stake/simulation/msgs.go b/x/stake/simulation/msgs.go index 5b2bc1ee8..76f778a15 100644 --- a/x/stake/simulation/msgs.go +++ b/x/stake/simulation/msgs.go @@ -23,32 +23,48 @@ func SimulateMsgCreateValidator(m auth.AccountMapper, k stake.Keeper) simulation description := stake.Description{ Moniker: simulation.RandStringOfLength(r, 10), } + + maxCommission := sdk.NewInt(10) + commission := stake.NewCommissionMsg( + sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), + sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), + sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), + ) + key := simulation.RandomKey(r, keys) pubkey := key.PubKey() address := sdk.ValAddress(pubkey.Address()) amount := m.GetAccount(ctx, sdk.AccAddress(address)).GetCoins().AmountOf(denom) + if amount.GT(sdk.ZeroInt()) { amount = simulation.RandomAmount(r, amount) } + if amount.Equal(sdk.ZeroInt()) { return "no-operation", nil, nil } + msg := stake.MsgCreateValidator{ Description: description, + Commission: commission, ValidatorAddr: address, DelegatorAddr: sdk.AccAddress(address), PubKey: pubkey, Delegation: sdk.NewCoin(denom, amount), } + 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("stake/MsgCreateValidator/%v", result.IsOK())) + // require.True(t, result.IsOK(), "expected OK result but instead got %v", result) action = fmt.Sprintf("TestMsgCreateValidator: ok %v, msg %s", result.IsOK(), msg.GetSignBytes()) return action, nil, nil @@ -66,16 +82,24 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { Website: simulation.RandStringOfLength(r, 10), Details: simulation.RandStringOfLength(r, 10), } + + maxCommission := sdk.NewInt(10) + newCommissionRate := sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1) + key := simulation.RandomKey(r, keys) pubkey := key.PubKey() address := sdk.ValAddress(pubkey.Address()) + msg := stake.MsgEditValidator{ - Description: description, - ValidatorAddr: address, + Description: description, + ValidatorAddr: address, + CommissionRate: &newCommissionRate, } + 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() { diff --git a/x/stake/stake.go b/x/stake/stake.go index 18b99fd4a..b421a5d25 100644 --- a/x/stake/stake.go +++ b/x/stake/stake.go @@ -12,6 +12,7 @@ type ( Keeper = keeper.Keeper Validator = types.Validator Description = types.Description + Commission = types.Commission Delegation = types.Delegation DelegationSummary = types.DelegationSummary UnbondingDelegation = types.UnbondingDelegation @@ -64,13 +65,16 @@ var ( GetREDsToValDstIndexKey = keeper.GetREDsToValDstIndexKey GetREDsByDelToValDstIndexKey = keeper.GetREDsByDelToValDstIndexKey - DefaultParams = types.DefaultParams - InitialPool = types.InitialPool - NewValidator = types.NewValidator - NewDescription = types.NewDescription - NewGenesisState = types.NewGenesisState - DefaultGenesisState = types.DefaultGenesisState - RegisterCodec = types.RegisterCodec + DefaultParams = types.DefaultParams + InitialPool = types.InitialPool + NewValidator = types.NewValidator + NewDescription = types.NewDescription + NewCommission = types.NewCommission + NewCommissionMsg = types.NewCommissionMsg + NewCommissionWithTime = types.NewCommissionWithTime + NewGenesisState = types.NewGenesisState + DefaultGenesisState = types.DefaultGenesisState + RegisterCodec = types.RegisterCodec NewMsgCreateValidator = types.NewMsgCreateValidator NewMsgCreateValidatorOnBehalfOf = types.NewMsgCreateValidatorOnBehalfOf diff --git a/x/stake/types/commission.go b/x/stake/types/commission.go new file mode 100644 index 000000000..b76971faa --- /dev/null +++ b/x/stake/types/commission.go @@ -0,0 +1,128 @@ +package types + +import ( + "fmt" + "time" + + sdk "github.com/cosmos/cosmos-sdk/types" +) + +type ( + // Commission defines a commission parameters for a given validator. + Commission struct { + Rate sdk.Dec `json:"rate"` // the commission rate charged to delegators + MaxRate sdk.Dec `json:"max_rate"` // maximum commission rate which validator can ever charge + MaxChangeRate sdk.Dec `json:"max_change_rate"` // maximum daily increase of the validator commission + UpdateTime time.Time `json:"update_time"` // the last time the commission rate was changed + } + + // CommissionMsg defines a commission message to be used for creating a + // validator. + CommissionMsg struct { + Rate sdk.Dec `json:"rate"` // the commission rate charged to delegators + MaxRate sdk.Dec `json:"max_rate"` // maximum commission rate which validator can ever charge + MaxChangeRate sdk.Dec `json:"max_change_rate"` // maximum daily increase of the validator commission + } +) + +// NewCommissionMsg returns an initialized validator commission message. +func NewCommissionMsg(rate, maxRate, maxChangeRate sdk.Dec) CommissionMsg { + return CommissionMsg{ + Rate: rate, + MaxRate: maxRate, + MaxChangeRate: maxChangeRate, + } +} + +// NewCommission returns an initialized validator commission. +func NewCommission(rate, maxRate, maxChangeRate sdk.Dec) Commission { + return Commission{ + Rate: rate, + MaxRate: maxRate, + MaxChangeRate: maxChangeRate, + UpdateTime: time.Unix(0, 0).UTC(), + } +} + +// NewCommission returns an initialized validator commission with a specified +// update time which should be the current block BFT time. +func NewCommissionWithTime(rate, maxRate, maxChangeRate sdk.Dec, updatedAt time.Time) Commission { + return Commission{ + Rate: rate, + MaxRate: maxRate, + MaxChangeRate: maxChangeRate, + UpdateTime: updatedAt, + } +} + +// Equal checks if the given Commission object is equal to the receiving +// Commission object. +func (c Commission) Equal(c2 Commission) bool { + return c.Rate.Equal(c2.Rate) && + c.MaxRate.Equal(c2.MaxRate) && + c.MaxChangeRate.Equal(c2.MaxChangeRate) && + c.UpdateTime.Equal(c2.UpdateTime) +} + +// String implements the Stringer interface for a Commission. +func (c Commission) String() string { + return fmt.Sprintf("rate: %s, maxRate: %s, maxChangeRate: %s, updateTime: %s", + c.Rate, c.MaxRate, c.MaxChangeRate, c.UpdateTime, + ) +} + +// Validate performs basic sanity validation checks of initial commission +// parameters. If validation fails, an SDK error is returned. +func (c Commission) Validate() sdk.Error { + switch { + case c.MaxRate.LT(sdk.ZeroDec()): + // max rate cannot be negative + return ErrCommissionNegative(DefaultCodespace) + + case c.MaxRate.GT(sdk.OneDec()): + // max rate cannot be greater than 100% + return ErrCommissionHuge(DefaultCodespace) + + case c.Rate.LT(sdk.ZeroDec()): + // rate cannot be negative + return ErrCommissionNegative(DefaultCodespace) + + case c.Rate.GT(c.MaxRate): + // rate cannot be greater than the max rate + return ErrCommissionGTMaxRate(DefaultCodespace) + + case c.MaxChangeRate.LT(sdk.ZeroDec()): + // change rate cannot be negative + return ErrCommissionChangeRateNegative(DefaultCodespace) + + case c.MaxChangeRate.GT(c.MaxRate): + // change rate cannot be greater than the max rate + return ErrCommissionChangeRateGTMaxRate(DefaultCodespace) + } + + return nil +} + +// ValidateNewRate performs basic sanity validation checks of a new commission +// rate. If validation fails, an SDK error is returned. +func (c Commission) ValidateNewRate(newRate sdk.Dec, blockTime time.Time) sdk.Error { + switch { + case blockTime.Sub(c.UpdateTime).Hours() < 24: + // new rate cannot be changed more than once within 24 hours + return ErrCommissionUpdateTime(DefaultCodespace) + + case newRate.LT(sdk.ZeroDec()): + // new rate cannot be negative + return ErrCommissionNegative(DefaultCodespace) + + case newRate.GT(c.MaxRate): + // new rate cannot be greater than the max rate + return ErrCommissionGTMaxRate(DefaultCodespace) + + case newRate.Sub(c.Rate).Abs().GT(c.MaxChangeRate): + // new rate % points change cannot be greater than the max change rate + return ErrCommissionGTMaxChangeRate(DefaultCodespace) + } + + return nil +} diff --git a/x/stake/types/errors.go b/x/stake/types/errors.go index 366012bbf..84a7e5ae6 100644 --- a/x/stake/types/errors.go +++ b/x/stake/types/errors.go @@ -65,6 +65,26 @@ func ErrCommissionHuge(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be more than 100%") } +func ErrCommissionGTMaxRate(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be more than the max rate") +} + +func ErrCommissionUpdateTime(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be changed more than once in 24h") +} + +func ErrCommissionChangeRateNegative(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "commission change rate must be positive") +} + +func ErrCommissionChangeRateGTMaxRate(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "commission change rate cannot be more than the max rate") +} + +func ErrCommissionGTMaxChangeRate(codespace sdk.CodespaceType) sdk.Error { + return sdk.NewError(codespace, CodeInvalidValidator, "commission cannot be changed more than max change rate") +} + func ErrNilDelegatorAddr(codespace sdk.CodespaceType) sdk.Error { return sdk.NewError(codespace, CodeInvalidInput, "delegator address is nil") } diff --git a/x/stake/types/msg.go b/x/stake/types/msg.go index 558c913fc..a313dd64f 100644 --- a/x/stake/types/msg.go +++ b/x/stake/types/msg.go @@ -20,6 +20,7 @@ var _, _ sdk.Msg = &MsgBeginRedelegate{}, &MsgCompleteRedelegate{} // MsgCreateValidator - struct for unbonding transactions type MsgCreateValidator struct { Description + Commission CommissionMsg DelegatorAddr sdk.AccAddress `json:"delegator_address"` ValidatorAddr sdk.ValAddress `json:"validator_address"` PubKey crypto.PubKey `json:"pubkey"` @@ -28,22 +29,23 @@ type MsgCreateValidator struct { // Default way to create validator. Delegator address and validator address are the same func NewMsgCreateValidator(valAddr sdk.ValAddress, pubkey crypto.PubKey, - selfDelegation sdk.Coin, description Description) MsgCreateValidator { + selfDelegation sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator { return NewMsgCreateValidatorOnBehalfOf( - sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description, + sdk.AccAddress(valAddr), valAddr, pubkey, selfDelegation, description, commission, ) } // Creates validator msg by delegator address on behalf of validator address func NewMsgCreateValidatorOnBehalfOf(delAddr sdk.AccAddress, valAddr sdk.ValAddress, - pubkey crypto.PubKey, delegation sdk.Coin, description Description) MsgCreateValidator { + pubkey crypto.PubKey, delegation sdk.Coin, description Description, commission CommissionMsg) MsgCreateValidator { return MsgCreateValidator{ Description: description, DelegatorAddr: delAddr, ValidatorAddr: valAddr, PubKey: pubkey, Delegation: delegation, + Commission: commission, } } @@ -95,10 +97,13 @@ func (msg MsgCreateValidator) ValidateBasic() sdk.Error { if !(msg.Delegation.Amount.GT(sdk.ZeroInt())) { return ErrBadDelegationAmount(DefaultCodespace) } - empty := Description{} - if msg.Description == empty { + if msg.Description == (Description{}) { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "description must be included") } + if msg.Commission == (CommissionMsg{}) { + return sdk.NewError(DefaultCodespace, CodeInvalidInput, "commission must be included") + } + return nil } @@ -108,12 +113,20 @@ func (msg MsgCreateValidator) ValidateBasic() sdk.Error { type MsgEditValidator struct { Description ValidatorAddr sdk.ValAddress `json:"address"` + + // We pass a reference to the new commission rate as it's not mandatory to + // update. If not updated, the deserialized rate will be zero with no way to + // distinguish if an update was intended. + // + // REF: #2373 + CommissionRate *sdk.Dec `json:"commission_rate"` } -func NewMsgEditValidator(valAddr sdk.ValAddress, description Description) MsgEditValidator { +func NewMsgEditValidator(valAddr sdk.ValAddress, description Description, newRate *sdk.Dec) MsgEditValidator { return MsgEditValidator{ - Description: description, - ValidatorAddr: valAddr, + Description: description, + CommissionRate: newRate, + ValidatorAddr: valAddr, } } @@ -144,10 +157,11 @@ func (msg MsgEditValidator) ValidateBasic() sdk.Error { if msg.ValidatorAddr == nil { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "nil validator address") } - empty := Description{} - if msg.Description == empty { + + if msg.Description == (Description{}) { return sdk.NewError(DefaultCodespace, CodeInvalidInput, "transaction must include some information to modify") } + return nil } diff --git a/x/stake/types/msg_test.go b/x/stake/types/msg_test.go index eb66c0422..b5adbd0ad 100644 --- a/x/stake/types/msg_test.go +++ b/x/stake/types/msg_test.go @@ -17,26 +17,30 @@ var ( // test ValidateBasic for MsgCreateValidator func TestMsgCreateValidator(t *testing.T) { + commission1 := NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) + commission2 := NewCommissionMsg(sdk.NewDec(5), sdk.NewDec(5), sdk.NewDec(5)) + tests := []struct { name, moniker, identity, website, details string + commissionMsg CommissionMsg validatorAddr sdk.ValAddress pubkey crypto.PubKey bond sdk.Coin expectPass bool }{ - {"basic good", "a", "b", "c", "d", addr1, pk1, coinPos, true}, - {"partial description", "", "", "c", "", addr1, pk1, coinPos, true}, - {"empty description", "", "", "", "", addr1, pk1, coinPos, false}, - {"empty address", "a", "b", "c", "d", emptyAddr, pk1, coinPos, false}, - {"empty pubkey", "a", "b", "c", "d", addr1, emptyPubkey, coinPos, true}, - {"empty bond", "a", "b", "c", "d", addr1, pk1, coinZero, false}, - {"negative bond", "a", "b", "c", "d", addr1, pk1, coinNeg, false}, - {"negative bond", "a", "b", "c", "d", addr1, pk1, coinNeg, false}, + {"basic good", "a", "b", "c", "d", commission1, addr1, pk1, coinPos, true}, + {"partial description", "", "", "c", "", commission1, addr1, pk1, coinPos, true}, + {"empty description", "", "", "", "", commission2, addr1, pk1, coinPos, false}, + {"empty address", "a", "b", "c", "d", commission2, emptyAddr, pk1, coinPos, false}, + {"empty pubkey", "a", "b", "c", "d", commission1, addr1, emptyPubkey, coinPos, true}, + {"empty bond", "a", "b", "c", "d", commission2, addr1, pk1, coinZero, false}, + {"negative bond", "a", "b", "c", "d", commission2, addr1, pk1, coinNeg, false}, + {"negative bond", "a", "b", "c", "d", commission1, addr1, pk1, coinNeg, false}, } for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) - msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description) + msg := NewMsgCreateValidator(tc.validatorAddr, tc.pubkey, tc.bond, description, tc.commissionMsg) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) } else { @@ -60,7 +64,9 @@ func TestMsgEditValidator(t *testing.T) { for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) - msg := NewMsgEditValidator(tc.validatorAddr, description) + newRate := sdk.ZeroDec() + + msg := NewMsgEditValidator(tc.validatorAddr, description, &newRate) if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) } else { @@ -71,28 +77,35 @@ func TestMsgEditValidator(t *testing.T) { // test ValidateBasic and GetSigners for MsgCreateValidatorOnBehalfOf func TestMsgCreateValidatorOnBehalfOf(t *testing.T) { + commission1 := NewCommissionMsg(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()) + commission2 := NewCommissionMsg(sdk.NewDec(5), sdk.NewDec(5), sdk.NewDec(5)) + tests := []struct { name, moniker, identity, website, details string + commissionMsg CommissionMsg delegatorAddr sdk.AccAddress validatorAddr sdk.ValAddress validatorPubKey crypto.PubKey bond sdk.Coin expectPass bool }{ - {"basic good", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, - {"partial description", "", "", "c", "", sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, - {"empty description", "", "", "", "", sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, - {"empty delegator address", "a", "b", "c", "d", sdk.AccAddress(emptyAddr), addr2, pk2, coinPos, false}, - {"empty validator address", "a", "b", "c", "d", sdk.AccAddress(addr1), emptyAddr, pk2, coinPos, false}, - {"empty pubkey", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, emptyPubkey, coinPos, true}, - {"empty bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinZero, false}, - {"negative bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinNeg, false}, - {"negative bond", "a", "b", "c", "d", sdk.AccAddress(addr1), addr2, pk2, coinNeg, false}, + {"basic good", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, + {"partial description", "", "", "c", "", commission2, sdk.AccAddress(addr1), addr2, pk2, coinPos, true}, + {"empty description", "", "", "", "", commission1, sdk.AccAddress(addr1), addr2, pk2, coinPos, false}, + {"empty delegator address", "a", "b", "c", "d", commission1, sdk.AccAddress(emptyAddr), addr2, pk2, coinPos, false}, + {"empty validator address", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), emptyAddr, pk2, coinPos, false}, + {"empty pubkey", "a", "b", "c", "d", commission1, sdk.AccAddress(addr1), addr2, emptyPubkey, coinPos, true}, + {"empty bond", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinZero, false}, + {"negative bond", "a", "b", "c", "d", commission1, sdk.AccAddress(addr1), addr2, pk2, coinNeg, false}, + {"negative bond", "a", "b", "c", "d", commission2, sdk.AccAddress(addr1), addr2, pk2, coinNeg, false}, } for _, tc := range tests { description := NewDescription(tc.moniker, tc.identity, tc.website, tc.details) - msg := NewMsgCreateValidatorOnBehalfOf(tc.delegatorAddr, tc.validatorAddr, tc.validatorPubKey, tc.bond, description) + msg := NewMsgCreateValidatorOnBehalfOf( + tc.delegatorAddr, tc.validatorAddr, tc.validatorPubKey, tc.bond, description, tc.commissionMsg, + ) + if tc.expectPass { require.Nil(t, msg.ValidateBasic(), "test: %v", tc.name) } else { @@ -100,11 +113,11 @@ func TestMsgCreateValidatorOnBehalfOf(t *testing.T) { } } - msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{}) + msg := NewMsgCreateValidator(addr1, pk1, coinPos, Description{}, CommissionMsg{}) addrs := msg.GetSigners() require.Equal(t, []sdk.AccAddress{sdk.AccAddress(addr1)}, addrs, "Signers on default msg is wrong") - msg = NewMsgCreateValidatorOnBehalfOf(sdk.AccAddress(addr2), addr1, pk1, coinPos, Description{}) + msg = NewMsgCreateValidatorOnBehalfOf(sdk.AccAddress(addr2), addr1, pk1, coinPos, Description{}, CommissionMsg{}) addrs = msg.GetSigners() require.Equal(t, []sdk.AccAddress{sdk.AccAddress(addr2), sdk.AccAddress(addr1)}, addrs, "Signers for onbehalfof msg is wrong") } diff --git a/x/stake/types/validator.go b/x/stake/types/validator.go index 6c5066a78..f124ab595 100644 --- a/x/stake/types/validator.go +++ b/x/stake/types/validator.go @@ -36,68 +36,56 @@ type Validator struct { UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding - Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators - CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge - CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission - CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) + Commission Commission `json:"commission"` // commission parameters } // NewValidator - initialize a new validator func NewValidator(operator sdk.ValAddress, pubKey crypto.PubKey, description Description) Validator { return Validator{ - OperatorAddr: operator, - ConsPubKey: pubKey, - Jailed: false, - Status: sdk.Unbonded, - Tokens: sdk.ZeroDec(), - DelegatorShares: sdk.ZeroDec(), - Description: description, - BondHeight: int64(0), - BondIntraTxCounter: int16(0), - UnbondingHeight: int64(0), - UnbondingMinTime: time.Unix(0, 0).UTC(), - Commission: sdk.ZeroDec(), - CommissionMax: sdk.ZeroDec(), - CommissionChangeRate: sdk.ZeroDec(), - CommissionChangeToday: sdk.ZeroDec(), + OperatorAddr: operator, + ConsPubKey: pubKey, + Jailed: false, + Status: sdk.Unbonded, + Tokens: sdk.ZeroDec(), + DelegatorShares: sdk.ZeroDec(), + Description: description, + BondHeight: int64(0), + BondIntraTxCounter: int16(0), + UnbondingHeight: int64(0), + UnbondingMinTime: time.Unix(0, 0).UTC(), + Commission: NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), } } // what's kept in the store value type validatorValue struct { - ConsPubKey crypto.PubKey - Jailed bool - Status sdk.BondStatus - Tokens sdk.Dec - DelegatorShares sdk.Dec - Description Description - BondHeight int64 - BondIntraTxCounter int16 - UnbondingHeight int64 - UnbondingMinTime time.Time - Commission sdk.Dec - CommissionMax sdk.Dec - CommissionChangeRate sdk.Dec - CommissionChangeToday sdk.Dec + ConsPubKey crypto.PubKey + Jailed bool + Status sdk.BondStatus + Tokens sdk.Dec + DelegatorShares sdk.Dec + Description Description + BondHeight int64 + BondIntraTxCounter int16 + UnbondingHeight int64 + UnbondingMinTime time.Time + Commission Commission } // return the redelegation without fields contained within the key for the store func MustMarshalValidator(cdc *codec.Codec, validator Validator) []byte { val := validatorValue{ - ConsPubKey: validator.ConsPubKey, - Jailed: validator.Jailed, - Status: validator.Status, - Tokens: validator.Tokens, - DelegatorShares: validator.DelegatorShares, - Description: validator.Description, - BondHeight: validator.BondHeight, - BondIntraTxCounter: validator.BondIntraTxCounter, - UnbondingHeight: validator.UnbondingHeight, - UnbondingMinTime: validator.UnbondingMinTime, - Commission: validator.Commission, - CommissionMax: validator.CommissionMax, - CommissionChangeRate: validator.CommissionChangeRate, - CommissionChangeToday: validator.CommissionChangeToday, + ConsPubKey: validator.ConsPubKey, + Jailed: validator.Jailed, + Status: validator.Status, + Tokens: validator.Tokens, + DelegatorShares: validator.DelegatorShares, + Description: validator.Description, + BondHeight: validator.BondHeight, + BondIntraTxCounter: validator.BondIntraTxCounter, + UnbondingHeight: validator.UnbondingHeight, + UnbondingMinTime: validator.UnbondingMinTime, + Commission: validator.Commission, } return cdc.MustMarshalBinary(val) } @@ -124,21 +112,18 @@ func UnmarshalValidator(cdc *codec.Codec, operatorAddr, value []byte) (validator } return Validator{ - OperatorAddr: operatorAddr, - ConsPubKey: storeValue.ConsPubKey, - Jailed: storeValue.Jailed, - Tokens: storeValue.Tokens, - Status: storeValue.Status, - DelegatorShares: storeValue.DelegatorShares, - Description: storeValue.Description, - BondHeight: storeValue.BondHeight, - BondIntraTxCounter: storeValue.BondIntraTxCounter, - UnbondingHeight: storeValue.UnbondingHeight, - UnbondingMinTime: storeValue.UnbondingMinTime, - Commission: storeValue.Commission, - CommissionMax: storeValue.CommissionMax, - CommissionChangeRate: storeValue.CommissionChangeRate, - CommissionChangeToday: storeValue.CommissionChangeToday, + OperatorAddr: operatorAddr, + ConsPubKey: storeValue.ConsPubKey, + Jailed: storeValue.Jailed, + Tokens: storeValue.Tokens, + Status: storeValue.Status, + DelegatorShares: storeValue.DelegatorShares, + Description: storeValue.Description, + BondHeight: storeValue.BondHeight, + BondIntraTxCounter: storeValue.BondIntraTxCounter, + UnbondingHeight: storeValue.UnbondingHeight, + UnbondingMinTime: storeValue.UnbondingMinTime, + Commission: storeValue.Commission, }, nil } @@ -156,16 +141,13 @@ func (v Validator) HumanReadableString() (string, error) { resp += fmt.Sprintf("Validator Consensus Pubkey: %s\n", bechConsPubKey) resp += fmt.Sprintf("Jailed: %v\n", v.Jailed) resp += fmt.Sprintf("Status: %s\n", sdk.BondStatusToString(v.Status)) - resp += fmt.Sprintf("Tokens: %s\n", v.Tokens.String()) - resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares.String()) + resp += fmt.Sprintf("Tokens: %s\n", v.Tokens) + resp += fmt.Sprintf("Delegator Shares: %s\n", v.DelegatorShares) resp += fmt.Sprintf("Description: %s\n", v.Description) resp += fmt.Sprintf("Bond Height: %d\n", v.BondHeight) resp += fmt.Sprintf("Unbonding Height: %d\n", v.UnbondingHeight) resp += fmt.Sprintf("Minimum Unbonding Time: %v\n", v.UnbondingMinTime) - resp += fmt.Sprintf("Commission: %s\n", v.Commission.String()) - resp += fmt.Sprintf("Max Commission Rate: %s\n", v.CommissionMax.String()) - resp += fmt.Sprintf("Commission Change Rate: %s\n", v.CommissionChangeRate.String()) - resp += fmt.Sprintf("Commission Change Today: %s\n", v.CommissionChangeToday.String()) + resp += fmt.Sprintf("Commission: {%s}\n", v.Commission) return resp, nil } @@ -189,10 +171,7 @@ type bechValidator struct { UnbondingHeight int64 `json:"unbonding_height"` // if unbonding, height at which this validator has begun unbonding UnbondingMinTime time.Time `json:"unbonding_time"` // if unbonding, min time for the validator to complete unbonding - Commission sdk.Dec `json:"commission"` // XXX the commission rate of fees charged to any delegators - CommissionMax sdk.Dec `json:"commission_max"` // XXX maximum commission rate which this validator can ever charge - CommissionChangeRate sdk.Dec `json:"commission_change_rate"` // XXX maximum daily increase of the validator commission - CommissionChangeToday sdk.Dec `json:"commission_change_today"` // XXX commission rate change today, reset each day (UTC time) + Commission Commission `json:"commission"` // commission parameters } // MarshalJSON marshals the validator to JSON using Bech32 @@ -203,21 +182,18 @@ func (v Validator) MarshalJSON() ([]byte, error) { } return codec.Cdc.MarshalJSON(bechValidator{ - OperatorAddr: v.OperatorAddr, - ConsPubKey: bechConsPubKey, - Jailed: v.Jailed, - Status: v.Status, - Tokens: v.Tokens, - DelegatorShares: v.DelegatorShares, - Description: v.Description, - BondHeight: v.BondHeight, - BondIntraTxCounter: v.BondIntraTxCounter, - UnbondingHeight: v.UnbondingHeight, - UnbondingMinTime: v.UnbondingMinTime, - Commission: v.Commission, - CommissionMax: v.CommissionMax, - CommissionChangeRate: v.CommissionChangeRate, - CommissionChangeToday: v.CommissionChangeToday, + OperatorAddr: v.OperatorAddr, + ConsPubKey: bechConsPubKey, + Jailed: v.Jailed, + Status: v.Status, + Tokens: v.Tokens, + DelegatorShares: v.DelegatorShares, + Description: v.Description, + BondHeight: v.BondHeight, + BondIntraTxCounter: v.BondIntraTxCounter, + UnbondingHeight: v.UnbondingHeight, + UnbondingMinTime: v.UnbondingMinTime, + Commission: v.Commission, }) } @@ -232,21 +208,18 @@ func (v *Validator) UnmarshalJSON(data []byte) error { return err } *v = Validator{ - OperatorAddr: bv.OperatorAddr, - ConsPubKey: consPubKey, - Jailed: bv.Jailed, - Tokens: bv.Tokens, - Status: bv.Status, - DelegatorShares: bv.DelegatorShares, - Description: bv.Description, - BondHeight: bv.BondHeight, - BondIntraTxCounter: bv.BondIntraTxCounter, - UnbondingHeight: bv.UnbondingHeight, - UnbondingMinTime: bv.UnbondingMinTime, - Commission: bv.Commission, - CommissionMax: bv.CommissionMax, - CommissionChangeRate: bv.CommissionChangeRate, - CommissionChangeToday: bv.CommissionChangeToday, + OperatorAddr: bv.OperatorAddr, + ConsPubKey: consPubKey, + Jailed: bv.Jailed, + Tokens: bv.Tokens, + Status: bv.Status, + DelegatorShares: bv.DelegatorShares, + Description: bv.Description, + BondHeight: bv.BondHeight, + BondIntraTxCounter: bv.BondIntraTxCounter, + UnbondingHeight: bv.UnbondingHeight, + UnbondingMinTime: bv.UnbondingMinTime, + Commission: bv.Commission, } return nil } @@ -254,18 +227,14 @@ func (v *Validator) UnmarshalJSON(data []byte) error { //___________________________________________________________________ // only the vitals - does not check bond height of IntraTxCounter -// nolint gocyclo - why dis fail? -func (v Validator) Equal(c2 Validator) bool { - return v.ConsPubKey.Equals(c2.ConsPubKey) && - bytes.Equal(v.OperatorAddr, c2.OperatorAddr) && - v.Status.Equal(c2.Status) && - v.Tokens.Equal(c2.Tokens) && - v.DelegatorShares.Equal(c2.DelegatorShares) && - v.Description == c2.Description && - v.Commission.Equal(c2.Commission) && - v.CommissionMax.Equal(c2.CommissionMax) && - v.CommissionChangeRate.Equal(c2.CommissionChangeRate) && - v.CommissionChangeToday.Equal(c2.CommissionChangeToday) +func (v Validator) Equal(v2 Validator) bool { + return v.ConsPubKey.Equals(v2.ConsPubKey) && + bytes.Equal(v.OperatorAddr, v2.OperatorAddr) && + v.Status.Equal(v2.Status) && + v.Tokens.Equal(v2.Tokens) && + v.DelegatorShares.Equal(v2.DelegatorShares) && + v.Description == v2.Description && + v.Commission.Equal(v2.Commission) } // return the TM validator address @@ -400,6 +369,17 @@ func (v Validator) RemoveTokens(pool Pool, tokens sdk.Dec) (Validator, Pool) { return v, pool } +// SetInitialCommission attempts to set a validator's initial commission. An +// error is returned if the commission is invalid. +func (v Validator) SetInitialCommission(commission Commission) (Validator, sdk.Error) { + if err := commission.Validate(); err != nil { + return v, err + } + + v.Commission = commission + return v, nil +} + //_________________________________________________________________________________________________________ // AddTokensFromDel adds tokens to a validator diff --git a/x/stake/types/validator_test.go b/x/stake/types/validator_test.go index eceffbad3..de9e18480 100644 --- a/x/stake/types/validator_test.go +++ b/x/stake/types/validator_test.go @@ -274,3 +274,37 @@ func TestValidatorMarshalUnmarshalJSON(t *testing.T) { assert.NoError(t, err) assert.Equal(t, validator, *got) } + +func TestValidatorSetInitialCommission(t *testing.T) { + val := NewValidator(addr1, pk1, Description{}) + testCases := []struct { + validator Validator + commission Commission + expectedErr bool + }{ + {val, NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), false}, + {val, NewCommission(sdk.ZeroDec(), sdk.NewDecWithPrec(-1, 1), sdk.ZeroDec()), true}, + {val, NewCommission(sdk.ZeroDec(), sdk.NewDec(15000000000), sdk.ZeroDec()), true}, + {val, NewCommission(sdk.NewDecWithPrec(-1, 1), sdk.ZeroDec(), sdk.ZeroDec()), true}, + {val, NewCommission(sdk.NewDecWithPrec(2, 1), sdk.NewDecWithPrec(1, 1), sdk.ZeroDec()), true}, + {val, NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.NewDecWithPrec(-1, 1)), true}, + {val, NewCommission(sdk.ZeroDec(), sdk.NewDecWithPrec(1, 1), sdk.NewDecWithPrec(2, 1)), true}, + } + + for i, tc := range testCases { + val, err := tc.validator.SetInitialCommission(tc.commission) + + if tc.expectedErr { + require.Error(t, err, + "expected error for test case #%d with commission: %s", i, tc.commission, + ) + } else { + require.NoError(t, err, + "unexpected error for test case #%d with commission: %s", i, tc.commission, + ) + require.Equal(t, tc.commission, val.Commission, + "invalid validator commission for test case #%d with commission: %s", i, tc.commission, + ) + } + } +}