keeper

package
v23.0.0-rc1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 12, 2024 License: Apache-2.0 Imports: 16 Imported by: 0

README

validator-set Preference

Abstract

Validator-Set preference is a new module which gives users and contracts a better UX for staking to a set of validators. For example: a one click button that delegates to multiple validators. Then the user can set (or realistically a frontend provides) a list of recommended defaults (Ex: active governors, relayers, core stack contributors etc). Currently this can be done on-chain with frontends, but having a preference list stored locally eases frontend code burden.

Design

How does this module work?

  • Allow a user to set a list of {val-addr, weight} in the state, called their validator-set preference.
  • Allow a user to update a list of {val-addr, weight} in the state, then do the following;
    • Unstake the existing tokens (run the same unbond logic as cosmos-sdk staking).
    • Update the validator distribution weights.
    • Stake the tokens based on the new weights.
    • Redelegate their current delegation to the currently configured set.
  • Give users a single message to delegate {X} tokens, according to their validator-set preference distribution.
  • Give users a single message to undelegate {X} tokens, according to their validator-set preference distribution.
  • Give users a single message to claim rewards from everyone on their preference list.
  • If the delegator has not set a validator-set preference list then the validator set, then it defaults to their current validator set.
  • If a user has no preference list and has not staked, then these messages / queries return errors.

Logic

Delegate to validator set

This is pretty straight-forward, there's not really any edge cases here.

The user provides an amount to delegate and our MsgDelegateToValidatorSet divides the amount based on validator weight distribution.

For example: Stake 100osmo with validator-set {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2} our delegate logic will attempt to delegate (100 * 0.5) 50osmo for ValA , (100 * 0.3) 30osmo from ValB and (100 * 0.2) 20osmo from ValC.

Undelegate from validator set

We can imagine describing undelegate from validator set in two cases:

  • Users existing delegation distribution matches their validator-set preference distribution.
  • Users existing delegation distribution does not match their validator-set preference distribution.

Algorithm for undelegation; unbond as true to valset ratios as possible. Undelegation should be possible. Idea of what we should be doing for Undelegate(valset, amount):

  1. Calculate the amount to undelegate from each validator under full valset usage
  2. If all amounts are less than amount staked to validator, undelegate all
  3. If any amount is greater than amount staked to validator (S,V), fully unstake S from that validator. Recursively proceed with undelegating the remaining amount from the remaining validators. Undelegate(valset - V, amount - S)

The above algorithm would take O(V^2) worst case, so we instead do something better to be O(V).

  1. Calculate the amount to undelegate from each validator under full valset usage
  2. For each validator, compute V.ratio = undelegate_amount / amount_staked_to_val
  3. Sort validators by V_ratio descending. If V_ratio <= 1, there is no need to re-calculate amount to undelegate for each validator, undelegate and end algorithm.
  4. If V_ratio <= 1, undelegate target amount from each validator. (happy path)
  5. Set target_ratio = 1, amount_remaining_to_unbond = amount
  6. While greatest V_ratio > target_ratio:
    • Fully undelegate validator with greatest V_ratio. (Amount S)
    • remove validator from list
    • recalculate target_ratio = target_ratio * (1 - removed_V.target_percent)
      • this works, because if you recalculated target ratio scaled to 1 every val's ratio would just differ by the constant factor of 1 / (1 - removed_V.target_percent) doing that would take O(V), hence we change the target.
  7. Normal undelegate the remainder.
Case 1: Users existing delegation distribution matches their validator-set preference distribution

{Old docs below}

  • The user provides an amount to undelegate and our MsgUnDelegateToValidatorSet divides the amount based on validator weight distribution.
  • Here, the user can either undelegate the entire amount or partial amount
    • Entire amount unstaking: UnStake 100osmo from validator-set {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2}, our undelegate logic will attempt to undelegate 50osmo from ValA , 30osmo from ValB, 20osmo from ValC
    • Partial amount unstaking: UnStake 27osmo from validator-set {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2}, our undelegate logic will attempt to undelegate (27 * 0.5) 13.5osmos from ValA, (27 * 0.3), 8.1osmo from ValB, and (50 * 0.2) 5.4smo from ValC where 13.5osmo + 8.1osmo + 5.4osmo = 27osmo
    • The user will then have 73osmo remaining with unchanged weights {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2},

Messages

SetValidatorSetPreference

Creates a validator-set of {valAddr, Weight} given the delegator address. and preferences. The weights are in decimal format from 0 to 1 and must add up to 1.

    string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];
    repeated ValidatorPreference preferences = 2 [
      (gogoproto.moretags) = "yaml:\"preferences\"",
      (gogoproto.nullable) = false
    ];

State Modifications:

  • Safety Checks
    • check if the user already has a validator-set created.
    • check if the validator exist and is valid.
    • check if the validator-set add up to 1.
  • Add owner address to the KVStore, where a state of validator-set is stored.
MsgDelegateToValidatorSet

Gets the existing validator-set of the delegator and delegates the given amount. The given amount will be divided based on the weights distributed to the validators. The weights will be unchanged. If the user does not have an existing validator set use delegators' current staking position.

    string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];
    // the amount of tokens the user is trying to delegate.
    // For ex: delegate 10osmo with validator-set {ValA -> 0.5, ValB -> 0.3, ValC
    // -> 0.2} our staking logic would attempt to delegate 5osmo to A , 3osmo to
    // B, 2osmo to C.
    cosmos.base.v1beta1.Coin coin = 2 [
      (gogoproto.nullable) = false,
      (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin"
    ];

State Modifications:

  • Check if the user has a validator-set and if so, get the users validator-set from KVStore.
  • Safety Checks
    • check if the user has enough funds to delegate.
    • check overflow/underflow since Delegate method takes osmomath.Int as tokenAmount.
  • use the Delegate method from the cosmos-sdk to handle delegation.
MsgUndelegateFromValidatorSet

Gets the existing validator-set of the delegator and undelegate the given amount. The amount to undelegate will will be divided based on the weights distributed to the validators. The weights will be unchanged! If the user does not have an existing validator set use delegators' current staking position. The given amount will be divided based on the weights distributed to the validators.

    string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];
    // the amount the user wants to undelegate
    // For ex: Undelegate 10osmo with validator-set {ValA -> 0.5, ValB -> 0.3,
    // ValC
    // -> 0.2} our undelegate logic would attempt to undelegate 5osmo from A ,
    // 3osmo from B, 2osmo from C
    cosmos.base.v1beta1.Coin coin = 3 [
      (gogoproto.nullable) = false,
      (gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin"
    ];

State Modifications:

  • Check if the user has a validator-set and if so, get the users validator-set from KVStore.
  • The unbonding logic will be follow the UnDelegate logic from the cosmos-sdk.
  • Safety Checks
    • check that the amount of funds to undelegate is <= to the funds the user has in the address.
    • UnDelegate method takes osmomath.Dec as tokenAmount, so check if overflow/underflow case is relevant.
  • use the UnDelegate method from the cosmos-sdk to handle delegation.
MsgWithdrawDelegationRewards

Allows the user to claim rewards based from the existing validator-set. The user can claim rewards from all the validators at once. If the user does not have an existing validator set use delegators' current staking position.

    string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];
MsgRedelegateValidatorSet

The redelegation command allows delegators to instantly switch validators. Once the unbonding period has passed, the redelegation is automatically completed in the EndBlocker. If the user does not have an existing validator set use delegators' current staking position.

  // delegator is the user who is trying to create a validator-set.
  string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];

  // list of {valAddr, weight} to delegate to
  repeated ValidatorPreference preferences = 2 [
    (gogoproto.moretags) = "yaml:\"preferences\"",
    (gogoproto.nullable) = false
  ];

Redelegate algorithm logic pseudocode

Existing ValSet 20osmos {ValA-> 0.5, ValB-> 0.3, ValC-> 0.2} [ValA-> 10osmo, ValB-> 6osmo, ValC-> 4osmo] New ValSet 20osmos {ValD-> 0.2, ValE-> 0.2, ValF-> 0.6} [ValD-> 4osmo, ValE-> 4osmo, ValF-> 12osmo]

  • // Rearranging the existingValSet and newValSet to to add extra validator padding

    • existing_valset_updated = [ValA: 10, ValB: 6, ValC: 4, ValD: 0, ValE: 0, ValF: 0]
    • new_valset_updated = [ValD: 4, ValE: 4, ValF: 12, ValA: 0, ValB: 0, ValC: 0]

    // calculate the difference between two sets

    • diff_arr = [ValA: 10, ValB: 6, ValC: 4, ValD: -4, ValE: -4, ValF: -12]

      // Algorithm starts here

  • for _, validator in diff_arr:

    • if validator.amount > 0:
      • for idx, targetDiffVal := range diff_arr // this will gives us target validator
        • if targetDiffVal.Amount < 0 && targetDiffVal.valAddr != validator.Address source_validator = validator.Address target_validator = targetDiffVal.valAddr

          // checks if there are any more redelegation possible if target_validator.amount.Equal(0) { break }

          // reDelegationAmt to is the amount to redelegate, which is the min of diffAmount and target_validator reDelegationAmt = FindMin(abs(target_validator.amount), validator.amount) sdk.BeginRedelegation(ctx, delegator, source_validator, target_validator, reDelegationAmt)

          // Update the current diffAmount by subtracting it with the reDelegationAmount validator.amount = validator.amount - reDelegationAmt // Find target_validator through idx in diffValSet and set that to (target_validatorAmount - reDelegationAmount) diff_arr[idx].amount = target_validator.amount + reDelegationAmt

  • Result

    1. diff_arr = [ValA: 0, ValB: 0, ValC: 0, ValD: 0, ValE: 0, ValF: 0]
    2. [ValA: 0, ValB: 0, ValC: 0, ValD: 4, ValE: 4, ValF: 12] // final result

Redelegation Constraints

  1. ValA -> ValB redelegate upto 7 times in 21 day period
  2. ValA -> ValB (redelegate) ValB -> ValC (redelegate) CONSECUTIVE REDELEGATION DOES NOT WORK
  3. Once you redelegate from ValA -> ValB, you will not be able to redelegate from ValB to another validator for the next 21 days.
  • the validator on the receiving end of redelegation will be on a 21-day redelegation lock
  1. Cannot redelegate to same validator

Code Layout

The Code Layout is very similar to TWAP module.

  • client/* - Implementation of GRPC and CLI queries
  • types/* - Implement ValidatorSetPreference, GenesisState. Define the interface and setup keys.
  • valpref-module/module.go - SDK AppModule interface implementation.
  • api.go - Public API, that other users / modules can/should depend on
  • listeners.go - Defines hooks & calls to logic.go, for triggering actions on
  • keeper.go - generic SDK boilerplate (defining a wrapper for store keys + params)
  • msg_server.go - handle messages request from client and process responses.
  • store.go - Managing logic for getting and setting things to underlying stores (KVStore)

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewMsgServerImpl

func NewMsgServerImpl(keeper *Keeper) types.MsgServer

NewMsgServerImpl returns an implementation of the MsgServer interface for the provided Keeper.

Types

type Keeper

type Keeper struct {
	// contains filtered or unexported fields
}

func NewKeeper

func NewKeeper(storeKey storetypes.StoreKey,
	paramSpace paramtypes.Subspace,
	stakingKeeper types.StakingInterface,
	distirbutionKeeper types.DistributionKeeper,
	lockupKeeper types.LockupKeeper,
) Keeper

func (Keeper) DelegateToValidatorSet

func (k Keeper) DelegateToValidatorSet(ctx sdk.Context, delegatorAddr string, coin sdk.Coin) error

DelegateToValidatorSet delegates to a delegators existing validator-set. If the valset does not exist, it delegates to existing staking position. For ex: delegate 10osmo with validator-set {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2} our delegate logic would attempt to delegate 5osmo to A , 2osmo to B, 3osmo to C nolint: staticcheck

func (Keeper) ForceUnlockBondedOsmo

func (k Keeper) ForceUnlockBondedOsmo(ctx sdk.Context, lockID uint64, delegatorAddr string) (sdk.Coin, error)

ForceUnlockBondedOsmo allows breaking of a bonded lockup (by ID) of osmo, of length <= 2 weeks. We want to later have osmo incentives get auto-staked, we want people w/ no staking positions to get their osmo auto-locked. This function takes all that osmo and stakes according to your current validator set preference. (Note: Noting that there is an implicit valset preference if you've already staked) CONTRACT: This method should **never** be used alone.

func (Keeper) GetDelegationPreferences

func (k Keeper) GetDelegationPreferences(ctx sdk.Context, delegator string) (types.ValidatorSetPreferences, error)

GetDelegationPreferences checks if valset position exists, if it does return that else return existing delegation that's not valset.

func (Keeper) GetValSetPreferencesWithDelegations

func (k Keeper) GetValSetPreferencesWithDelegations(ctx sdk.Context, delegator string) (types.ValidatorSetPreferences, error)

GetValSetPreferencesWithDelegations fetches the delegator's validator set preferences considering their existing delegations. -If validator set preference does not exist and there are no existing delegations, it returns an error. -If validator set preference exists and there are no existing delegations, it returns the existing preference. -If there is any existing delegation: calculates the delegator's shares in each delegation as a ratio of the total shares and returns it as part of ValidatorSetPreferences.

func (Keeper) GetValSetStruct

func (k Keeper) GetValSetStruct(validator types.ValidatorPreference, amountFromShares osmomath.Dec) (valStruct valSet, valStructZeroAmt valSet)

GetValSetStruct initializes valSet struct with valAddr, weight and amount. It also creates an extra struct with zero amount, that can be appended to newValSet that will be created. We do this to make sure the struct array length is the same to calculate their difference.

func (Keeper) GetValidatorInfo

func (k Keeper) GetValidatorInfo(ctx sdk.Context, existingValAddr string) (sdk.ValAddress, stakingtypes.Validator, error)

func (Keeper) GetValidatorSetPreference

func (k Keeper) GetValidatorSetPreference(ctx sdk.Context, delegator string) (types.ValidatorSetPreferences, bool)

GetValidatorSetPreference returns the existing valset position for a delegator.

func (Keeper) IsPreferenceValid

func (k Keeper) IsPreferenceValid(ctx sdk.Context, preferences []types.ValidatorPreference) ([]types.ValidatorPreference, error)

IsPreferenceValid loops through the validator preferences and checks its existence and validity.

func (Keeper) IsValidatorSetEqual

func (k Keeper) IsValidatorSetEqual(newPreferences, existingPreferences []types.ValidatorPreference) bool

IsValidatorSetEqual returns true if the two preferences are equal.

func (Keeper) Logger

func (k Keeper) Logger(ctx sdk.Context) log.Logger

func (Keeper) PreformRedelegation

func (k Keeper) PreformRedelegation(ctx sdk.Context, delegator sdk.AccAddress, existingSet []types.ValidatorPreference, newSet []types.ValidatorPreference) error

The redelegation command allows delegators to instantly switch validators. Once the unbonding period has passed, the redelegation is automatically completed in the EndBlocker. A redelegation object is created every time a redelegation occurs. To prevent "redelegation hopping" where delegatorA can redelegate between many validators over small period of time, redelegations may not occur under the following situation: 1. delegatorA attempts to redelegate to the same validator

  • valA --redelegate--> valB
  • valB --redelegate--> valB (ERROR: Self redelegation is not allowed)

2. delegatorA attempts to redelegate to an immature redelegation validator

  • valA --redelegate--> valB
  • valB --redelegate--> valA (ERROR: Redelegation to ValB is already in progress)

3. delegatorA attempts to redelegate while unbonding is in progress

  • unbond (10osmo) from valA
  • valA --redelegate--> valB (ERROR: new redelegation while unbonding is in progress)

func (Keeper) SetValidatorSetPreferences

func (k Keeper) SetValidatorSetPreferences(ctx sdk.Context, delegator string, validators types.ValidatorSetPreferences)

SetValidatorSetPreferences sets a new valset position for a delegator in modules state.

func (Keeper) UndelegateFromRebalancedValidatorSet

func (k Keeper) UndelegateFromRebalancedValidatorSet(ctx sdk.Context, delegatorAddr string, undelegation sdk.Coin) error

UndelegateFromRebalancedValidatorSet undelegates a specified amount of tokens from a delegator's existing validator set, but takes into consideration the user's existing delegations to the validators in the set. The method first fetches the delegator's validator set preferences, checks their existing delegations, and returns a set with modified weights that consider their existing delegations. If there is no existing delegation, it returns an error. The method then computes the total amount delegated and the amount to undelegate for each validator under this newly calculated valset-ratio set.

If the undelegation amount is greater than the total delegated amount, it returns an error. The validators are then sorted in descending order of VRatio. The method ensures that the largest VRatio is under 1. If it is greater than 1, it returns an error. Finally, the method undelegates the target amount from each validator. If an error occurs during the undelegation process, it is returned.

func (Keeper) UndelegateFromValidatorSet

func (k Keeper) UndelegateFromValidatorSet(ctx sdk.Context, delegatorAddr string, undelegation sdk.Coin) error

UndelegateFromValidatorSet undelegates {coin} amount from the validator set. If the valset does not exist, it undelegates from existing staking position. Ex: user A has staked 10tokens with weight {Val->0.5, ValB->0.3, ValC->0.2} undelegate 6osmo with validator-set {ValA -> 0.5, ValB -> 0.3, ValC -> 0.2} our undelegate logic would attempt to undelegate 3osmo from A, 1.8osmo from B, 1.2osmo from C Truncation ensures we do not undelegate more than the user has staked with the validator set. NOTE: check README.md for more verbose description of the algorithm. TODO: This is currently disabled. Properly implement for vratio > 1 to hit steps 5-7, then re-enable https://github.com/osmosis-labs/osmosis/issues/6686

func (Keeper) ValidateValidatorSetPreference

func (k Keeper) ValidateValidatorSetPreference(ctx sdk.Context, delegator string, preferences []types.ValidatorPreference) (types.ValidatorSetPreferences, error)

ValidateValidatorSetPreference derives given validator set. It validates the list and formats the inputs such as rounding. Errors when the given preference is the same as the existing preference in state. NOTE: this function does not add valset to the state

func (Keeper) WithdrawDelegationRewards

func (k Keeper) WithdrawDelegationRewards(ctx sdk.Context, delegatorAddr string) error

WithdrawDelegationRewards withdraws all the delegation rewards from all validators the user is delegated to, disregarding the val-set. If the valset does not exist, it withdraws from existing staking position. Delegation reward is collected by the validator and in doing so, they can charge commission to the delegators. Rewards are calculated per period, and is updated each time validator delegation changes. For ex: when a delegator receives new delegation the rewards can be calculated by taking (total rewards before new delegation - the total current rewards).

type ValRatio

type ValRatio struct {
	ValAddr       sdk.ValAddress
	Weight        osmomath.Dec
	DelegatedAmt  osmomath.Int
	UndelegateAmt osmomath.Int
	VRatio        osmomath.Dec
}

Directories

Path Synopsis
cli
queryproto
Package queryproto is a reverse proxy.
Package queryproto is a reverse proxy.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL