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

feat: implement effective delegation shares #4

Open
wants to merge 9 commits into
base: release/v0.52.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ go 1.23.1
module github.com/cosmos/cosmos-sdk

require (
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.34.2-20240701160653-fedbb9acfd2f.2 // indirect
buf.build/gen/go/cometbft/cometbft/protocolbuffers/go v1.34.2-20240701160653-fedbb9acfd2f.2
cosmossdk.io/api v0.8.0 // main
cosmossdk.io/collections v0.4.1-0.20241031202146-5b7fc8ae90a7 // main
cosmossdk.io/core v1.0.0-alpha.5
Expand Down
7 changes: 4 additions & 3 deletions types/staking.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,10 @@ func (x BondStatus) String() string {

// DelegationI delegation bond for a delegated proof of stake system
type DelegationI interface {
GetDelegatorAddr() string // delegator string for the bond
GetValidatorAddr() string // validator operator address
GetShares() math.LegacyDec // amount of validator's shares held in this delegation
GetDelegatorAddr() string // delegator string for the bond
GetValidatorAddr() string // validator operator address
GetShares() math.LegacyDec // amount of validator's shares held in this delegation
GetEffectiveShares() math.LegacyDec // amount of validator's shares held in this delegation
}

// ValidatorI expected validator functions
Expand Down
4 changes: 2 additions & 2 deletions x/auth/vesting/exported/exported.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ type VestingAccount interface {
// delegating from a vesting account. It accepts the current block time, the
// delegation amount and balance of all coins whose denomination exists in
// the account's original vesting balance.
TrackDelegation(blockTime time.Time, balance, amount sdk.Coins)
TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins)

// TrackUndelegation performs internal vesting accounting necessary when a
// vesting account performs an undelegation.
TrackUndelegation(amount sdk.Coins)
TrackUndelegation(amount sdk.Coins) (sdk.Coins, sdk.Coins)

GetVestedCoins(blockTime time.Time) sdk.Coins
GetVestingCoins(blockTime time.Time) sdk.Coins
Expand Down
32 changes: 22 additions & 10 deletions x/auth/vesting/types/vesting_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ func (bva BaseVestingAccount) LockedCoinsFromVesting(vestingCoins sdk.Coins) sdk
//
// CONTRACT: The account's coins, delegation coins, vesting coins, and delegated
// vesting coins must be sorted.
func (bva *BaseVestingAccount) TrackDelegation(balance, vestingCoins, amount sdk.Coins) {
func (bva *BaseVestingAccount) TrackDelegation(balance, vestingCoins, amount sdk.Coins) (sdk.Coins, sdk.Coins) {
xCoins, yCoins := sdk.NewCoins(), sdk.NewCoins()

for _, coin := range amount {
baseAmt := balance.AmountOf(coin.Denom)
vestingAmt := vestingCoins.AmountOf(coin.Denom)
Expand All @@ -77,13 +79,17 @@ func (bva *BaseVestingAccount) TrackDelegation(balance, vestingCoins, amount sdk
if !x.IsZero() {
xCoin := sdk.NewCoin(coin.Denom, x)
bva.DelegatedVesting = bva.DelegatedVesting.Add(xCoin)
xCoins = xCoins.Add(xCoin)
}

if !y.IsZero() {
yCoin := sdk.NewCoin(coin.Denom, y)
bva.DelegatedFree = bva.DelegatedFree.Add(yCoin)
yCoins = yCoins.Add(yCoin)
}
}

return xCoins, yCoins
}

// TrackUndelegation tracks an undelegation amount by setting the necessary
Expand All @@ -96,7 +102,9 @@ func (bva *BaseVestingAccount) TrackDelegation(balance, vestingCoins, amount sdk
// the undelegated tokens are non-integral.
//
// CONTRACT: The account's coins and undelegation coins must be sorted.
func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) {
func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) (sdk.Coins, sdk.Coins) {
xCoins, yCoins := sdk.NewCoins(), sdk.NewCoins()

for _, coin := range amount {
// panic if the undelegation amount is zero
if coin.Amount.IsZero() {
Expand All @@ -114,13 +122,17 @@ func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) {
if !x.IsZero() {
xCoin := sdk.NewCoin(coin.Denom, x)
bva.DelegatedFree = bva.DelegatedFree.Sub(xCoin)
xCoins = xCoins.Add(xCoin)
}

if !y.IsZero() {
yCoin := sdk.NewCoin(coin.Denom, y)
bva.DelegatedVesting = bva.DelegatedVesting.Sub(yCoin)
yCoins = yCoins.Add(yCoin)
}
}

return yCoins, xCoins
}

// GetOriginalVesting returns a vesting account's original vesting amount
Expand Down Expand Up @@ -235,8 +247,8 @@ func (cva ContinuousVestingAccount) LockedCoins(blockTime time.Time) sdk.Coins {
// TrackDelegation tracks a desired delegation amount by setting the appropriate
// values for the amount of delegated vesting, delegated free, and reducing the
// overall amount of base coins.
func (cva *ContinuousVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
cva.BaseVestingAccount.TrackDelegation(balance, cva.GetVestingCoins(blockTime), amount)
func (cva *ContinuousVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins) {
return cva.BaseVestingAccount.TrackDelegation(balance, cva.GetVestingCoins(blockTime), amount)
}

// GetStartTime returns the time when vesting starts for a continuous vesting
Expand Down Expand Up @@ -340,8 +352,8 @@ func (pva PeriodicVestingAccount) LockedCoins(blockTime time.Time) sdk.Coins {
// TrackDelegation tracks a desired delegation amount by setting the appropriate
// values for the amount of delegated vesting, delegated free, and reducing the
// overall amount of base coins.
func (pva *PeriodicVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
pva.BaseVestingAccount.TrackDelegation(balance, pva.GetVestingCoins(blockTime), amount)
func (pva *PeriodicVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins) {
return pva.BaseVestingAccount.TrackDelegation(balance, pva.GetVestingCoins(blockTime), amount)
}

// GetStartTime returns the time when vesting starts for a periodic vesting
Expand Down Expand Up @@ -439,8 +451,8 @@ func (dva DelayedVestingAccount) LockedCoins(blockTime time.Time) sdk.Coins {
// TrackDelegation tracks a desired delegation amount by setting the appropriate
// values for the amount of delegated vesting, delegated free, and reducing the
// overall amount of base coins.
func (dva *DelayedVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
dva.BaseVestingAccount.TrackDelegation(balance, dva.GetVestingCoins(blockTime), amount)
func (dva *DelayedVestingAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins) {
return dva.BaseVestingAccount.TrackDelegation(balance, dva.GetVestingCoins(blockTime), amount)
}

// GetStartTime returns zero since a delayed vesting account has no start time.
Expand Down Expand Up @@ -495,8 +507,8 @@ func (plva PermanentLockedAccount) LockedCoins(_ time.Time) sdk.Coins {
// TrackDelegation tracks a desired delegation amount by setting the appropriate
// values for the amount of delegated vesting, delegated free, and reducing the
// overall amount of base coins.
func (plva *PermanentLockedAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) {
plva.BaseVestingAccount.TrackDelegation(balance, plva.OriginalVesting, amount)
func (plva *PermanentLockedAccount) TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins) {
return plva.BaseVestingAccount.TrackDelegation(balance, plva.OriginalVesting, amount)
}

// GetStartTime returns zero since a permanent locked vesting account has no start time.
Expand Down
2 changes: 1 addition & 1 deletion x/bank/keeper/keeper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,8 +435,8 @@ func (suite *KeeperTestSuite) TestSupply_DelegateUndelegateCoins() {
require.Equal(initCoins, keeper.GetAllBalances(ctx, baseAcc.GetAddress()))

suite.mockDelegateCoinsFromAccountToModule(baseAcc, burnerAcc)

require.NoError(keeper.DelegateCoinsFromAccountToModule(ctx, baseAcc.GetAddress(), authtypes.Burner, initCoins))

require.Equal(sdk.NewCoins(), keeper.GetAllBalances(ctx, baseAcc.GetAddress()))
require.Equal(initCoins, keeper.GetAllBalances(ctx, burnerAcc.GetAddress()))

Expand Down
4 changes: 2 additions & 2 deletions x/bank/types/vesting.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ type VestingAccount interface {
// delegating from a vesting account. It accepts the current block time, the
// delegation amount and balance of all coins whose denomination exists in
// the account's original vesting balance.
TrackDelegation(blockTime time.Time, balance, amount sdk.Coins)
TrackDelegation(blockTime time.Time, balance, amount sdk.Coins) (sdk.Coins, sdk.Coins)

// TrackUndelegation performs internal vesting accounting necessary when a
// vesting account performs an undelegation.
TrackUndelegation(amount sdk.Coins)
TrackUndelegation(amount sdk.Coins) (sdk.Coins, sdk.Coins)

GetOriginalVesting() sdk.Coins
GetDelegatedFree() sdk.Coins
Expand Down
2 changes: 1 addition & 1 deletion x/distribution/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func (k Keeper) initializeDelegation(ctx context.Context, val sdk.ValAddress, de
// calculate delegation stake in tokens
// we don't store directly, so multiply delegation shares * (tokens per share)
// note: necessary to truncate so we don't allow withdrawing more rewards than owed
stake := validator.TokensFromSharesTruncated(delegation.GetShares())
stake := validator.TokensFromSharesTruncated(delegation.GetEffectiveShares())
headerinfo := k.HeaderService.HeaderInfo(ctx)
return k.DelegatorStartingInfo.Set(ctx, collections.Join(val, del), types.NewDelegatorStartingInfo(previousPeriod, stake, uint64(headerinfo.Height)))
}
Expand Down
118 changes: 108 additions & 10 deletions x/staking/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,20 @@ func (k Keeper) Delegate(
if err != nil {
return math.LegacyZeroDec(), err
}
bondDenom, err := k.BondDenom(ctx)
if err != nil {
return math.LegacyDec{}, err
}

delVest, delNonVest := sdk.NewCoins(), sdk.NewCoins(sdk.NewCoin(bondDenom, bondAmt))

coins := sdk.NewCoins(sdk.NewCoin(bondDenom, bondAmt))
acc := k.authKeeper.GetAccount(ctx, delAddr)
if acc == nil {
return math.LegacyZeroDec(), errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", delAddr)
}

vacc, isVestingAccount := acc.(types.VestingAccount)

// if subtractAccount is true then we are
// performing a delegation and not a redelegation, thus the source tokens are
Expand All @@ -733,13 +747,14 @@ func (k Keeper) Delegate(
return math.LegacyZeroDec(), fmt.Errorf("invalid validator status: %v", validator.Status)
}

bondDenom, err := k.BondDenom(ctx)
if err != nil {
return math.LegacyDec{}, err
balances := k.bankKeeper.GetAllBalances(ctx, delAddr)

if isVestingAccount {
delVest, delNonVest = vacc.TrackDelegation(k.HeaderService.HeaderInfo(ctx).Time, balances, coins)
}

coins := sdk.NewCoins(sdk.NewCoin(bondDenom, bondAmt))
if err := k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, delAddr, sendName, coins); err != nil {
err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, delAddr, sendName, coins)
if err != nil {
return math.LegacyDec{}, err
}
} else {
Expand All @@ -764,15 +779,43 @@ func (k Keeper) Delegate(
default:
return math.LegacyZeroDec(), fmt.Errorf("unknown token source bond status: %v", tokenSrc)
}

// in case of redelegation we need to track how many vesting, non-vesting tokens are moving to
// another validator, they're needed in calculating effective delegation shares.
if isVestingAccount {
delVest, delNonVest = vacc.TrackUndelegation(coins)
}
}

_, newShares, err = k.AddValidatorTokensAndShares(ctx, validator, bondAmt)
validator, newShares, err = k.AddValidatorTokensAndShares(ctx, validator, bondAmt)
if err != nil {
return newShares, err
return math.LegacyDec{}, err
}

effectiveDelegatorShares, err := k.calculateEffectiveDelegationShares(delVest, delNonVest, validator)
if err != nil {
return math.LegacyDec{}, err
}

if validator.EffectiveDelegatorShares.IsNil() {
validator.EffectiveDelegatorShares = effectiveDelegatorShares
} else {
validator.EffectiveDelegatorShares = validator.EffectiveDelegatorShares.Add(effectiveDelegatorShares)
}

if err = k.SetValidator(ctx, validator); err != nil {
return math.LegacyDec{}, err
}

// Update delegation
delegation.Shares = delegation.Shares.Add(newShares)

if delegation.EffectiveShares.IsNil() {
delegation.EffectiveShares = effectiveDelegatorShares
} else {
delegation.EffectiveShares = delegation.EffectiveShares.Add(effectiveDelegatorShares)
}

if err = k.SetDelegation(ctx, delegation); err != nil {
return newShares, err
}
Expand All @@ -785,6 +828,35 @@ func (k Keeper) Delegate(
return newShares, nil
}

// calculateEffectiveDelegationShares calculates the effective delegation shares
// If the account is vesting, it multiplies the delegated shares by param vestingRewardMultiplier
func (k Keeper) calculateEffectiveDelegationShares(
delegatedVesting, delegatedFree sdk.Coins, validator types.Validator,
) (shares math.LegacyDec, err error) {

// TODO: make defaultVRM as a param
defaultVRM := math.LegacyNewDecWithPrec(3, 1)
effectiveShares := math.LegacyZeroDec()

if len(delegatedFree) > 0 {
nonVestingShares, err := validator.SharesFromTokens(delegatedFree[0].Amount)
if err != nil {
return math.LegacyZeroDec(), err
}
effectiveShares = effectiveShares.Add(nonVestingShares)
}

if len(delegatedVesting) > 0 {
vestingShares, err := validator.SharesFromTokens(delegatedVesting[0].Amount)
if err != nil {
return math.LegacyZeroDec(), err
}
effectiveShares = effectiveShares.Add(vestingShares.Mul(defaultVRM))
}

return effectiveShares, nil
}

// Unbond unbonds a particular delegation and perform associated store operations.
func (k Keeper) Unbond(
ctx context.Context, delAddr sdk.AccAddress, valAddr sdk.ValAddress, shares math.LegacyDec,
Expand Down Expand Up @@ -813,14 +885,42 @@ func (k Keeper) Unbond(
return amount, err
}

unbondAmt := validator.TokensFromShares(shares)

// subtract shares from delegation
delegation.Shares = delegation.Shares.Sub(shares)
effectiveDelegatorShares := shares

delegatorAddress, err := k.authKeeper.AddressCodec().StringToBytes(delegation.DelegatorAddress)
if err != nil {
return amount, err
}

bondDenom, err := k.BondDenom(ctx)
if err != nil {
return amount, err
}

acc := k.authKeeper.GetAccount(ctx, delegatorAddress)
if acc == nil {
return amount, errorsmod.Wrapf(sdkerrors.ErrUnknownAddress, "account %s does not exist", delAddr)
}

vacc, ok := acc.(types.VestingAccount)
if ok {
vest, nonVest := vacc.TrackUndelegation(sdk.NewCoins(sdk.NewCoin(bondDenom, unbondAmt.TruncateInt())))

effectiveDelegatorShares, err = k.calculateEffectiveDelegationShares(vest, nonVest, validator)
if err != nil {
return amount, err
}
}

// safesub
validator.EffectiveDelegatorShares = validator.EffectiveDelegatorShares.Sub(effectiveDelegatorShares)
// safesub
delegation.EffectiveShares = delegation.EffectiveShares.Sub(effectiveDelegatorShares)

valbz, err := k.ValidatorAddressCodec().StringToBytes(validator.GetOperator())
if err != nil {
return amount, err
Expand Down Expand Up @@ -1006,9 +1106,7 @@ func (k Keeper) CompleteUnbonding(ctx context.Context, delAddr sdk.AccAddress, v
// track undelegation only when remaining or truncated shares are non-zero
if !entry.Balance.IsZero() {
amt := sdk.NewCoin(bondDenom, entry.Balance)
if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(
ctx, types.NotBondedPoolName, delegatorAddress, sdk.NewCoins(amt),
); err != nil {
if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(ctx, types.NotBondedPoolName, delegatorAddress, sdk.NewCoins(amt)); err != nil {
return nil, err
}

Expand Down
2 changes: 2 additions & 0 deletions x/staking/keeper/grpc_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package keeper
import (
"context"
"errors"
"fmt"
"strings"

"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -81,6 +82,7 @@ func (k Querier) Validators(ctx context.Context, req *types.QueryValidatorsReque
validatorInfoList = append(validatorInfoList, valInfo)
}

fmt.Printf("vals.Validators: %+v\n", vals.Validators)
return &types.QueryValidatorsResponse{
Validators: vals.Validators,
ValidatorInfo: validatorInfoList,
Expand Down
1 change: 1 addition & 0 deletions x/staking/keeper/unbonding.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,7 @@ func (k Keeper) unbondingDelegationEntryCanComplete(ctx context.Context, id uint
// track undelegation only when remaining or truncated shares are non-zero
if !ubd.Entries[i].Balance.IsZero() {
amt := sdk.NewCoin(bondDenom, ubd.Entries[i].Balance)

if err := k.bankKeeper.UndelegateCoinsFromModuleToAccount(
ctx, types.NotBondedPoolName, delegatorAddress, sdk.NewCoins(amt),
); err != nil {
Expand Down
13 changes: 13 additions & 0 deletions x/staking/proto/cosmos/staking/v1beta1/staking.proto
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ message Validator {

// list of unbonding ids, each uniquely identifying an unbonding of this validator
repeated uint64 unbonding_ids = 13;

// effective_delegator_shares defines total effective shares issued to a validator's delegators.
string effective_delegator_shares = 14 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}

// BondStatus is the status of a validator.
Expand Down Expand Up @@ -207,6 +214,12 @@ message Delegation {
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
// effective_shares defines total effective shares issued to a validator's delegators.
string effective_shares = 4 [
(cosmos_proto.scalar) = "cosmos.Dec",
(gogoproto.customtype) = "cosmossdk.io/math.LegacyDec",
(gogoproto.nullable) = false
];
}

// UnbondingDelegation stores all of a single delegator's unbonding bonds
Expand Down
Loading
Loading