Skip to content

Commit

Permalink
Merge PR #3717: Allow the undelegation of more coins than were delega…
Browse files Browse the repository at this point in the history
…ted; More validity checks.
  • Loading branch information
jaekwon authored and cwgoes committed Feb 27, 2019
1 parent 7e08b62 commit 10bd98e
Show file tree
Hide file tree
Showing 20 changed files with 188 additions and 46 deletions.
6 changes: 6 additions & 0 deletions PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ CLI flag.
where validator is unexpectedly slashed throwing off test calculations
* [\#3411] Include the `RequestInitChain.Time` in the block header init during
`InitChain`.
* [\#3717] Update the vesting specification and implementation to cap deduction from
`DelegatedVesting` by at most `DelegatedVesting`. This accounts for the case where
the undelegation amount may exceed the original delegation amount due to
truncation of undelegation tokens.
* [\#3717] Ignore unknown proposers in allocating rewards for proposers, in case
unbonding period was just 1 block and proposer was already deleted.
* [\#3726] Cap(clip) reward to remaining coins in AllocateTokens.

### Tendermint
2 changes: 1 addition & 1 deletion cmd/gaia/app/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func appStateRandomizedFn(r *rand.Rand, accs []simulation.Account, genesisTimest
Pool: staking.InitialPool(),
Params: staking.Params{
UnbondingTime: time.Duration(randIntBetween(r, 60, 60*60*24*3*2)) * time.Second,
MaxValidators: uint16(r.Intn(250)),
MaxValidators: uint16(r.Intn(250) + 1),
BondDenom: sdk.DefaultBondDenom,
},
}
Expand Down
11 changes: 9 additions & 2 deletions docs/spec/auth/05_vesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,12 @@ func DelegateCoins(t Time, from Account, amount Coins) {
### Undelegating

For a vesting account attempting to undelegate `D` coins, the following is performed:
NOTE: `DV < D` and `(DV + DF) < D` may be possible due to quirks in the rounding of
delegation/undelegation logic.

1. Verify `(DV + DF) >= D > 0` (this is simply a sanity check)
1. Verify `D > 0`
2. Compute `X := min(DF, D)` (portion of `D` that should become free, prioritizing free coins)
3. Compute `Y := D - X` (portion of `D` that should remain vesting)
3. Compute `Y := min(DV, D - X)` (portion of `D` that should remain vesting)
4. Set `DF -= X`
5. Set `DV -= Y`
6. Set `BC += D`
Expand All @@ -274,6 +276,11 @@ func (cva ContinuousVestingAccount) TrackUndelegation(amount Coins) {
with an excess `DV` amount, even after all its coins have vested. This is because
undelegating free coins are prioritized.

**Note**: The undelegation (bond refund) amount may exceed the delegated
vesting (bond) amount due to the way undelegation truncates the bond refund,
which can increase the validator's exchange rate (tokens/shares) slightly if the
undelegated tokens are non-integral.

#### Keepers/Handlers

```go
Expand Down
2 changes: 0 additions & 2 deletions types/coin.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,6 @@ func (coins Coins) AmountOf(denom string) Int {

// IsAllPositive returns true if there is at least one coin and all currencies
// have a positive value.
//
// TODO: Remove once unsigned integers are used.
func (coins Coins) IsAllPositive() bool {
if len(coins) == 0 {
return false
Expand Down
10 changes: 8 additions & 2 deletions x/auth/account.go
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ func (bva *BaseVestingAccount) trackDelegation(vestingCoins, amount sdk.Coins) {
// by which amount the base coins need to increase. The resulting base coins are
// returned.
//
// NOTE: The undelegation (bond refund) amount may exceed the delegated
// vesting (bond) amount due to the way undelegation truncates the bond refund,
// which can increase the validator's exchange rate (tokens/shares) slightly if
// the undelegated tokens are non-integral.
//
// CONTRACT: The account's coins and undelegation coins must be sorted.
func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) {
for _, coin := range amount {
Expand All @@ -295,12 +300,13 @@ func (bva *BaseVestingAccount) TrackUndelegation(amount sdk.Coins) {
panic("undelegation attempt with zero coins")
}
delegatedFree := bva.DelegatedFree.AmountOf(coin.Denom)
delegatedVesting := bva.DelegatedVesting.AmountOf(coin.Denom)

// compute x and y per the specification, where:
// X := min(DF, D)
// Y := D - X
// Y := min(DV, D - X)
x := sdk.MinInt(delegatedFree, coin.Amount)
y := coin.Amount.Sub(x)
y := sdk.MinInt(delegatedVesting, coin.Amount.Sub(x))

if !x.IsZero() {
xCoin := sdk.NewCoin(coin.Denom, x)
Expand Down
74 changes: 63 additions & 11 deletions x/bank/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ func NewBaseKeeper(ak auth.AccountKeeper,
}

// SetCoins sets the coins at the addr.
func (keeper BaseKeeper) SetCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) sdk.Error {
func (keeper BaseKeeper) SetCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) sdk.Error {

if !amt.IsValid() {
return sdk.ErrInvalidCoins(amt.String())
}
return setCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -56,6 +62,9 @@ func (keeper BaseKeeper) SubtractCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}
return subtractCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -64,6 +73,9 @@ func (keeper BaseKeeper) AddCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}
return addCoins(ctx, keeper.ak, addr, amt)
}

Expand All @@ -78,14 +90,27 @@ func (keeper BaseKeeper) InputOutputCoins(
// DelegateCoins performs delegation by deducting amt coins from an account with
// address addr. For vesting accounts, delegations amounts are tracked for both
// vesting and vested coins.
func (keeper BaseKeeper) DelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
func (keeper BaseKeeper) DelegateCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return delegateCoins(ctx, keeper.ak, addr, amt)
}

// UndelegateCoins performs undelegation by crediting amt coins to an account with
// address addr. For vesting accounts, undelegation amounts are tracked for both
// vesting and vested coins.
func (keeper BaseKeeper) UndelegateCoins(ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
// If any of the undelegation amounts are negative, an error is returned.
func (keeper BaseKeeper) UndelegateCoins(
ctx sdk.Context, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return undelegateCoins(ctx, keeper.ak, addr, amt)
}

Expand Down Expand Up @@ -126,6 +151,10 @@ func NewBaseSendKeeper(ak auth.AccountKeeper,
func (keeper BaseSendKeeper) SendCoins(
ctx sdk.Context, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
return sendCoins(ctx, keeper.ak, fromAddr, toAddr, amt)
}

Expand Down Expand Up @@ -188,6 +217,9 @@ func getCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress) sdk.C
}

func setCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) sdk.Error {
if !amt.IsValid() {
return sdk.ErrInvalidCoins(amt.String())
}
acc := am.GetAccount(ctx, addr)
if acc == nil {
acc = am.NewAccountWithAddress(ctx, addr)
Expand Down Expand Up @@ -218,6 +250,11 @@ func setAccount(ctx sdk.Context, ak auth.AccountKeeper, acc auth.Account) {
//
// CONTRACT: If the account is a vesting account, the amount has to be spendable.
func subtractCoins(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}

oldCoins, spendableCoins := sdk.Coins{}, sdk.Coins{}

acc := getAccount(ctx, ak, addr)
Expand All @@ -244,6 +281,11 @@ func subtractCoins(ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress,

// AddCoins adds amt to the coins at the addr.
func addCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins) (sdk.Coins, sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, nil, sdk.ErrInvalidCoins(amt.String())
}

oldCoins := getCoins(ctx, am, addr)
newCoins := oldCoins.Add(amt)

Expand All @@ -260,9 +302,9 @@ func addCoins(ctx sdk.Context, am auth.AccountKeeper, addr sdk.AccAddress, amt s
}

// SendCoins moves coins from one account to another
// Returns ErrInvalidCoins if amt is invalid.
func sendCoins(ctx sdk.Context, am auth.AccountKeeper, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) {
// Safety check ensuring that when sending coins the keeper must maintain the
// supply invariant.
if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}
Expand All @@ -284,7 +326,7 @@ func sendCoins(ctx sdk.Context, am auth.AccountKeeper, fromAddr sdk.AccAddress,
// NOTE: Make sure to revert state changes from tx on error
func inputOutputCoins(ctx sdk.Context, am auth.AccountKeeper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) {
// Safety check ensuring that when sending coins the keeper must maintain the
// supply invariant.
// Check supply invariant and validity of Coins.
if err := ValidateInputsOutputs(inputs, outputs); err != nil {
return nil, err
}
Expand Down Expand Up @@ -314,6 +356,10 @@ func delegateCoins(
ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}

acc := getAccount(ctx, ak, addr)
if acc == nil {
return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr))
Expand Down Expand Up @@ -344,6 +390,10 @@ func undelegateCoins(
ctx sdk.Context, ak auth.AccountKeeper, addr sdk.AccAddress, amt sdk.Coins,
) (sdk.Tags, sdk.Error) {

if !amt.IsValid() {
return nil, sdk.ErrInvalidCoins(amt.String())
}

acc := getAccount(ctx, ak, addr)
if acc == nil {
return nil, sdk.ErrUnknownAddress(fmt.Sprintf("account %s does not exist", addr))
Expand All @@ -361,22 +411,24 @@ func undelegateCoins(
), nil
}

func trackDelegation(acc auth.Account, blockTime time.Time, amount sdk.Coins) error {
// CONTRACT: assumes that amt is valid.
func trackDelegation(acc auth.Account, blockTime time.Time, amt sdk.Coins) error {
vacc, ok := acc.(auth.VestingAccount)
if ok {
vacc.TrackDelegation(blockTime, amount)
vacc.TrackDelegation(blockTime, amt)
return nil
}

return acc.SetCoins(acc.GetCoins().Sub(amount))
return acc.SetCoins(acc.GetCoins().Sub(amt))
}

func trackUndelegation(acc auth.Account, amount sdk.Coins) error {
// CONTRACT: assumes that amt is valid.
func trackUndelegation(acc auth.Account, amt sdk.Coins) error {
vacc, ok := acc.(auth.VestingAccount)
if ok {
vacc.TrackUndelegation(amount)
vacc.TrackUndelegation(amt)
return nil
}

return acc.SetCoins(acc.GetCoins().Add(amount))
return acc.SetCoins(acc.GetCoins().Add(amt))
}
3 changes: 3 additions & 0 deletions x/bank/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ func (msg MsgSend) ValidateBasic() sdk.Error {
if msg.ToAddress.Empty() {
return sdk.ErrInvalidAddress("missing recipient address")
}
if !msg.Amount.IsValid() {
return sdk.ErrInvalidCoins("send amount is invalid: " + msg.Amount.String())
}
if !msg.Amount.IsAllPositive() {
return sdk.ErrInsufficientCoins("send amount must be positive")
}
Expand Down
18 changes: 16 additions & 2 deletions x/distribution/keeper/allocation.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package keeper

import (
"fmt"

abci "github.com/tendermint/tendermint/abci/types"

sdk "github.com/cosmos/cosmos-sdk/types"
)

// allocate fees handles distribution of the collected fees
func (k Keeper) AllocateTokens(ctx sdk.Context, sumPrecommitPower, totalPower int64, proposer sdk.ConsAddress, votes []abci.VoteInfo) {
logger := ctx.Logger().With("module", "x/distribution")

// fetch collected fees & fee pool
feesCollectedInt := k.feeCollectionKeeper.GetCollectedFees(ctx)
Expand Down Expand Up @@ -35,9 +38,20 @@ func (k Keeper) AllocateTokens(ctx sdk.Context, sumPrecommitPower, totalPower in
proposerReward := feesCollected.MulDecTruncate(proposerMultiplier)

// pay proposer
remaining := feesCollected
proposerValidator := k.stakingKeeper.ValidatorByConsAddr(ctx, proposer)
k.AllocateTokensToValidator(ctx, proposerValidator, proposerReward)
remaining := feesCollected.Sub(proposerReward)
if proposerValidator != nil {
k.AllocateTokensToValidator(ctx, proposerValidator, proposerReward)
remaining = feesCollected.Sub(proposerReward)
} else {
// proposer can be unknown if say, the unbonding period is 1 block, so
// e.g. a validator undelegates at block X, it's removed entirely by
// block X+1's endblock, then X+2 we need to refer to the previous
// proposer for X+1, but we've forgotten about them.
logger.Error(fmt.Sprintf(
"WARNING: Attempt to allocate proposer rewards to unknown proposer %s. This should happen only if the proposer unbonded completely within a single block, which generally should not happen except in exceptional circumstances (or fuzz testing). We recommend you investigate immediately.",
proposer.String()))
}

// calculate fraction allocated to validators
communityTax := k.GetCommunityTax(ctx)
Expand Down
8 changes: 5 additions & 3 deletions x/distribution/keeper/delegation.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@ func (k Keeper) withdrawDelegationRewards(ctx sdk.Context, val sdk.Validator, de
k.SetFeePool(ctx, feePool)

// add coins to user account
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr())
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
if !coins.IsZero() {
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, del.GetDelegatorAddr())
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
}
}

// remove delegator starting info
Expand Down
10 changes: 6 additions & 4 deletions x/distribution/keeper/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ func (h Hooks) AfterValidatorRemoved(ctx sdk.Context, _ sdk.ConsAddress, valAddr
h.k.SetOutstandingRewards(ctx, outstanding.Sub(commission))

// add to validator account
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr)
if !coins.IsZero() {
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := h.k.GetDelegatorWithdrawAddr(ctx, accAddr)

if _, _, err := h.k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
panic(err)
if _, _, err := h.k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
panic(err)
}
}
}
// remove commission record
Expand Down
10 changes: 6 additions & 4 deletions x/distribution/keeper/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,13 @@ func (k Keeper) WithdrawValidatorCommission(ctx sdk.Context, valAddr sdk.ValAddr
outstanding := k.GetOutstandingRewards(ctx)
k.SetOutstandingRewards(ctx, outstanding.Sub(sdk.NewDecCoins(coins)))

accAddr := sdk.AccAddress(valAddr)
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr)
if !coins.IsZero() {
accAddr := sdk.AccAddress(valAddr)
withdrawAddr := k.GetDelegatorWithdrawAddr(ctx, accAddr)

if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
if _, _, err := k.bankKeeper.AddCoins(ctx, withdrawAddr, coins); err != nil {
return err
}
}

return nil
Expand Down
2 changes: 1 addition & 1 deletion x/gov/simulation/msgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func randomDeposit(r *rand.Rand) sdk.Coins {
// Pick a random proposal ID
func randomProposalID(r *rand.Rand, k gov.Keeper, ctx sdk.Context) (proposalID uint64, ok bool) {
lastProposalID := k.GetLastProposalID(ctx)
if lastProposalID < 1 {
if lastProposalID < 1 || lastProposalID == (2<<63-1) {
return 0, false
}
proposalID = uint64(r.Intn(1+int(lastProposalID)) - 1)
Expand Down
1 change: 1 addition & 0 deletions x/ibc/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func handleIBCReceiveMsg(ctx sdk.Context, ibcm Mapper, ck BankKeeper, msg MsgIBC
return ErrInvalidSequence(ibcm.codespace).Result()
}

// XXX Check that packet.Coins is valid and positive (nonzero)
_, _, err := ck.AddCoins(ctx, packet.DestAddr, packet.Coins)
if err != nil {
return err.Result()
Expand Down
Loading

0 comments on commit 10bd98e

Please sign in to comment.