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

Vesting Spec #1875

Merged
merged 14 commits into from Aug 22, 2018
Merged

Vesting Spec #1875

merged 14 commits into from Aug 22, 2018

Conversation

AdityaSripal
Copy link
Member

  • Linked to github-issue with discussion and accepted design
  • Updated all relevant documentation (docs/)
  • Updated all relevant code comments
  • Wrote tests
  • Added entries in PENDING.md that include links to the relevant issue or PR that most accurately describes the change.
  • Updated cmd/gaia and examples/

For Admin Use:

  • Added appropriate labels to PR (ex. wip, ready-for-review, docs)
  • Reviewers Assigned
  • Squashed all commits, uses message "Merge pull request #XYZ: [title]" (coding standards)

@AdityaSripal AdityaSripal changed the title WIP: Vesting Spec Vesting Spec Jul 30, 2018
@codecov
Copy link

codecov bot commented Jul 30, 2018

Codecov Report

Merging #1875 into develop will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##           develop   #1875   +/-   ##
=======================================
  Coverage     63.5%   63.5%           
=======================================
  Files          118     118           
  Lines         7006    7006           
=======================================
  Hits          4449    4449           
  Misses        2301    2301           
  Partials       256     256

`{"BlockLock": BlockLock}` gets created and put in initial state. Otherwise if `Type == "base"` a base account is created
and the `BlockLock` attribute of corresponding `GenesisAccount` is ignored. `InitChain` will panic on any other account types.

### Pros and Cons
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lol I'm not sure that a pro's con's section really belongs in a spec - but not sure where it belongs beyond a proposal github issue


This paper specifies changes to the auth and bank modules to implement vested accounts for the Cosmos Hub.
The requirements for this vested account is that it should be capable of being initialized during genesis with
a starting balance X coins and a vesting blocknumber N. The owner of this account should be able to delegate to validators and vote,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be way better if this used BFT time instead of block number

// It is upto handler to use these appropriately
GetParams() map[string]interface{}
SetParams(map[string]interface{}) error
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be more like GetParams([]byte) []byte and SetParams([]byte, []byte)
p.s. golang maps are a non-deterministic type meaning we can't use them in the state-machine


The `VestedAccount` will be an implementation of `Account` interface that wraps `BaseAccount` with
`Type() => "vested` and params, `GetParams() => {"BlockLock": blockN (int64)}`.
`SetParams` will be disabled as we do not want to update params after vested account initialization.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if we can just eliminate Type (are we introducing it?) and only have a block-lock param - if it exists then we know it's vested, or rather just locked - which is the important part

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea I want to introduce it. If we just want to support this specific case, we can. I think we should add the Type tho, because we may want to add other types of Account. And people building on top of sdk should be able to create multiple account types as well.

For example, in the future we may want to support NLocked Accounts where each month a new amount of coins get unlocked, or maybe some other feature. I don't think we should add cruft to BaseAccount every time we want something new and specific to our application.

imo, it makes sense to create different Account types that define their own parameters and have handlers that can handle specific account types.

Will move a lot of this into a proposal issue as you suggested and rewrite.
Thanks for the review!

Copy link
Contributor

@rigelrozanski rigelrozanski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a few comments throughout - I like the approach you've proposed of extending the account

This document is structured more like a proposal for these features, and it's placed in a funny location given that it affects bank and auth.

I think maybe this PR should actually be modifying the existing spec documents which are relevant, and also having a new document talking about vesting, but not changes to other modules. Like, let's phrase this as more definite changes and implementation requirements - not a proposal

@AdityaSripal AdityaSripal added this to In progress in Current Iteration via automation Jul 30, 2018
@AdityaSripal AdityaSripal moved this from In progress to Review Needed Now in Current Iteration Jul 30, 2018
// Account is a standard account using a sequence number for replay protection
// and a pubkey for authentication.
type Account interface {
Type() string // returns the type of the account
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(btw, should be Vesting not Vested).

I'm not sure that having a top-level type is the right solution.
Would it make more sense to have an optional interface method, like

type VestingAccount interface {
    Account
    AssertIsVestingAccount() // existence implies that account is vesting.
}

Then you an check with vacc, ok := acc.(VestingAccount); ok

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works well in our case where we are choosing from two possible account types.

I think we should leave the option open to adding more account types in the future, and for developers building on the SDK to have many supported account types.

In those cases, I think having a Type() function that handlers can use to switch their functionality based on the account type is better than trying to type cast for multiple account types.

handler can then call `GetParams` to handle the specific account type using the parameters it expects to
exist in the parameter map.

The `VestedAccount` will be an implementation of `Account` interface that wraps `BaseAccount` with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vesting

}
```

During `InitChain`, the GenesisAccount's are decoded. If they have `Type == "vested`, a vested account with parameters =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GenesisAccounts are

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vesting

@jaekwon
Copy link
Contributor

jaekwon commented Jul 30, 2018

Can someone document why we want to do this, as opposed to having one account that can vest continuously over time? The latter seems much more flexible and I am worried about having to deal with e.g. 24 accounts, or more if we want more fine-grained vesting.

  • Additional fields OrigTotalCoins, StartVestingTime, and EndVestingTime in the concrete VestingAccount struct, used only for calculating how much is required to stay in the account.
  • You can add and remove coins from the vesting account, but none of the coins can ever be less than OrigTotalCoins * Max(VectorOfOnes, Now-StartTimeVesting/(EndTimeVesting-StartTimeVesting)). The relevant function here is IsGTE() or IsPositive() after a subtraction.

I think it would require more tooling for us to deal with 24 accounts, than to deal with this more flexible vesting system.

@AdityaSripal
Copy link
Member Author

@jaekwon I originally considered doing vesting like a step function, it ends up being a bit more complicated than what you're suggesting. This is because transactions that we want to allow a vesting account to make (like staking) will subtract coins. Thus, vesting accounts balance are allowed to go below the equation you posted.

One thing we could do is to have UnsendableCoins be initialized with the starting balance of the account. When the account delegates or deposits for a vote, the associated handler subtracts the tx amount from UnsendableCoins. Then, the above formula works by replacing OriginalTotalCoins with UnsendableCoins. This will also allow vesting account to send coins earned from fees/inflation.

The downside with this approach is that all handlers for messages we want vesting accounts to be able to call (e.g staking, governance) before fully vesting must be aware of the existence of vesting accounts and update their state correctly. Whereas with the current approach, only the bank module is aware and cares about the existence of vesting accounts.

Not sure how the pros and cons weigh out between the two approaches.

@rigelrozanski
Copy link
Contributor

Jae:

Can someone document why we want to do this, as opposed to having one account that can vest continuously over time? The latter seems much more flexible and I am worried about having to deal with e.g. 24 accounts, or more if we want more fine-grained vesting.

^ @zmanian

@rigelrozanski
Copy link
Contributor

Just realized I don't think we want any of this.. Account already IS an interface which means we can have multiple implementations of Account - why don't we just implement new a new Account for vesting which is the same as sdk.AccAccount BUT with different GetCoins() sdk.Coins and SetCoins(sdk.Coins) error methods which don't allow the withdrawal of tokens earlier than is allowable?

@AdityaSripal
Copy link
Member Author

Yea, realized we shouldn't be changing Account interface as well. Reworking spec

But we can't just implement SetCoins differently. This is because we still want staking handler to be able to subtract coins from our account.

I'll finish up what I have in mind and hopefully that clears things up.

@rigelrozanski
Copy link
Contributor

Awesome - yeah sounds good, excited to see your thinking

The requirements for this vesting account is that it should be capable of being initialized during genesis with
a starting balance X coins and a vesting blocktime T. The owner of this account should be able to delegate to validators
and vote with locked coins, however they cannot send locked coins to other accounts until those coins have been unlocked.
The vesting account should also be able to spend any coins it receives from other users or from fees/inflation rewards.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Old spec was not able to spend coins gained from fees/inflation. New spec will allow vesting account to do so.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait this is kind of seperate, the ability to spend coins received through the fee-distribution mechanism is independent of the vesting account because the fee-distribution mechanism allows you with withdraw fees to a seperate account - we should keep these distinct and not make the vesting account have special logic for this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the purpose of ReceivedCoins is to makes sure that funds sent to the VestingAccount after initialization are spendable immediately.

```

During `InitChain`, the GenesisAccounts are decoded. If `EndTime == 0`, a BaseAccount gets created and put in Genesis state.
Otherwise a vesting account is created with `StartTime = RequestInitChain.Time`, `EndTime = gacc.EndTime`, and `OriginalCoins = Coins`.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flagging for future reference. Since we are using the time in Genesis file as start time, we should make sure creation of Genesis file is close-ish to launch. Otherwise it messes up the slope for continuous vesting.

@AdityaSripal
Copy link
Member Author

AdityaSripal commented Aug 2, 2018

@jaekwon the checks described in this spec happen "upstream" from the checks that are already in bank's Keeper methods. Thus, I am really calculating the maximum amount of unlocked coins this account can spend.

If the formula written says you are allowed to spend 15 steak, but you only have 10 steak in the account because you have delegated 5 of your unlocked coins, the keeper will still only allow you to spend up to 10 steak.

There's no need to convert. I think it's worthwhile because Vesting accounts take up more space and gas cost for running transactions. Thus, it makes sense to me to do the one-time conversion though it isn't necessary

I don't think its possible to avoid changing bank. This is because we want all the other modules to be able to set account Coins as normal, but bank module needs special logic before it can set Coins. Thus, the logic can't be implemented at the account or mapper level. I'm not sure how a different message type alleviates this though interested to hear the proposal.

Ex for further clarity:

Let's say I start with 50 steak in a vesting account of which 30 is locked.
I have 40 steak delegated to various validators.

I get sent 10 steak by some other users.

The maximum amount of unlocked coins I am allowed to spend calculated by formula in spec: 30 steak
Amount of steak I should be allowed to spend at this moment: 20 steak

In order for a bank MsgSend to succeed, it checks:
Amount less than maximum allowed spend (30 steak)
Amount less than current balance (20 steak)

If both checks pass, the MsgSend succeeds. Else it fails.

@AdityaSripal AdityaSripal dismissed stale reviews from rigelrozanski and alexanderbez August 3, 2018 20:18

addressed comments, added formulas at the bottom for easier verification

@alexanderbez
Copy link
Contributor

@AdityaSripal I think the updates help greatly. It seems like the pseudo-code format could be improved a bit.

@AdityaSripal
Copy link
Member Author

@alexanderbez anything specific about the pseudocode that was hard to read? I cleaned some of it up to hopefully make it clearer. Let me know if there's still some improvements to be made

`sendCoins` and `inputOutputCoins`. These methods must check that an account is a vesting account using the check described above.

```go
if Now < vacc.EndTime:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AdityaSripal I was mostly referring to this 👍

cwgoes
cwgoes previously requested changes Aug 14, 2018
Copy link
Contributor

@cwgoes cwgoes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments, also:

  • Can we describe how ContinuousVestingAccount implements BaseAccount (in particular GetCoins)?

```go
type VestingAccount interface {
Account
AssertIsVestingAccount() // existence implies that account is vesting.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this function?


// Calculates total amount of unlocked coins released by vesting schedule
// May be larger than total coins in account right now
TotalUnlockedCoins(sdk.Context) sdk.Coins
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be a public method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it will be used in bank's keeper methods

type VestingAccount interface {
Account
AssertIsVestingAccount() // existence implies that account is vesting.
ConvertAccount(sdk.Context) BaseAccount
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just accesses BaseAccount functions, right? Convert is a strange name, it implies that one account is being turned into another, maybe AsBase()?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Originally the plan was to check if account has fully vested (i.e. Now > EndTime) and then simply return the embedded BaseAccount.
This BaseAccount would then replace the completely vested VestingAccount in the Account store. The main benefit would be that BaseAccount would take up less space in store but it is certainly not necessary.

After talking to @jaekwon , removing this functionality for now.

// Continuously vests by unlocking coins linearly with respect to time
type ContinuousVestingAccount struct {
BaseAccount
OriginalCoins sdk.Coins // Coins in account on Initialization
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i.e. that are vesting? VestingCoins?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No upon initialization all coins in OriginalCoins are vesting, true. However as time goes on, some of those coins become vested but OriginalCoins never gets updated.

OriginalCoins denotes all the coins that are in account on initialization, which are the only coins subject to the vesting schedule.
However as these coins go from vesting to vested, OriginalCoins never changes.

The way I understand it, a coin that is unspendable is "vesting" and once it becomes spendable it is "vested". It's entirely possible that this is not correct terminology.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "OriginalVestingCoins" would make it clearer?

OriginalCoins sdk.Coins // Coins in account on Initialization
ReceivedCoins sdk.Coins // Coins received from other accounts

// StartTime and EndTime used to calculate how much of OriginalCoins is unlocked at any given point
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these will now be time.Time since we've merged TM 0.23


// ConvertAccount converts VestingAccount into BaseAccount
// Will convert only after account has fully vested
ConvertAccount(vacc ContinuousVestingAccount, ctx sdk.Context) (BaseAccount):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we planning to save the converted account or something? Don't we need to deal with any remaining OriginalCoins or ReceivedCoins?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed above, will remove


// Uses time in context to calculate total unlocked coins
TotalUnlockedCoins(vacc ContinuousVestingAccount, ctx sdk.Context) sdk.Coins:
unlockedCoins := ReceivedCoins + OriginalCoins * (Now - StartTime) / (EndTime - StartTime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is continuous, but I think we want discrete vesting (once per month)?

I could be wrong, if you've discussed this with others please disregard

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah for now it's continuous because that is what jae and I discussed last. I'm currently trying to confirm what the preferred vesting schedule is for AiB.

Changing the vesting schedule just comes down to changing implementation of this single method.

// NOTE: SendableCoins may be greater than total coins in account
// because coins can be subtracted by staking module
// SendableCoins denotes maximum coins allowed to be spent.
if msg.Amount > vestingAccount.TotalUnlockedCoins() then fail
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if I've already sent some unlocked coins? I don't think this check is correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, originally thought the second check would stop this but it won't.

Fixing this in refactor


// Account fully vested, convert to BaseAccount
else:
account = ConvertAccount(account)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above comment, also I'm not clear on why we need to convert in the first place, the space savings don't matter much since the number of vesting accounts is finite and very small

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, removing this functionality


**Maximum amount of coins spendable right now:**

`min( ReceivedCoins + OriginalCoins - LockedCoins, CurrentCoins )`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to contradict the above definition under keeper changes?

@cwgoes
Copy link
Contributor

cwgoes commented Aug 14, 2018

Discrete vesting is not hard as far as I know, just use time floor div vesting interval.

`SentCoins`, `StartTime`, and `EndTime` to calculate how many coins are sendable at any given point.
Since the vesting restrictions need to be implemented on a per-module basis, the `ContinuousVestingAccount` implements
the `Account` interface exactly like `BaseAccount`. Thus, `ContinuousVestingAccount.GetCoins()` will return the total of
both locked coins and unlocked coins currently in the account.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe good to add "Delegated coins are deducted from Account.GetCoins(), but do not count against unlocked coins because they are still at stake and will be reinstated (partially if slashed) after waiting the full unbonding period."

originally own should increment `ReceivedCoins` by the amount sent.
Unlocked coins that are sent to other accounts will increment the vesting account's `SentCoins` attribute.

CONTRACT: Handlers SHOULD NOT update `ReceivedCoins` if they were originally sent from the vesting account. For example, if a vesting account unbonds from a validator, their tokens should be added back to account but `ReceivedCoins` SHOULD NOT be incremented.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Staking handlers SHOULD NOT update ...

@jaekwon
Copy link
Contributor

jaekwon commented Aug 16, 2018

Left comments, otherwise LGTM.

@AdityaSripal AdityaSripal dismissed cwgoes’s stale review August 21, 2018 16:45

Addressed comments

Copy link
Contributor

@rigelrozanski rigelrozanski left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should merge unless @cwgoes or @alexanderbez have additional comments

Copy link
Contributor

@cwgoes cwgoes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

utACK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

6 participants