diff --git a/builtin/plugins/dposv3/dpos.go b/builtin/plugins/dposv3/dpos.go index ccbb3e95cf..b681b09a8b 100644 --- a/builtin/plugins/dposv3/dpos.go +++ b/builtin/plugins/dposv3/dpos.go @@ -74,6 +74,7 @@ var ( errDistributionNotFound = errors.New("Distribution record not found.") errOnlyOracle = errors.New("Function can only be called with oracle address.") errDelegationLocked = errors.New("Delegation currently locked.") + errMigrationModeEnabled = errors.New("Migration mode enabled.") ) type ( @@ -129,6 +130,7 @@ type ( SetMaxDowntimePercentageRequest = dtypes.SetMaxDowntimePercentageRequest EnableValidatorJailingRequest = dtypes.EnableValidatorJailingRequest IgnoreUnbondLocktimeRequest = dtypes.IgnoreUnbondLocktimeRequest + ToggleMigrationModeRequest = dtypes.ToggleMigrationModeRequest Candidate = dtypes.Candidate CandidateStatistic = dtypes.CandidateStatistic Delegation = dtypes.Delegation @@ -258,6 +260,16 @@ func (c *DPOS) Delegate(ctx contract.Context, req *DelegateRequest) error { delegator := ctx.Message().Sender ctx.Logger().Info("DPOSv3 Delegate", "delegator", delegator, "request", req) + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return err + } + if state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + } + if req.ValidatorAddress == nil { return logDposError(ctx, errors.New("Delegate called with req.ValidatorAddress == nil"), req.String()) } @@ -341,6 +353,16 @@ func (c *DPOS) Delegate(ctx contract.Context, req *DelegateRequest) error { } func (c *DPOS) Redelegate(ctx contract.Context, req *RedelegateRequest) error { + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return err + } + if state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + } + delegator := ctx.Message().Sender if req.ValidatorAddress == nil { @@ -480,6 +502,16 @@ func (c *DPOS) ConsolidateDelegations(ctx contract.Context, req *ConsolidateDele delegator := ctx.Message().Sender ctx.Logger().Info("DPOSv3 ConsolidateDelegations", "delegator", delegator, "request", req) + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return err + } + if state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + } + // Unless considation is for the limbo validator, check that the new // validator address corresponds to one of the registered candidates if loom.UnmarshalAddressPB(req.ValidatorAddress).Compare(LimboValidatorAddress(ctx)) != 0 { @@ -591,6 +623,16 @@ func (c *DPOS) CheckRewardsFromAllValidators(ctx contract.StaticContext, req *Ch /// a delegator has delegated to, and returns the total amount which will be transferred to the /// delegator's account after the next election. func (c *DPOS) ClaimRewardsFromAllValidators(ctx contract.Context, req *ClaimDelegatorRewardsRequest) (*ClaimDelegatorRewardsResponse, error) { + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return nil, err + } + if state.Params.MigrationModeEnabled { + return nil, errMigrationModeEnabled + } + } + if ctx.FeatureEnabled(features.DPOSVersion3_6, false) { return c.claimRewardsFromAllValidators2(ctx, req) } @@ -717,6 +759,16 @@ func (c *DPOS) Unbond(ctx contract.Context, req *UnbondRequest) error { delegator := ctx.Message().Sender ctx.Logger().Info("DPOSv3 Unbond", "delegator", delegator, "request", req) + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return err + } + if state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + } + if req.ValidatorAddress == nil { return logDposError(ctx, errors.New("Unbond called with req.ValidatorAddress == nil"), req.String()) } else if req.Amount == nil { @@ -1027,6 +1079,10 @@ func (c *DPOS) RegisterCandidate(ctx contract.Context, req *RegisterCandidateReq return err } + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) && state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + if (statistic == nil || common.IsZero(statistic.WhitelistAmount.Value)) && common.IsPositive(state.Params.RegistrationRequirement.Value) { // A currently unregistered candidate must make a loom token deposit // = 'registrationRequirement' in order to run for validator. @@ -1195,6 +1251,16 @@ func (c *DPOS) UpdateCandidateInfo(ctx contract.Context, req *UpdateCandidateInf // Leaving the validator set mid-election period results in a loss of rewards // but it should not result in slashing due to downtime. func (c *DPOS) UnregisterCandidate(ctx contract.Context, req *UnregisterCandidateRequest) error { + if ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + state, err := LoadState(ctx) + if err != nil { + return err + } + if state.Params.MigrationModeEnabled { + return errMigrationModeEnabled + } + } + candidateAddress := ctx.Message().Sender // Allow oracle to specify the candidate @@ -1621,6 +1687,24 @@ func (c *DPOS) IgnoreUnbondLocktime(ctx contract.Context, req *IgnoreUnbondLockt return saveState(ctx, state) } +func (c *DPOS) ToggleMigrationMode(ctx contract.Context, req *ToggleMigrationModeRequest) error { + if !ctx.FeatureEnabled(features.DPOSVersion3_11, false) { + return errors.New("DPOS v3.11 is not enabled") + } + + state, err := LoadState(ctx) + if err != nil { + return err + } + sender := ctx.Message().Sender + if state.Params.OracleAddress == nil || sender.Compare(loom.UnmarshalAddressPB(state.Params.OracleAddress)) != 0 { + return errOnlyOracle + } + + state.Params.MigrationModeEnabled = !state.Params.MigrationModeEnabled + return saveState(ctx, state) +} + // *************************** // REWARDS & SLASHING // *************************** diff --git a/cmd/loom/dposV3_commands.go b/cmd/loom/dposV3_commands.go index c55f4f6ac5..e6954f570b 100644 --- a/cmd/loom/dposV3_commands.go +++ b/cmd/loom/dposV3_commands.go @@ -1636,6 +1636,28 @@ func IgnoreUnbondLocktimeCmd() *cobra.Command { return cmd } +const toggleMigrationModeCmdExample = ` +loom dpos3 toggle-migration-mode -k path/to/private_key +` + +func ToggleMigrationModeCmd() *cobra.Command { + var flags cli.ContractCallFlags + cmd := &cobra.Command{ + Use: "toggle-migration-mode", + Example: toggleMigrationModeCmdExample, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cli.CallContractWithFlags( + &flags, DPOSV3ContractName, "ToggleMigrationMode", &dposv3.ToggleMigrationModeRequest{}, nil, + ); err != nil { + return err + } + return nil + }, + } + cli.AddContractCallFlags(cmd.Flags(), &flags) + return cmd +} + func NewDPOSV3Command() *cobra.Command { cmd := &cobra.Command{ Use: "dpos3 ", @@ -1684,6 +1706,7 @@ func NewDPOSV3Command() *cobra.Command { UnjailValidatorCmdV3(), EnableValidatorJailingCmd(), IgnoreUnbondLocktimeCmd(), + ToggleMigrationModeCmd(), ) return cmd } diff --git a/features/features.go b/features/features.go index 050e012533..62f1ff7565 100644 --- a/features/features.go +++ b/features/features.go @@ -61,6 +61,9 @@ const ( DPOSVersion3_9 = "dpos:v3.9" // Makes it possible for the oracle to call Redelegate & UnregisterCandidate DPOSVersion3_10 = "dpos:v3.10" + // Make it possible to put the DPOS contract into migration mode, where + // delegation/redelegation/consolidation and validator registration is disabled + DPOSVersion3_11 = "dpos:v3.11" // Enables rewards to be distributed even when a delegator owns less than 0.01% of the validator's stake // Also makes whitelists give bonuses correctly if whitelist locktime tier is set to be 0-3 (else defaults to 5%)