Agoric / agoric-sdk

monorepo for the Agoric Javascript smart contract platform
Apache License 2.0
322 stars 201 forks source link

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

Closed JimLarson closed 2 years ago

JimLarson commented 2 years ago

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

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

Lockup Dimension

Lien/Vesting Dimension

Vesting bookkeeping

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

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

Operations

Each has a single argument - the number of tokens.

Current Implementation

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:

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 commented 2 years ago

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

JimLarson commented 2 years ago

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

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

JimLarson commented 2 years ago

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 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 commented 2 years ago

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

JimLarson commented 2 years ago

Work to do:

Closing this task in favor of specific subtasks.