-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
WIP: Vesting Implementation #2168
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good start, see comments.
x/auth/account.go
Outdated
// already been transferred or delegated | ||
func (vacc ContinuousVestingAccount) SendableCoins(blockTime time.Time) sdk.Coins { | ||
unlockedCoins := vacc.TransferredCoins | ||
scale := float64(blockTime.Unix() - vacc.StartTime.Unix()) / float64(vacc.EndTime.Unix() - vacc.StartTime.Unix()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Floats are dangerous - we should use sdk.Dec
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah floats are non-deterministic, we pretty much can't use them anywhere
x/auth/account.go
Outdated
|
||
// Must constrain with coins left in account | ||
// Since some unlocked coins may have left account due to delegation | ||
currentAmount := vacc.GetCoins().AmountOf(c.Denom).Int64() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should do all this match on the sdk.Int
type, not convert to int64
.
x/auth/account.go
Outdated
scale := float64(blockTime.Unix() - vacc.StartTime.Unix()) / float64(vacc.EndTime.Unix() - vacc.StartTime.Unix()) | ||
|
||
// Add original coins unlocked by vesting schedule | ||
for _, c := range vacc.OriginalVestingCoins { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand why we need this loop - can't we just subtract TransferredCoins
from OriginalVestingCoins
?
x/bank/keeper.go
Outdated
@@ -175,6 +175,19 @@ func addCoins(ctx sdk.Context, am auth.AccountMapper, addr sdk.AccAddress, amt s | |||
// SendCoins moves coins from one account to another | |||
// NOTE: Make sure to revert state changes from tx on error | |||
func sendCoins(ctx sdk.Context, am auth.AccountMapper, fromAddr sdk.AccAddress, toAddr sdk.AccAddress, amt sdk.Coins) (sdk.Tags, sdk.Error) { | |||
// check if sender is vesting account | |||
blockTime := ctx.BlockHeader().Time | |||
vacc, ok := am.GetAccount(ctx, fromAddr).(auth.VestingAccount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way we can implement this which just implements BaseAccount
differently and returns SendableCoins()
for GetCoins()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm... wouldn't that entail changing the Account
interface?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Account's an interface, we can implement a different GetCoins()
for the VestingAccount
struct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't believe we can without a lot of changes to bank and other modules that depend on it.
For example, bank keeper's subtractCoins method needs GetCoins
to return the full amount in the account. Otherwise the staking handler will only allow you to delegate unlocked coins
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK got it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I actually think this is important to do though. A large premise of the SDK is that you should be able to add your own account types that can support all the features you want within your other modules. Here this is a sign that the API we provide is insufficient, or we haven't tried to make vesting fit into the correct API. We want anyone to be able to achieve the functionality they want with their custom accounts without modding baseapp like this.
I think we should refactor things accordingly so that vesting can be its own full module, not an add-on within auth.
Perhaps we add a "UseableCoins" function to account that takes in the context. This would have utility outside of just vesting, e.g. for covenants and the like.
Codecov Report
@@ Coverage Diff @@
## develop #2168 +/- ##
==========================================
+ Coverage 61.52% 61.93% +0.4%
==========================================
Files 122 122
Lines 7486 7587 +101
==========================================
+ Hits 4606 4699 +93
- Misses 2561 2569 +8
Partials 319 319 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still just don't like putting this logic in x/bank
, I think it's the wrong approach to abstraction: it places the requirement to manage token tracking correctly on the caller, and it increases the surface area for bugs in any other modules which might utilize a VestingAccount
.
What about changing the account interface itself, so that instead of having GetCoins
and SetCoins
, an account has AddCoins
, SubtractCoins
, GetSpendableCoins
, and GetTotalCoins
(or something, but you get the idea). That way we could encapsulate the vesting logic cleanly within auth and guarantee that any module calling methods on the account can't handle vesting accounts incorrectly by accident.
Is this too much of an interface change? Replies / other ideas welcome.
Action items based on conversation with @jaekwon @cwgoes @alexanderbez
|
I think to some extent this is unavoidable because the token updating behavior of the account changes depending on who the caller is (banking vs staking module). I do think there are ways to alleviate this by changing the Account interface tho. Maybe we can allow the Account interface to distinguish between a coin update that is caused by a transfer and one that is caused by something else (i.e. staking) So either we could have in Account interface: SetCoins, GetCoins, TransferCoins(amt) OR SetCoins(oldargs, bool transfer), GetCoins(bool transfer) where the SetCoins and GetCoins changes behavior depending on whether the caller intends to set/get because of a transfer. =========================================================================
I think this is an ok solution, but do we also want vesting accounts to bond governance deposits to create proposals. If so, allowing a special case for staking only seems strange.
Don't we want DelegateCoins to subtract vesting coins from the account? If so, changing subtract coins is wrong and we should be changing the transfer-specific functions as I have done. |
Another point on Action Items: If only DelegateCoins can subtract vesting coins, then an account with only vesting coins will not be able to create a delegate transaction because there are no vested coins to submit for the transaction fee. There needs to be a way for feeKeeper to subtract vestingcoins for the fee if no vested coins are available. |
x/auth/account.go
Outdated
// Else return all coins in account (like BaseAccount) | ||
func (vacc DelayTransferAccount) SendableCoins(blockTime time.Time) sdk.Coins { | ||
// Check if ctx.Time < EndTime | ||
if blockTime.Unix() < vacc.EndTime.Unix() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whats the reason for casting these both to unix? They should be comparable in time.Time format. (e.g. time1.Before(time2)
x/auth/account.go
Outdated
} | ||
|
||
// If EndTime has passed, DelayTransferAccount behaves like BaseAccount | ||
return vacc.BaseAccount.GetCoins() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is deleting the vesting account and making a base-account w/ the same address something thats easy to do? (I feel like doing this makes one less edge-case to be concerned with)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was in the original spec but later removed because it is technically unnecessary. The thinking was it takes up a little bit more space, and one could always transfer all coins into a baseaccount after vesting.
Fair point about the edge case. It is easy to do but makes the code more complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it will be easier to verify correctness of auto-transitioning a vesting account to a base account than it will be verify correctness of a vesting account once done vesting. (I also think it makes the mental model of this far simpler, and consequently safer) Hence why I think its probs worth doing.
The address would remain the same, so this change wouldn't be noticeable by the other modules as well.
Fair point about the edge case. It is easy to do but makes the code more complicated.
I don't see why it adds a non-neglible increase to code complexity. Isn't basically just setting whats in the account mapper to a new base account with the correct number of tokens?
@jaekwon et al, summarizing our discussion from earlier today: Delegating coins from a vesting account currently supports delegating with both vestING (locked) and vestedED (plus received) coins. The problem arises when the account decides to
|
@alexanderbez Not making vested/vesting a property of the coin IS what makes undelegating easy Instead whenever someone tries to withdraw from account, the calculation is made dynamically to determine how many of the account's coins are vesting vs unlocked. If Vesting becomes a property of the coin, I think implementing something like ContinuousVestingAccount becomes much more complicated |
@AdityaSripal agreed -- we still need to come up with a better solution. |
@AdityaSripal @cwgoes @rigelrozanski @ValarDragon Had a discussion with Jae earlier and the following was suggested to update in the spec:
With that in mind, a vesting account can be thought to have the following notation:
The following operations on a vesting account can be defined as such: Receiving coinsAdds to the Sending coinsA vesting account can only send what is available. This is defined as an amount up to: Note, the account may have Delegating coinsA vesting account can delegate from its vesting pool and vested pool. Assuming it wants to delegate Presuming the account has enough, deduct Undelegating coinsWhen Finally, I've ran through a few examples, albeit late at night, and didn't see anything obviously wrong, but would like thoughts on this. |
here's another representation of the same:
|
it looks like the Vesting period freezes during delegation. If all the coins I have in an account are vesting with 1 month left, then I delegate them out for 2 months. When they come back into my account, it looks like they are still vesting? Shouldn't it be undelegated by that point since the vesting continues during delegation? I feel like the desired behavior should be that coins continue vesting while they are also used as delegation. Is there a reason this behavior was changed or am I just wrong about the above? |
Questions:
|
@jaekwon Also point 2 above is more relevant if the delegation in question gets slashed. |
Upon further reflection, I think the existence of slashing is a serious problem here. Consider the following sequence of events (all occurring in quick succession, so no change in the vested coin amount):
If validator A had been slashed by 100%, I would have ended up with 5 vesting coins and no free coins - and worse, if both validators got slashed and I later delegated free coins they would be turned back into vesting coins when I undelegated. Possibly we could rectify this by updating |
LockedCoins/UnlockedCoins are calculated dynamically based on the constants OriginalVestingCoins and start/end dates.
See above.
5 vesting, 0 delegated vesting, 10 BC.
4.99 vesting (time passed), 5 delegated vesting, 5 BC.
It should now be 4.98 vesting (time passed), 0 delegated vesting, 7.5 BC. This is fine. I think there was a problem with slashing but not the way you mentioned... I think this is resolved with the new rule for sending/transfering coins:
|
OK, talked with @jaekwon, makes more sense now - note: if delegated vesting coins are slashed, I still think "unlocked coins get slashed first" (above example) may be a problem. |
@cwgoes is this what you were referring to?
|
@jaekwon The above makes sense to me, except:
I think you mean
I don't understand why we need this check, shouldn't our existing undelegation logic ensure both? |
Yes.
Just a sanity check / assertion, might as well? |
@alexanderbez The plan for vesting has changed enough that I suspect it might be easier to close this PR and open a fresh one - do you agree? |
I agree 100%. Closing in favor of impending new implementation. |
Starting implementation for Vesting
Targeted PR against correct branch (see CONTRIBUTING.md)
Linked to github-issue with discussion and accepted design OR link to spec that describes this work.
Wrote tests
Updated relevant documentation (
docs/
)Added entries in
PENDING.md
with issue #rereviewed
Files changed
in the github PR explorerFor Admin Use: