Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: Fix valset pref unbonding bugs #5967

Merged
merged 35 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
fe7290f
Add helper to get existing delegations along with valset pref
ValarDragon Aug 6, 2023
fcdc961
Lay out more of the logic nuance
ValarDragon Aug 6, 2023
58792cc
Add pseudo-code needed
ValarDragon Aug 6, 2023
2f8dea8
algorithm v1
stackman27 Aug 7, 2023
f4c0dab
added algorithm docs
stackman27 Aug 7, 2023
144f0d1
fixed all test
stackman27 Aug 8, 2023
41cd2f6
removed unwanted files
stackman27 Aug 8, 2023
7403e06
remove more code
stackman27 Aug 8, 2023
fbd4ba4
added more tests
stackman27 Aug 8, 2023
2f5caca
update changelog
devbot-wizard Aug 8, 2023
d3cec49
Merge branch 'main' into dev/valset-pref-unbond-issues
stackman27 Aug 8, 2023
90e9fb2
added test and addressed feedback
stackman27 Aug 8, 2023
b317a0e
Update x/valset-pref/validator_set.go
czarcas7ic Aug 9, 2023
b1ece1b
Update x/valset-pref/validator_set.go
czarcas7ic Aug 9, 2023
53a14d9
Update x/valset-pref/README.md
czarcas7ic Aug 9, 2023
5d54956
Merge branch 'main' into dev/valset-pref-unbond-issues
czarcas7ic Aug 11, 2023
11bc176
Minor cleanup
ValarDragon Aug 13, 2023
3799111
re-use validator gets
ValarDragon Aug 13, 2023
dcfc806
Refactor
ValarDragon Aug 14, 2023
d9891dd
Highlight bug
ValarDragon Aug 14, 2023
5ac4023
fixed the issue
stackman27 Aug 22, 2023
25cdc5f
added comments
stackman27 Aug 22, 2023
20f7c29
Merge branch 'main' into dev/valset-pref-unbond-issues
stackman27 Aug 22, 2023
e27f258
update changelog
devbot-wizard Aug 8, 2023
54b4dec
fixed the issue
stackman27 Aug 22, 2023
77a9655
fixed go test
stackman27 Aug 22, 2023
324a281
fixed test
stackman27 Aug 22, 2023
365b721
Merge branch 'main' into dev/valset-pref-unbond-issues
p0mvn Sep 27, 2023
31e9008
lint
p0mvn Sep 28, 2023
1c48875
Merge branch 'main' into dev/valset-pref-unbond-issues
p0mvn Oct 3, 2023
9e98292
Finish ValSet Pref Unbonding (#6630)
mattverse Oct 5, 2023
eedd243
Merge branch 'main' into dev/valset-pref-unbond-issues
czarcas7ic Oct 5, 2023
ce280e8
feat: unbond with rebalanced val set weights (#6685)
czarcas7ic Oct 13, 2023
30af740
Merge branch 'main' into dev/valset-pref-unbond-issues
czarcas7ic Oct 13, 2023
246d95e
Update x/valset-pref/validator_set.go
ValarDragon Oct 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* [#6309](https://github.com/osmosis-labs/osmosis/pull/6309) Add Cosmwasm Pool Queries to Stargate Query
* [#6493](https://github.com/osmosis-labs/osmosis/pull/6493) Add PoolManager Params query to Stargate Whitelist
* [#6421](https://github.com/osmosis-labs/osmosis/pull/6421) Moves ValidatePermissionlessPoolCreationEnabled out of poolmanager module
* [#5967](https://github.com/osmosis-labs/osmosis/pull/5967) fix ValSet undelegate API out of sync with existing staking
* [#6627](https://github.com/osmosis-labs/osmosis/pull/6627) Limit pow iterations in osmomath.
* [#6586](https://github.com/osmosis-labs/osmosis/pull/6586) add auth.moduleaccounts to the stargate whitelist
* [#6680](https://github.com/osmosis-labs/osmosis/pull/6680) Add Taker Fee query and add it to stargate whitelist
Expand Down
28 changes: 28 additions & 0 deletions proto/osmosis/valset-pref/v1beta1/tx.proto
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ service Msg {
rpc UndelegateFromValidatorSet(MsgUndelegateFromValidatorSet)
returns (MsgUndelegateFromValidatorSetResponse);

// UndelegateFromRebalancedValidatorSet undelegates the proivded amount from
// the validator set, but takes into consideration the current delegations
// to the user's validator set to determine the weights assigned to each.
rpc UndelegateFromRebalancedValidatorSet(
MsgUndelegateFromRebalancedValidatorSet)
returns (MsgUndelegateFromRebalancedValidatorSetResponse);

// RedelegateValidatorSet takes the existing validator set and redelegates to
// a new set.
rpc RedelegateValidatorSet(MsgRedelegateValidatorSet)
Expand Down Expand Up @@ -98,6 +105,27 @@ message MsgUndelegateFromValidatorSet {

message MsgUndelegateFromValidatorSetResponse {}

message MsgUndelegateFromRebalancedValidatorSet {
option (amino.name) = "osmosis/MsgUndelegateFromRebalancedValidatorSet";

// delegator is the user who is trying to undelegate.
string delegator = 1 [ (gogoproto.moretags) = "yaml:\"delegator\"" ];

// the amount the user wants to undelegate
// For ex: Undelegate 50 osmo with validator-set {ValA -> 0.5, ValB -> 0.5}
// Our undelegate logic would first check the current delegation balance.
// If the user has 90 osmo delegated to ValA and 10 osmo delegated to ValB,
// the rebalanced validator set would be {ValA -> 0.9, ValB -> 0.1}
// So now the 45 osmo would be undelegated from ValA and 5 osmo would be
// undelegated from ValB.
cosmos.base.v1beta1.Coin coin = 2 [
(gogoproto.nullable) = false,
(gogoproto.castrepeated) = "github.com/cosmos/cosmos-sdk/types.Coin"
];
}

message MsgUndelegateFromRebalancedValidatorSetResponse {}

message MsgRedelegateValidatorSet {
option (amino.name) = "osmosis/MsgRedelegateValidatorSet";

Expand Down
57 changes: 48 additions & 9 deletions x/valset-pref/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,54 @@ How does this module work?
- 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.

## Calculations

Staking Calculation

- 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.

UnStaking Calculation
## Logic

### Delegate to validator set

This is pretty straight-forward, theres 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
Expand Down
27 changes: 21 additions & 6 deletions x/valset-pref/client/cli/tx.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ func GetTxCmd() *cobra.Command {
txCmd := osmocli.TxIndexCmd(types.ModuleName)
osmocli.AddTxCmd(txCmd, NewSetValSetCmd)
osmocli.AddTxCmd(txCmd, NewDelValSetCmd)
osmocli.AddTxCmd(txCmd, NewUnDelValSetCmd)
// TODO: Uncomment when undelegate is implemented
// https://github.com/osmosis-labs/osmosis/issues/6686
//osmocli.AddTxCmd(txCmd, NewUnDelValSetCmd)
osmocli.AddTxCmd(txCmd, NewUndelRebalancedValSetCmd)
osmocli.AddTxCmd(txCmd, NewReDelValSetCmd)
osmocli.AddTxCmd(txCmd, NewWithRewValSetCmd)
return txCmd
Expand All @@ -44,13 +47,25 @@ func NewDelValSetCmd() (*osmocli.TxCliDesc, *types.MsgDelegateToValidatorSet) {
}, &types.MsgDelegateToValidatorSet{}
}

func NewUnDelValSetCmd() (*osmocli.TxCliDesc, *types.MsgUndelegateFromValidatorSet) {
// TODO: Uncomment when undelegate is implemented
// https://github.com/osmosis-labs/osmosis/issues/6686
// func NewUnDelValSetCmd() (*osmocli.TxCliDesc, *types.MsgUndelegateFromValidatorSet) {
// return &osmocli.TxCliDesc{
// Use: "undelegate-valset",
// Short: "UnDelegate tokens from existing valset using delegatorAddress and tokenAmount.",
// Example: "osmosisd tx valset-pref undelegate-valset osmo1... 100stake",
// NumArgs: 2,
// }, &types.MsgUndelegateFromValidatorSet{}
// }

func NewUndelRebalancedValSetCmd() (*osmocli.TxCliDesc, *types.MsgUndelegateFromRebalancedValidatorSet) {
return &osmocli.TxCliDesc{
Use: "undelegate-valset",
Short: "UnDelegate tokens from existing valset using delegatorAddress and tokenAmount.",
Example: "osmosisd tx valset-pref undelegate-valset osmo1... 100stake",
Use: "undelegate-rebalanced-valset",
Short: "Undelegate tokens from rebalanced valset using delegatorAddress and tokenAmount.",
Long: "Undelegates from an existing valset, but calculates the valset weights based on current user delegations.",
Example: "osmosisd tx valset-pref undelegate-rebalanced-valset osmo1... 100stake",
NumArgs: 2,
}, &types.MsgUndelegateFromValidatorSet{}
}, &types.MsgUndelegateFromRebalancedValidatorSet{}
}

func NewReDelValSetCmd() (*osmocli.TxCliDesc, *types.MsgRedelegateValidatorSet) {
Expand Down
11 changes: 11 additions & 0 deletions x/valset-pref/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package keeper

import (
sdk "github.com/cosmos/cosmos-sdk/types"
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

"github.com/osmosis-labs/osmosis/osmomath"
lockuptypes "github.com/osmosis-labs/osmosis/v20/x/lockup/types"
"github.com/osmosis-labs/osmosis/v20/x/valset-pref/types"
)

type (
Expand All @@ -14,3 +16,12 @@ type (
func (k Keeper) ValidateLockForForceUnlock(ctx sdk.Context, lockID uint64, delegatorAddr string) (*lockuptypes.PeriodLock, osmomath.Int, error) {
return k.validateLockForForceUnlock(ctx, lockID, delegatorAddr)
}

func (k Keeper) GetValsetRatios(ctx sdk.Context, delegator sdk.AccAddress,
prefs []types.ValidatorPreference, undelegateAmt osmomath.Int) ([]ValRatio, map[string]stakingtypes.Validator, osmomath.Dec, error) {
return k.getValsetRatios(ctx, delegator, prefs, undelegateAmt)
}

func FormatToValPrefArr(delegations []stakingtypes.Delegation) []types.ValidatorPreference {
return formatToValPrefArr(delegations)
}
70 changes: 46 additions & 24 deletions x/valset-pref/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import (

"github.com/tendermint/tendermint/libs/log"

stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types"

sdk "github.com/cosmos/cosmos-sdk/types"
paramtypes "github.com/cosmos/cosmos-sdk/x/params/types"

"github.com/osmosis-labs/osmosis/osmomath"
"github.com/osmosis-labs/osmosis/v20/x/valset-pref/types"
)

Expand Down Expand Up @@ -49,42 +50,63 @@ func (k Keeper) GetDelegationPreferences(ctx sdk.Context, delegator string) (typ
if err != nil {
return types.ValidatorSetPreferences{}, err
}
existingDelsValSetFormatted, err := k.GetExistingStakingDelegations(ctx, delAddr)
if err != nil {
return types.ValidatorSetPreferences{}, err
existingDelegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delAddr, math.MaxUint16)
if len(existingDelegations) == 0 {
return types.ValidatorSetPreferences{}, types.ErrNoDelegation
}

return types.ValidatorSetPreferences{Preferences: existingDelsValSetFormatted}, nil
return types.ValidatorSetPreferences{Preferences: formatToValPrefArr(existingDelegations)}, nil
}

return valSet, nil
}

// GetExistingStakingDelegations returns the existing delegation that's not valset.
// This function also formats the output into ValidatorSetPreference struct {valAddr, weight}.
// The weight is calculated based on (valDelegation / totalDelegations) for each validator.
// This method erros when given address does not have any existing delegations.
func (k Keeper) GetExistingStakingDelegations(ctx sdk.Context, delAddr sdk.AccAddress) ([]types.ValidatorPreference, error) {
var existingDelsValSetFormatted []types.ValidatorPreference
// 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 (k Keeper) GetValSetPreferencesWithDelegations(ctx sdk.Context, delegator string) (types.ValidatorSetPreferences, error) {
delAddr, err := sdk.AccAddressFromBech32(delegator)
if err != nil {
return types.ValidatorSetPreferences{}, err
}

valSet, exists := k.GetValidatorSetPreference(ctx, delegator)
existingDelegations := k.stakingKeeper.GetDelegatorDelegations(ctx, delAddr, math.MaxUint16)
if len(existingDelegations) == 0 {
return nil, types.ErrNoDelegation

// No existing delegations for a delegator when valSet does not exist
if !exists && len(existingDelegations) == 0 {
return types.ValidatorSetPreferences{}, fmt.Errorf("No Existing delegation to unbond from")
}

existingTotalShares := osmomath.NewDec(0)
// calculate total shares that currently exists
for _, existingDelegation := range existingDelegations {
existingTotalShares = existingTotalShares.Add(existingDelegation.Shares)
// Returning existing valSet when there are no existing delegations
if exists && len(existingDelegations) == 0 {
return valSet, nil
}

// for each delegation format it in types.ValidatorSetPreferences format
for _, existingDelegation := range existingDelegations {
existingDelsValSetFormatted = append(existingDelsValSetFormatted, types.ValidatorPreference{
ValOperAddress: existingDelegation.ValidatorAddress,
Weight: existingDelegation.Shares.Quo(existingTotalShares),
})
// when existing delegation exists, have it based upon the existing delegation
// regardless of the delegator having valset pref or not
return types.ValidatorSetPreferences{Preferences: formatToValPrefArr(existingDelegations)}, nil
}

// formatToValPrefArr iterates over given delegations array, formats it into ValidatorPreference array.
// Used to calculate weights for the each delegation towards validator.
// CONTRACT: This method assumes no duplicated ValOperAddress exists in the given delegation.
func formatToValPrefArr(delegations []stakingtypes.Delegation) []types.ValidatorPreference {
totalShares := sdk.NewDec(0)
for _, existingDelegation := range delegations {
totalShares = totalShares.Add(existingDelegation.Shares)
Comment on lines +100 to +101
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait... is this the code for valset preference? I think this is fundamentally wrong. This needs to be based on the token amount, not the number of shares

}

return existingDelsValSetFormatted, nil
valPrefs := make([]types.ValidatorPreference, len(delegations))
for i, delegation := range delegations {
valPrefs[i] = types.ValidatorPreference{
ValOperAddress: delegation.ValidatorAddress,
Weight: delegation.Shares.Quo(totalShares),
}
}
return valPrefs
}
Loading