-
Notifications
You must be signed in to change notification settings - Fork 3.8k
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
Changes from 3 commits
dd75176
7caea52
4543dcf
4c21c59
5703eae
0215365
a55164a
bab8b34
3a8235d
91547f4
778b53d
c1935a4
73224dd
d119e0f
63c6f4a
c1d746f
b3bb5b1
748cd14
aff8b0e
abd3eec
c30da51
4b9d008
98d475c
2412404
18aad2d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package auth | ||
|
||
import ( | ||
"time" | ||
"errors" | ||
|
||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
|
@@ -30,11 +31,13 @@ type Account interface { | |
// AccountDecoder unmarshals account bytes | ||
type AccountDecoder func(accountBytes []byte) (Account, error) | ||
|
||
var _ Account = (*BaseAccount)(nil) | ||
var _ VestingAccount = (*ContinuousVestingAccount)(nil) | ||
var _ VestingAccount = (*DelayTransferAccount)(nil) | ||
|
||
//----------------------------------------------------------- | ||
// BaseAccount | ||
|
||
var _ Account = (*BaseAccount)(nil) | ||
|
||
// BaseAccount - base account structure. | ||
// Extend this by embedding this in your AppAccount. | ||
// See the examples/basecoin/types/account.go for an example. | ||
|
@@ -115,6 +118,133 @@ func (acc *BaseAccount) SetSequence(seq int64) error { | |
return nil | ||
} | ||
|
||
// VestingAccount is an account that can define a vesting schedule | ||
// Vesting coins can still be delegated, but only transferred after they have vested | ||
type VestingAccount interface { | ||
Account | ||
|
||
// Returns true if account is still vesting, else false | ||
// CONTRACT: After account is done vesting, account behaves exactly like BaseAccount | ||
IsVesting(time.Time) bool | ||
|
||
// Calculates amount of coins that can be sent to other accounts given the current time | ||
SendableCoins(time.Time) sdk.Coins | ||
// Called on bank transfer functions (e.g. bank.SendCoins and bank.InputOutputCoins) | ||
// Used to track coins that are transferred in and out of vesting account after initialization | ||
TrackTransfers(sdk.Coins) | ||
} | ||
|
||
// Implement Vesting Interface. Continuously vests coins linearly from StartTime until EndTime | ||
type ContinuousVestingAccount struct { | ||
BaseAccount | ||
OriginalVestingCoins sdk.Coins // Coins in account on Initialization | ||
TransferredCoins sdk.Coins // Net coins transferred into and out of account. May be negative | ||
|
||
// StartTime and EndTime used to calculate how much of OriginalCoins is unlocked at any given point | ||
StartTime time.Time | ||
EndTime time.Time | ||
} | ||
|
||
func NewContinuousVestingAccount(addr sdk.AccAddress, originalCoins sdk.Coins, startTime, endTime time.Time) ContinuousVestingAccount { | ||
bacc := BaseAccount{ | ||
Address: addr, | ||
Coins: originalCoins, | ||
} | ||
return ContinuousVestingAccount{ | ||
BaseAccount: bacc, | ||
OriginalVestingCoins: originalCoins, | ||
StartTime: startTime, | ||
EndTime: endTime, | ||
} | ||
} | ||
|
||
// Implements VestingAccount interface. | ||
func (vacc ContinuousVestingAccount) IsVesting(blockTime time.Time) bool { | ||
return blockTime.Unix() > vacc.EndTime.Unix() | ||
} | ||
|
||
// Implement Vesting Account interface. Uses time in context to calculate how many coins | ||
// has been released by vesting schedule and then accounts for unlocked coins that have | ||
// 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()) | ||
|
||
// 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 commentThe 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 |
||
amt := int64(float64(c.Amount.Int64()) * scale) | ||
|
||
// 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 commentThe reason will be displayed to describe this comment to others. Learn more. We should do all this match on the |
||
if currentAmount < amt { | ||
amt = currentAmount | ||
// prevent double count of transferred coins | ||
amt -= vacc.TransferredCoins.AmountOf(c.Denom).Int64() | ||
} | ||
|
||
// Add non-zero coins | ||
if amt != 0 { | ||
coin := sdk.NewInt64Coin(c.Denom, amt) | ||
unlockedCoins = unlockedCoins.Plus(sdk.Coins{coin}) | ||
} | ||
} | ||
|
||
return unlockedCoins | ||
} | ||
|
||
// Implement Vesting Account. Track transfers in and out of account | ||
// Send amounts must be negated | ||
func (vacc *ContinuousVestingAccount) TrackTransfers(coins sdk.Coins) { | ||
vacc.TransferredCoins = vacc.TransferredCoins.Plus(coins) | ||
} | ||
|
||
// Implements Vesting Account. Vests all original coins after EndTime but keeps them | ||
// all locked until that point. | ||
type DelayTransferAccount struct { | ||
BaseAccount | ||
TransferredCoins sdk.Coins // Any received coins are sendable immediately | ||
|
||
// All coins unlocked after EndTime | ||
EndTime time.Time | ||
} | ||
|
||
func NewDelayTransferAccount(addr sdk.AccAddress, originalCoins sdk.Coins, endTime time.Time) DelayTransferAccount { | ||
bacc := BaseAccount{ | ||
Address: addr, | ||
Coins: originalCoins, | ||
} | ||
return DelayTransferAccount{ | ||
BaseAccount: bacc, | ||
EndTime: endTime, | ||
} | ||
} | ||
|
||
// Implements VestingAccount | ||
func (vacc DelayTransferAccount) IsVesting(blockTime time.Time) bool { | ||
return blockTime.Unix() > vacc.EndTime.Unix() | ||
} | ||
|
||
// Implements VestingAccount. If Time < EndTime return only net transferred coins | ||
// 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 commentThe 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. |
||
// Return net transferred coins | ||
// If positive, then those coins are sendable | ||
return vacc.TransferredCoins | ||
} | ||
|
||
// 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 commentThe 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 commentThe 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 commentThe 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.
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? |
||
} | ||
|
||
// Implement Vesting Account. Track transfers in and out of account | ||
// Send amounts must be negated | ||
func (vacc *DelayTransferAccount) TrackTransfers(coins sdk.Coins) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the spirit of minimizing contracts, can we have two separate methods, "TrackSend", "TrackReceive" and have these functions handle negating. |
||
vacc.TransferredCoins = vacc.TransferredCoins.Plus(coins) | ||
} | ||
|
||
//---------------------------------------- | ||
// Wire | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe reason will be displayed to describe this comment to others. Learn more. Is there a way we can implement this which just implements There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm... wouldn't that entail changing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Account's an interface, we can implement a different There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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. |
||
if ok && vacc.IsVesting(blockTime) { | ||
// check if account has enough unlocked coins | ||
sendableCoins := vacc.SendableCoins(blockTime) | ||
if !sendableCoins.IsGTE(amt) { | ||
return nil, sdk.ErrInsufficientCoins(fmt.Sprintf("Vesting account does not have enough unlocked coins: %s < %s", sendableCoins, amt)) | ||
} | ||
// Track the transfer amount (send negated) | ||
vacc.TrackTransfers(amt.Negative()) | ||
} | ||
|
||
_, subTags, err := subtractCoins(ctx, am, fromAddr, amt) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -184,16 +197,35 @@ func sendCoins(ctx sdk.Context, am auth.AccountMapper, fromAddr sdk.AccAddress, | |
if err != nil { | ||
return nil, err | ||
} | ||
// check if receiver is a vesting account | ||
vacc, ok = am.GetAccount(ctx, fromAddr).(auth.VestingAccount) | ||
if ok && vacc.IsVesting(blockTime) { | ||
// Track the transfer amount | ||
vacc.TrackTransfers(amt) | ||
} | ||
|
||
return subTags.AppendTags(addTags), nil | ||
} | ||
|
||
// InputOutputCoins handles a list of inputs and outputs | ||
// NOTE: Make sure to revert state changes from tx on error | ||
func inputOutputCoins(ctx sdk.Context, am auth.AccountMapper, inputs []Input, outputs []Output) (sdk.Tags, sdk.Error) { | ||
blockTime := ctx.BlockHeader().Time | ||
allTags := sdk.EmptyTags() | ||
|
||
for _, in := range inputs { | ||
// Check if sender is vesting account | ||
vacc, ok := am.GetAccount(ctx, in.Address).(auth.VestingAccount) | ||
if ok && vacc.IsVesting(blockTime) { | ||
AdityaSripal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// check if vesting account has enough unlocked coins | ||
sendableCoins := vacc.SendableCoins(blockTime) | ||
if !sendableCoins.IsGTE(in.Coins) { | ||
return nil, sdk.ErrInsufficientCoins(fmt.Sprintf("Vesting account does not have enough unlocked coins: %s < %s", sendableCoins, in.Coins)) | ||
} | ||
// Track the transfer amount (send negated) | ||
vacc.TrackTransfers(in.Coins.Negative()) | ||
} | ||
|
||
_, tags, err := subtractCoins(ctx, am, in.Address, in.Coins) | ||
if err != nil { | ||
return nil, err | ||
|
@@ -206,6 +238,14 @@ func inputOutputCoins(ctx sdk.Context, am auth.AccountMapper, inputs []Input, ou | |
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// check if receiver is a vesting account | ||
vacc, ok := am.GetAccount(ctx, out.Address).(auth.VestingAccount) | ||
if ok && vacc.IsVesting(blockTime) { | ||
// Track the transfer amount | ||
vacc.TrackTransfers(out.Coins) | ||
} | ||
|
||
allTags = allTags.AppendTags(tags) | ||
} | ||
|
||
|
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