Cosmos-sdk: Balance is inaccurate when delagating to a validator which had been jailed

Created on 6 Nov 2019  Â·  10Comments  Â·  Source: cosmos/cosmos-sdk

Summary of Bug

I delegated 9 tokens to a validator which had been jailed, and then queried delegations via rest api '/staking/validators/{validatorAddr}/delegations'. I found there were only 8 tokens left in balance.

$curl http://localhost:1317/staking/validators/$val_addr/delegations
{
...
    "shares": "9.001800270036004500",
    "balance": "8"
...
}

$curl http://localhost:1317/staking/validators/$val_addr
{
...
  "tokens": "1001099900000009",
  "delegator_shares": "1001300150017010.902010293038504261",
...
}

Version

v0.37.3

Steps to Reproduce

Delegate to a validator which had been jailed and query the delegation. It does not occur every time.

https://github.com/cosmos/cosmos-sdk/blob/master/x/staking/types/validator.go#L368

func (v Validator) TokensFromShares(shares sdk.Dec) sdk.Dec {
    return (shares.MulInt(v.Tokens)).Quo(v.DelegatorShares)
}

Only 18 decimal places are left when calculating shares. So the value of shares*tokens is a little less than the actual value. In this case, shares * tokens / delegator_shares = 9.001800270036004500 * 1001099900000009 / 1001300150017010.902010293038504261 = 8.99999999... (round down to 8)


For Admin Use

  • [ ] Not duplicate issue
  • [ ] Appropriate labels applied
  • [ ] Appropriate contributors tagged
  • [ ] Contributor assigned/self-assigned
needs-more-info

Most helpful comment

Let me try to explain.

The problem is, when a validator is slashed, for example by 0.01%. Future delegations on the validator will take a fraction slightly smaller than 1 as validator's token/shares. Thus when a new delegator delegates 99 tokens to the validator, it will get 99 *10000/9999 shares. However, it will be stored as 99.009900990099009900 due to precision limit. Then, when using this truncated shares to compute the balance of the delegation(using function TokensFromShares), it will get 98 tokens, as the results of querying the delegation.

When the delegator wants to undelegate, it's okay that he send a tx with 99 tokens to be undelegate. The current implementation will do the above computation again to get the truncated shares, and calling TokensFromShares with the truncated shares will get 98 as the result. And when the undelegation gets mature, the delegator gets 98 tokens back, as what we mean "1 lost".

The remaining 1 is left in the validator's total token, and the last person who undelegates from the validator can get his delegations back together with the additional "1 lost" token. Because when the undelegation causes validator's remaining shares to be zero, the tokens get back to his account will be validator's total token left.

All 10 comments

How much was the validator slashed by?

"slash_fraction_downtime": "0.000100000000000000"

I find the way to reproduce.

  1. The validator delegates 1,000,000,000,000 tokens to self.
  2. Make it jailed, and slashed by 0.01% (100,000,000).
  3. Self delegate 100,000,000 and send unjail tx. [Shares: 1000100010001.000100010001000100]
  4. Make it jailed again, and slashed by 0.01% (100,000,000).
  5. Delegate 9 tokens by other account. Query the delegations.
$ curl localhost:1317/staking/validators/$val_addr
{
  ...
  "tokens": "999900000009",
  "delegator_shares": "1000100010010.001900280037004600",
}

$ curl localhost:1317/staking/validators/$val_addr/delegations
{"height":"588","result":[
  {
    ...
    "shares": "1000100010001.000100010001000100",
    "balance": "999900000000"
  },
  {
    ...
    "shares": "9.001800270036004500",
    "balance": "8"
  }
]}

What are you claiming should be the balance of the account that delegated 9 tokens? 9? If so, the account has the remaining clipped.

My question is I delegated 9 tokens, and I can also unbond 9 tokens, but only 8 tokens left in balance.
If my delegation is before the validator jailed and slashed by 0.01%, it is no problem.
But my delegation is after the validator jailed, no slash for me (new delegator). I 'lost' 1 token just because of calculation accuracy. It is strange.

Ohhh, I see. When you unbond, you get 9 tokens back?

Sorry, I make a mistake. I receive a success event, but can only get 8 tokens back.

    "logs":[
        {
            "msg_index":0,
            "success":true,
            "log":"",
            "events":[
                {
                    "type":"message",
                    "attributes":[
                        {
                            "key":"action",
                            "value":"begin_unbonding"
                        },
                       ...
                    ]
                },
                {
                    "type":"unbond",
                    "attributes":[
                        ...
                        {
                            "key":"amount",
                            "value":"9"
                        },
                        ...
                   ]
            ...
          }]

Account token amounts are from 989859999990 to 989849999998. I really lost 1 token.

Before my undelegation:

$ curl localhost:1317/staking/validators/$val_addr/delegations
{"height":"12277","result":[
  {
    ...
    "shares": "1000100010001.000100010001000100",
    "balance": "999900000000"
  },
  {
    ...
    "shares": "9.001800270036004500",
    "balance": "8"
  }
]}

And after undelegation:

$ curl localhost:1317/staking/validators/$val_addr/delegations
{"height":"12779","result":[
  {
    ...
    "shares": "1000100010001.000100010001000100",
    "balance": "999900000001"
  }
]}

So my lost token is transferred to the validator.

There is the same problem in the code of undelegation.
https://github.com/cosmos/cosmos-sdk/blob/master/x/staking/keeper/delegation.go#L802
https://github.com/cosmos/cosmos-sdk/blob/master/x/staking/types/validator.go#L483
SharesFromTokens (a little less) -> TokensFromShares (truncate int)

You didn't "lose" the token nor did the validator "take" it. These are "clippings". @rigelrozanski is my understanding correct?

Let me try to explain.

The problem is, when a validator is slashed, for example by 0.01%. Future delegations on the validator will take a fraction slightly smaller than 1 as validator's token/shares. Thus when a new delegator delegates 99 tokens to the validator, it will get 99 *10000/9999 shares. However, it will be stored as 99.009900990099009900 due to precision limit. Then, when using this truncated shares to compute the balance of the delegation(using function TokensFromShares), it will get 98 tokens, as the results of querying the delegation.

When the delegator wants to undelegate, it's okay that he send a tx with 99 tokens to be undelegate. The current implementation will do the above computation again to get the truncated shares, and calling TokensFromShares with the truncated shares will get 98 as the result. And when the undelegation gets mature, the delegator gets 98 tokens back, as what we mean "1 lost".

The remaining 1 is left in the validator's total token, and the last person who undelegates from the validator can get his delegations back together with the additional "1 lost" token. Because when the undelegation causes validator's remaining shares to be zero, the tokens get back to his account will be validator's total token left.

Correct, that sounds accurate @selectAname. IIRC, this is already known and discussions were had around this prior to launch -- we call these "clippings". Until we can work on finalizing https://github.com/cosmos/cosmos-sdk/issues/4399, there isn't anything you can do here.

Not sure how you'd like to proceed on this issue, but I recommend we close it.

OK

Was this page helpful?
0 / 5 - 0 ratings