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

Consistent account behavior: vesting, slashing, and liens, oh my! #4524

Closed
JimLarson opened this issue Feb 10, 2022 · 5 comments
Closed

Consistent account behavior: vesting, slashing, and liens, oh my! #4524

JimLarson opened this issue Feb 10, 2022 · 5 comments
Assignees
Labels
agd Agoric (Golang) Daemon agoric-cosmos BLD-Boost Issues relating to the BLD Boost contract and UI enhancement New feature or request restival to be done before RUN Protocol Purple Team festival
Milestone

Comments

@JimLarson
Copy link
Contributor

JimLarson commented Feb 10, 2022

What is the Problem Being Solved?

Vesting with clawback adds another dimension of state and activity to an already complicated picture. There are concerns that the currently-implemented behavior isn't as consistent as it could be. This issue explores the options.

Description of the Design

For the purpose of this discussion, we'll only consider staking tokens. Other tokens work similarly, except they can't be staked.

We'll also describe the operations and behavior of a ClawbackVestingAccount, as every other account type is a subset of its capabilities.

Primary Facts of Account Management in Cosmis-SDK

  • Slashing happens without any per-delegator overhead
    • We're likely to have around 10^2 validators but 10^5 stakers
    • Slashing effects are recognized when unstaking.
  • Staking and unstaking commands do not currently specify the character of tokens in other dimensions
    • Character of slashed tokens is recognized by policy.

Dimensions of State

Quantities noted as "encumbered" may not be transferred out of the account.

Staking Dimension

For the purposes here we'll lump "Unbonding" tokens in with "Bonded".

  • Bonded: currently staked (and undelegating) tokens, determined by scanning records in x/staking.
  • Unbonded: assets currently held in x/bank, not staked, could still be encumbered by other dimensions.
  • Total: derived as Bonded + Unbonded

Lockup Dimension

  • Locked: Amount of grant that has not yet unlocked. Encumbered.
  • Unlocked: derived as Total - Locked.

Lien/Vesting Dimension

  • Unvested: Amount of grant that has not yet vested. Encumbered.
  • Vested: derived as Total - Unvested
    • Liened: Amount used to back a getRUN loan. Encumbered. Could be greater than Total.
    • Unliened: derived as max(0, Total - (Unvested + Liened))

Vesting bookkeeping

Vesting accounts keep additional bookkeeping for the interaction with staking. Here "vested" means Unlocked and Vested in the clawback sense.

  • OV: the original vesting grant
  • V': the amount of OV that should be unlocked, according to the lockup or vesting schedule
  • V: the amount still vesting, derived as OV - V'
  • DV: the net Bonded tokens that were considered Unvested at time of staking
  • DF: the net Bonded tokens that were considered Vested at time of staking
  • DV + DV: the net amount staked, equals current Bounded plus historical slashing

To determine how much of your x/bank balance is actually available to spend:

  • From OV and V' for the current time, compute V - the current Unvested amount (by schedule).
  • Write off up to DV tokens from this, as this accounts for Unvested tokens that are being staked or have been slashed.
  • The remainder is the amount that must be prevented from transfer out of the x/bank account.
  • Liens separately add to this do-not-transfer amount.

Operations

Each has a single argument - the number of tokens.

  • Initial Grant: provides initial unbonded tokens which will unlock and vest according to a schedule.
  • Add Grant: adds a new grant to merge with an existing one. (Might need to fix up vesting bookkeeping.)
  • Unlock Event: some locked tokens become unlocked.
  • Vesting Event: some unvested tokens become vested.
  • Stake: stake some unbonded tokens.
  • Unstake: unstake some bonded tokens.
  • Slash: remove some bonded tokens. (Note that we don't notice this until we try to unstake the affected tokens.)
  • Reward: like add grant, but adds to amounts of an existing vesting schedule.
  • Lien: encumber some vested, bonded tokens.
  • Unlien: unencumber previously liened tokens.
  • Transfer In: add some unbonded, unliened, unlocked tokens to the account.
  • Transfer Out: remove some unbonded, unliened, unlocked tokens from the account.

Current Implementation

  • In most circumstances, staked tokens are:
    • lockup dimension: Locked first
    • vesting/lien dimension: Unvested before Unliened, with Liened disposition TBD.
    • slashing is also absorbed in this same order - as if you unstaked all unslashed funds and saw what's left
  • The exceptions are:
    • at clawback time, slashed and staked tokens are Vested first, so that the Unvested funds removed are as liquid as possible
    • at reward time, staked tokens are Vested first, to maximize the Vested amount in the reward

The Liened amount is never reduced by slashing, only by an unlien operation (from repayment of a loan or reduction of required lien ratio). This can lead to the Liened amount exceeding the total account balance.

Option A: Bonded prefers encumbered always

For greater consistency, we could remove the exceptions noted above and always prefer that slashed/bonded tokens be the Unvested/Locked ones. This would mean:

  • At clawback time, we'd get back bonded tokens first, unbonded last.
  • Rewards would be maximally unvested. However, they would vest no slower than the schedule for the remaining unvested coins. E.g. if we're 3 years into a 4-year monthly vesting schedule, then any unvested rewards will vest 1/12 each of the next 12 months.

This would be a relatively small change (3 story points?) and would actually clean up a small slashing-rewards inconsistency noted at the end of my Jan 10 comment on #4085.

Option B1: Proportional

We could make the different dimensions uncorrelated and simply say that each category of one dimension is proportionally divided among the other dimensions. E.g. if we have 100 tokens total, 40 bonded, 25 unvested, 50 liened, and 60 locked, then we'd have 8 bonded-liened-unlocked tokens (= 100 * 0.4 * 0.5 * 0.4), and so on for all other combinations.

Unfortunately, this gives some strange results, as each operation changes the proportions. For instance, if we are 100% vested but 50% locked and 50% bonded with 100 tokens, and we want to withdraw as many tokens as we can, we can immediately withdraw 25 unlocked-unbonded tokens, leaving 75 tokens with 67% bonded and 67% locked - which means we now have 8.3 unlocked-unbonded tokens, which we can withdraw leaving 67 tokens with 75% bonded and 75 locked, which means we can withdraw 4 more ... and so on. There is similar behavior for liens. It seems we can iteratively remove unencumbered tokens and have the account as if we were working with Option A.

Option B2: Inconsistent-Proportional

If we drop the requirement to use the same policy consistently everywhere, we could use a proportional strategy in some situations while using another policy (e.g. Option A) elsewhere. For instance, we can do reward division and clawback proportionally. This would avoid the strange behavior documented in Option B1. The cost of this, as with the current implementation, is that the impact of slashing would vary based on the operation.

Option B3: Proportional with memory

Doing my best to give viable options for proportional: there might be a way to avoid the silly behavior of plain Option B1, but without the user-facing complexity and development time impact of Option C below. The operations still don't specify the character of the tokens that are staked, vested, unlocked, liened, etc., but we keep track of the explicit amounts on each side and select tokens proportionally. For instance, if we're totally unstaked with 70 unvested tokens and 30 vested, and we stake 20, then the staked tokens will be 70% unvested, and we remember the amounts on each side of the staking barrier: 20 staked (of which 14 are unvested and 6 vested) and 80 unstaked (of which 56 are unvested and 24 vested). Now we transfer in 80 tokens (considered vested) from outside. So we have 160 unstaked (of which 56 are unvested and 104 vested - so 35% are unvested). The amount of vested/unvested staked tokens doesn't change. If we stake additional tokens, 35% will be unvested (reflecting the proportion from the unstaked side) but if we unstake tokens, 70% will be unvested (reflecting the proportion from the staked side).

But this is just one operation pair (staking/unstaking) and one orthogonal dimension (vesting). We have another operation pair (lien/unlien) and another orthogonal dimension (lockup). I'm not yet sure if this all plays well together, or if there are other paradoxes of silliness awaiting. Even if there's a consistent design, I don't know if it can be implemented without wholesale changes to cosmos-sdk.

Option C: Explicit Cartesian product of states

Lastly, we could make the user make choices instead of forcing a policy. When staking and unstaking, liening and unleining, the user could provide the exact character of the tokens involved, e.g. "stake 12 vested-locked, 37 vested-unlocked, and 53 lieneed-locked.

We would still need to come up with an automatic policy for determining what gets slashed, perhaps proportional.

This would require extensive rework to x/auth/vesting, x/staking, as well as the staking and getRUN UI.

Security Considerations

If correctly implemented, all are secure.

Test Plan

Existing unit tests provide reasonable coverage.

@JimLarson JimLarson added enhancement New feature or request question Further information is requested needs-design BLD-Boost Issues relating to the BLD Boost contract and UI agd Agoric (Golang) Daemon restival to be done before RUN Protocol Purple Team festival agoric-cosmos labels Feb 10, 2022
@JimLarson JimLarson self-assigned this Feb 10, 2022
@JimLarson
Copy link
Contributor Author

@dtribble here's some options, let's discuss. @Tartuffo too.

@JimLarson
Copy link
Contributor Author

Summary of Friday's (2022-02-11) discussion.

  • (Almost) Never slash unvested
  • Post-clawback slashing still impacts the vestee. The repurchase clause calls for a given number of tokens to ultimately be returned, so all slashing that results from vestee-initiated staking is visited on the vestee, not the funder.
  • Yet must be able to stake unvested tokens
    • participate in rewards
    • participate in governance
    • demonstrates dominion and control of the tokens
  • Rewards are tied to that-which-can-be-slashed, therefore rewards should prefer to be vested.
    • OTOH, that-which-can-be-slashed is also not transferrable, so does this mean that vested tokens should not be transferrable? Dean is okay weakening the consistency requirement here.
  • Examples successfully demonstrated that the pro rata scheme leads to unintended and undesired consequences, so that's off the table.

The goal is to create an account with rules that automate as much of the contract as practicable, though full automation is impossible

@JimLarson
Copy link
Contributor Author

Proposal based on Friday's discussion.

If we're allowed to stake vested tokens, we're allowed to lose vested tokens. There needs to be an out-of-band mechanism for exercising the repurchase right on tokens not available in the account.

Unvested tokens are "encumbered", meaning that they can't be transferred out of the account. Staked, locked, and liened tokens are also encumbered, and it's possible to be encumbered in multiple ways. As noted above, vesting and unlocking are automatic time-based processes, but staking and liening are user operations that do not specify the character of the tokens (un)staked or (un)liened. The minimum number of tokens that need to be encumbered is the maximum of staked, locked, or unvested + liened.

We'd like to avoid giving users the ability to "game" the system by giving them a better outcome when juggling tokens among multiple accounts, vs keeping everything in one account.

Lastly, we'd prefer not to have to check for slashing, a relatively expensive operation, on ever transfer out of the account. It's reasonable to check for slashing when we're traversing the staking data structures anyhow, but we'd like to avoid having idiosyncratic performance for common operations compared to other cosmos-sdk chains.

Therefore, I propose the consistent approach where encumbered tokens are overlapped as much as possible, and that we apply the same preference to rewards and slashing: staked tokens are locked-first and unvested-first, followed by liened.

But even if nominally-unvested tokens are slashed, it does not reduce the liability at clawback time, where one of the following happens:

  • Option 1: We claw back all the nominally-unvested tokens, then make up for any unvested-slashed tokens by taking:
    • 1a: Additional tokens, staked-first.
    • 1b: Additional tokens, unstaked-first.
  • Option 2: we always use out-of-band methods (which we need to have anyhow) to reclaim unvested-slashed tokens.

Option 1 has the disadvantage that it removes tokens which are nominally vested. For instance, if the user had transferred some tokens in from an external source, those could be taken during clawback. However, one could argue that the repurchase right covers all tokens in the account.

Option 2 has the disadvantage of needed to resort to out-of-band repurchase in more cases, though "more" could be negligible if slashing and clawback are both rare.

Option 2 will be the easiest to implement, and Option 1a easier than 1b.

@JimLarson
Copy link
Contributor Author

Dean agrees with the proposal. I'll detail work items, make an estimate, and see what's necessary to do before MN-1.

@JimLarson JimLarson removed needs-design question Further information is requested labels Feb 17, 2022
@JimLarson
Copy link
Contributor Author

Work to do:

  • implement in cosmos-sdk and update ag0
  • use design when integrating with liens

Closing this task in favor of specific subtasks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
agd Agoric (Golang) Daemon agoric-cosmos BLD-Boost Issues relating to the BLD Boost contract and UI enhancement New feature or request restival to be done before RUN Protocol Purple Team festival
Projects
None yet
Development

No branches or pull requests

2 participants