diff --git a/docs/architecture/adr-061-liquid-staking.md b/docs/architecture/adr-061-liquid-staking.md index 56e3f9b0c42b..556b05c80d3d 100644 --- a/docs/architecture/adr-061-liquid-staking.md +++ b/docs/architecture/adr-061-liquid-staking.md @@ -1,16 +1,17 @@ -# ADR ADR-061: Liquid Staking +# ADR 061: Liquid Staking Module ## Changelog * 2022-09-10: Initial Draft (@zmanian) +* 2023-07-10: (@zmanian, @sampocs, @rileyedmunds, @mpoke) ## Status -PROPOSED +ACCEPTED ## Abstract -Add a semi-fungible liquid staking primitive to the default Cosmos SDK staking module. This upgrades proof of stake to enable safe designs with lower overall monetary issuance and integration with numerous liquid staking protocols like Stride, Persistence, Quicksilver, Lido etc. +Add a semi-fungible liquid staking primitive to the default Cosmos SDK staking module. While implemented as changes to existing modules, these additional features are hereinafter referred to as the liquid staking module (LSM). This upgrades proof of stake to enable safe designs with lower overall monetary issuance and integration with numerous liquid staking protocols like Stride, Persistence, Quicksilver, Lido etc. ## Context @@ -24,59 +25,385 @@ The Osmosis team has adopted the idea of Superfluid and Interfluid staking where It's also important to note that Interchain Accounts are available in the default IBC implementation and can be used to [rehypothecate](https://www.investopedia.com/terms/h/hypothecation.asp#toc-what-is-rehypothecation) delegations. Thus liquid staking is already possible and these changes merely improve the UX of liquid staking. Centralized exchanges also rehypothecate staked assets, posing challenges for decentralization. This ADR takes the position that adoption of in-protocol liquid staking is the preferable outcome and provides new levers to incentivize decentralization of stake. -These changes to the staking module have been in development for more than a year and have seen substantial industry adoption who plan to build staking UX. The internal economics at Informal team has also done a review of the impacts of these changes and this review led to the development of the exempt delegation system. This system provides governance with a tuneable parameter for modulating the risks of principal agent problem called the exemption factor. +These changes to the staking module have been in development for more than a year and have seen substantial industry adoption by protocols who plan to build staking UX. The internal economics team at Informal has also done a review of the impact of these changes and this review led to the development of the validator bond system. This system provides governance with a tuneable parameter for modulating the risks of principal agent problem called the validator bond factor. + +Liquid proof of stake systems exacerbate the risk that a single entity - the liquid staking provider - amasses more than ⅓ the total staked supply on a given chain, giving it the power to halt that chain’s block production or censor transactions and proposals. + +Liquid proof of stake may also exacerbate the principal agent risk that exists at the heart of the delegated proof of stake system. The core of the problem is that validators do not actually own the stake that is delegated to them. This leaves them open to perverse incentives to attack the consensus system. Cosmos introduced the idea of min self bond in the staking. This creates a minimum amount of stake the must be bonded by the validators operator key. This feature has very little effect on the behavior of delegates. ## Decision -We implement the semi-fungible liquid staking system and exemption factor system within the cosmos sdk. Though registered as fungible assets, these tokenized shares have extremely limited fungibility, only among the specific delegation record that was created when shares were tokenized. These assets can be used for OTC trades but composability with DeFi is limited. The primary expected use case is improving the user experience of liquid staking providers. +We implement the semi-fungible liquid staking system and validator bond factor system within the cosmos sdk. Though registered as fungible assets, these tokenized shares have extremely limited fungibility, only among the specific delegation record that was created when shares were tokenized. These assets can be used for OTC trades but composability with DeFi is limited. The primary expected use case is improving the user experience of liquid staking providers. + +The LSM is designed to safely and efficiently facilitate the adoption of liquid staking. + +The LSM mitigates liquid staking risks by limiting the total amount of tokens that can be liquid staked to X% of all staked tokens (in the case of the Cosmos Hub, 25% as decided by Governance). + +As additional risk-mitigation features, the LSM introduces a requirement that validators self-bond tokens to be eligible for delegations from liquid staking providers, and that the portion of their liquid staked shares must not exceed X% of their total shares (50% on the Cosmos Hub). -A new governance parameter is introduced that defines the ratio of exempt to issued tokenized shares. This is called the exemption factor. A larger exemption factor allows more tokenized shares to be issued for a smaller amount of exempt delegations. If governance is comfortable with how the liquid staking market is evolving, it makes sense to increase this value. +A new governance parameter is introduced that defines the ratio of validator bonded tokens to issued tokenized shares. This is called the _validator bond factor_. A larger validator bond factor allows more tokenized shares to be issued for a smaller amount of validator bond. If governance is comfortable with how the liquid staking market is evolving, it makes sense to increase this value. -Min self delegation is removed from the staking system with the expectation that it will be replaced by the exempt delegations system. The exempt delegation system allows multiple accounts to demonstrate economic alignment with the validator operator as team members, partners etc. without co-mingling funds. Delegation exemption will likely be required to grow the validators' business under widespread adoption of liquid staking once governance has adjusted the exemption factor. +Min self delegation is removed from the staking system with the expectation that it will be replaced by the validator bond system. The validator bond system allows multiple accounts to demonstrate economic alignment with the validator operator as team members, partners etc. without co-mingling funds. Validator bonding will likely be required to grow the validators' business under widespread adoption of liquid staking once governance has adjusted the validator bond factor. -When shares are tokenized, the underlying shares are transfered to a module account and rewards go to the module account for the TokenizedShareRecord. +When shares are tokenized, the underlying shares are transferred to a module account and rewards go to the module account for the TokenizedShareRecord. There is no longer a mechanism to override the validators vote for TokenizedShares. +Delegations from 32-length addresses and LSM tokenized shares are tracked against the global liquid staking, validator liquid staking cap, and validator bond caps. This requires changing the standard staking transactions to track these variables and ensure safety limits are enforced. The reason for checking the account type is because ICAs and tokenize share record module accounts have 32-length addresses, so in practice this limits liquid staking. To be clear, any ICA or module account staking is counted against this cap - not just ICA delegations from liquid staking providers. -### `MsgTokenizeShares` +### Limiting liquid staking -The MsgTokenizeShares message is used to create tokenize delegated tokens. This message can be executed by any delegator who has positive amount of delegation and after execution the specific amount of delegation disappear from the account and share tokens are provided. Share tokens are denominated in the validator and record id of the underlying delegation. -A user may tokenize some or all of their delegation. +The LSM would limit the percentage of liquid staked tokens by all liquid staking providers to 25% of the total supply of staked tokens. For example, if 100M tokens were currently staked, and if the LSM were installed today then the total liquid staked supply would be limited to a maximum of 25M tokens. -They will receive shares with the denom of `cosmosvaloper1xxxx/5` where 5 is the record id for the validator operator. +This is a key safety feature, as it would prevent liquid staking providers from collectively controlling more than ⅓ of the total staked token supply, which is the threshold at which a group of bad actors could halt block production. -MsgTokenizeShares fails if the account is a VestingAccount. Users will have to move vested tokens to a new account and endure the unbonding period. We view this as an acceptable tradeoff vs. the complex book keeping required to track vested tokens. +Additionally, a separate cap is enforced on each validator's portion of liquid staked shares. Once X% of shares (on the Cosmoshub, 50% based on the parameter value chosen by governance) are liquid, the validator is unable to accept additional liquid stakes. -The total amount of outstanding tokenized shares for the validator is checked against the sum of exempt delegations multiplied by the exemption factor. If the tokenized shares exceeds this limit, execution fails. +Technically speaking, this cap on liquid staked tokens is enforced by limiting the total number of tokens that can be staked via interchain accounts plus the number of tokens that can be tokenized using LSM. Once this joint cap is reached, the LSM prevents interchain accounts from staking any more tokens and prevents tokenization of delegations using LSM. + +Note that the limit of the percentage of liquid staked tokens will not fully hold if the total stake is dropping. As an example, a 25% cap leaves room for over 33% of the non-LS ATOM to unbond before the share of voting power held by liquid staking providers would reach 33%. For example, say there are 100 ATOM total staked, 25 of which are liquid staked; 25 of the 75 remaining ATOM need to unbond for the liquid staked voting power to rise to 33%. + + +### Validator bond + +As an additional security feature, validators who want to receive delegations from liquid staking providers would be required to self-bond a certain amount of tokens. The validator self-bond, or “validator-bond,” means that validators need to have “skin in the game” in order to be entrusted with delegations from liquid staking providers. This disincentivizes malicious behavior and enables the validator to negotiate its relationship with liquid staking providers. + +Technically speaking, the validator-bond is tracked by the LSM. The maximum number of tokens that can be delegated to a validator by a liquid staking provider is equal to the validator-bond multiplied by the “validator-bond factor.” The initial validator bond factor would be set at 250, but can be configured by governance. + +With a validator-bond factor of 250, for every 1 token a validator self-bonds, that validator is eligible to receive up to two-hundred-and-fifty tokens delegated from liquid staking providers. The validator-bond has no impact on anything other than eligibility for delegations from liquid staking providers. + +Without self-bonding tokens, a validator can’t receive delegations from liquid staking providers. And if a validator’s maximum amount of delegated tokens from liquid staking providers has been met, it would have to self-bond more tokens to become eligible for additional liquid staking provider delegations. + +### Instantly liquid staking tokens that are already staked + +Next, let’s discuss how the LSM makes the adoption of liquid staking more efficient, and can help the blockchain that installs it build strong relationships with liquid staking providers. The LSM enables users to instantly liquid stake their staked tokens, without having to wait the unbonding period. This is important, because a very large portion of the token supply on most Cosmos blockchains is currently staked. Liquid staking tokens that are already staked incur a switching cost in the form of forfeited staking rewards over the chain's unbonding period. The LSM eliminates this switching cost. -MsgTokenizeSharesResponse provides the number of tokens generated and their denom. +A user would be able to visit any liquid staking provider that has integrated with the LSM and click a button to convert his staked tokens to liquid staked tokens. It would be as easy as liquid staking unstaked tokens. -### `MsgRedeemTokensforShares` +Technically speaking, this is accomplished by using something called an “LSM share.” Using the liquid staking module, a user can tokenize their staked tokens and turn it into LSM shares. LSM shares can be redeemed for underlying staked tokens and are transferable. After staked tokens are tokenized they can be immediately transferred to a liquid staking provider in exchange for liquid staking tokens - without having to wait for the unbonding period. -The MsgRedeemTokensforShares message is used to redeem the delegation from share tokens. This message can be executed by any user who owns share tokens. After execution delegations will appear to the user. +### LSM share token -### `MsgTransferTokenizeShareRecord` +When tokenizing a delegation, the returned token has a denom of the format `{validatorAddress}/{recordId}`, where `recordId` is a monotonically increasing number that increments every tokenization. As a result, two successive tokenizations to the same validator will yield different denom's. +Additionally, the share tokens returned will map 1:1 with the number of shares of the underlying delegation (e.g. if the delegation of X shares is tokenized, X share tokens be returned). This reduces ambiguity with respect to the value of the token if a slash occurs after tokenization. -The MsgTransferTokenizeShareRecord message is used to transfer the ownership of rewards generated from the tokenized amount of delegation. The tokenize share record is created when a user tokenize his/her delegation and deleted when the full amount of share tokens are redeemed. +### Toggling the ability to tokenize shares -This is designed to work with liquid staking designs that do not redeem the tokenized shares and may instead want to keep the shares tokenized. +Currently LSM facilitates the immediate conversion of staked assets into liquid staked tokens (referred to as "tokenization"). Despite the many benefits that come with this capability, it does inadvertently negate a protective measure available via traditional staking, where an account can stake their tokens to render them illiquid in the event that their wallet is compromised (the attacker would first need to unbond, then transfer out the tokens). +LSM would obviate this safety measure, as an attacker could tokenize and immediately transfer staked tokens to another wallet. So, as an additional protective measure, this proposal incorporates a feature to permit accounts to selectively disable the tokenization of their stake. -### `MsgExemptDelegation` +The LSM grants the ability to enable and disable the ability to tokenizate their stake. When tokenization is disabled, a lock is placed on the account, effectively preventing the conversion of any of their delegations. Re-enabling tokenization would initiate the removal of the lock, but the process is not immediate. The lock removal is queued, with the lock itself persisting throughout the unbonding period. Following the completion of the unbonding period, the lock would be completely removed, restoring the account's ablility to tokenize. For LST protocols that enable the lock, this delay better positions the base layer to coordinate a recovery in the event of an exploit. -The MsgExemptDelegation message is used to exempt a delegation to a validator. If the exemption factor is greater than 0, this will allow more delegation shares to be issued from the validator. +## Economics -This design allows the chain to force an amount of self-delegation by validators participating in liquid staking schemes. +We expect that eventually governance may decide that the principal agent problems between validators and liquid staking are resolved through the existence of mature liquid staking synthetic asset systems and their associate risk framework. Governance can effectively disable the feature by setting the scalar value to -1 and allow unlimited minting and all liquid delegations to be freely undelegated. -## Consequences +During the transitionary period, this creates a market for liquid shares that may serve to help further decentralize the validator set. -### Backwards Compatibility +It also allows multiple participants in a validator business to hold their personal stakes in segregated accounts but all collectively contribute towards demonstrating alignment with the safety of the protocol. -By setting the exemption factor to zero, this module works like legacy staking. The only substantial change is the removal of min-self-bond and without any tokenized shares, there is no incentive to exempt delegation. +## Instructions for validators +Once delegated to a validator, a delegator (or validator operator) can convert their delegation to a validator into Validator Bond by signing a ValidatorBond message. -### Positive +The ValidatorBond message is exposed by the staking module and can be executed as follows: +``` +gaiad tx staking validator-bond cosmosvaloper13h5xdxhsdaugwdrkusf8lkgu406h8t62jkqv3h --from mykey +``` +There are no partial Validator Bonds: when a delegator or validator converts their shares to a particular validator into Validator Bond, their entire delegation to that validator is converted to Validator Bond. If a validator or delegator wishes to convert only some of their delegation to Validator Bond, they should transfer those funds to a separate address and Validator Bond from that address, or redelegate the funds that they do not wish to validator bond to another validator before converting their delegation to validator bond. + +To convert Validator Bond back into a standard delegation, simply unbond the shares. + +## Technical Spec: + +### Software parameters + +New governance parameters are introduced that define the cap on the percentage of delegated shares than can be liquid, namely the `GlobalLiquidStakingCap` and `ValidatorLiquidStakingCap`. The `ValidatorBondFactor` governance parameter defines the number of tokens that can be liquid staked, relative to a validator's validator bond. + +```proto +// Params defines the parameters for the staking module. +message Params { + // ... existing params... + // validator_bond_factor is required as a safety check for tokenizing shares and + // delegations from liquid staking providers + string validator_bond_factor = 7 [ + (gogoproto.moretags) = "yaml:\"validator_bond_factor\"", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + // global_liquid_staking_cap represents a cap on the portion of stake that + // comes from liquid staking providers + string global_liquid_staking_cap = 8 [ + (gogoproto.moretags) = "yaml:\"global_liquid_staking_cap\"", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + // validator_liquid_staking_cap represents a cap on the portion of stake that + // comes from liquid staking providers for a specific validator + string validator_liquid_staking_cap = 9 [ + (gogoproto.moretags) = "yaml:\"validator_liquid_staking_cap\"", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; +} +``` + +### Data structures + +#### Validator +The `ValidatorBondShares` and `LiquidShares` attributes were added to the `Validator` struct. + +```proto +message Validator { + // ...existing attributes... + // Number of shares self bonded from the validator + string validator_bond_shares = 11 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; + // Number of shares either tokenized or owned by a liquid staking provider + string liquid_shares = 12 [ + (cosmos_proto.scalar) = "cosmos.Dec", + (gogoproto.customtype) = "github.com/cosmos/cosmos-sdk/types.Dec", + (gogoproto.nullable) = false + ]; +} +``` + +#### Delegation +The `ValidatorBond` attribute was added to the `Delegation` struct. + +```proto +// Delegation represents the bond with tokens held by an account. It is +// owned by one delegator, and is associated with the voting power of one +// validator. +message Delegation { + // ...existing attributes... + // has this delegation been marked as a validator self bond. + bool validator_bond = 4; +} +``` + +#### Toggling the ability to tokenize shares +```proto +// PendingTokenizeShareAuthorizations stores a list of addresses that have their +// tokenize share re-enablement in progress +message PendingTokenizeShareAuthorizations { + repeated string addresses = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} +// Prevents an address from tokenizing any of their delegations +message MsgDisableTokenizeShares { + string delegator_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} + +// EnableTokenizeShares begins the re-allowing of tokenizing shares for an address, +// which will complete after the unbonding period +// The time at which the lock is completely removed is returned in the response +message MsgEnableTokenizeShares { + string delegator_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; +} +``` + + +### Tracking total liquid stake +To monitor the progress towards the global liquid staking cap, the module needs to know two things: the total amount of staked tokens and the total amount of *liquid staked* tokens. The total staked tokens can be found by checking the balance of the "Bonded" pool. The total *liquid staked* tokens are stored separately and can be found under the `TotalLiquidStakedTokensKey` prefix (`[]byte{0x65}`). The value is managed by the following keeper functions: +```go +func (k Keeper) SetTotalLiquidStakedTokens(ctx sdk.Context, tokens sdk.Dec) +func (k Keeper) GetTotalLiquidStakedTokens(ctx sdk.Context) sdk.Dec +``` + +### Tokenizing shares + +The MsgTokenizeShares message is used to create tokenize delegated tokens. This message can be executed by any delegator who has positive amount of delegation and after execution the specific amount of delegation disappear from the account and share tokens are provided. Share tokens are denominated in the validator and record id of the underlying delegation. + +A user may tokenize some or all of their delegation. + +They will receive shares with the denom of `cosmosvaloper1xxxx/5` where 5 is the record id for the validator operator. + +MsgTokenizeShares fails if the account is a VestingAccount and the user does not have enough free delegation to complete the tokenization. + +The total amount of outstanding tokenized shares for the validator is checked against the sum of validator bond delegations multiplied by the validator bond factor. If the tokenized shares exceeds this limit, execution fails. + +MsgTokenizeSharesResponse provides the number of tokens generated and their denom. -This approach should enable integration with liquid staking providers and improved user experience. It provides a pathway to security under non-exponential issuance policies in the baseline staking module. \ No newline at end of file +### Helper functions +In order to identify whether a liquid stake transaction will exceed either the global liquid staking cap or the validator bond cap, the following functions were added: + +```go +// Check if an account is a owned by a liquid staking provider +// Checking for a 32-length address will capture +// ICA accounts, as well as tokenized delegations which are owned by module accounts under the hood +// which will identify the following scenarios: +// - An account has tokenized their shares, and thus the delegation is +// owned by the tokenize share record module account +// - A liquid staking provider is delegating through an ICA account +// +// Both ICA accounts and tokenize share record module accounts have 32-length addresses +func (k Keeper) DelegatorIsLiquidStaker(address sdk.AccAddress) bool + +// SafelyIncreaseTotalLiquidStakedTokens increments the total liquid staked tokens +// if the caps are enabled and the global cap is not surpassed by this delegation +func (k Keeper) SafelyIncreaseTotalLiquidStakedTokens(ctx sdk.Context, amount sdk.Int) error + +// DecreaseTotalLiquidStakedTokens decrements the total liquid staked tokens +// if the caps are enabled +func (k Keeper) DecreaseTotalLiquidStakedTokens(ctx sdk.Context, amount sdk.Int) error + +// SafelyIncreaseValidatorLiquidShares increments the liquid shares on a validator +// if the caps are enabled and the validator bond cap is not surpassed by this delegation +func (k Keeper) SafelyIncreaseValidatorLiquidShares(ctx sdk.Context, validator types.Validator, shares sdk.Dec) error + +// DecreaseValidatorLiquidShares decrements the liquid shares on a validator +// if the caps are enabled +func (k Keeper) DecreaseValidatorLiquidShares(ctx sdk.Context, validator types.Validator, shares sdk.Dec) error + +// SafelyDecreaseValidatorBond decrements the validator's self bond +// so long as it will not cause the current delegations to exceed the threshold +// set by validator bond factor +func (k Keeper) SafelyDecreaseValidatorBond(ctx sdk.Context, validator types.Validator, shares sdk.Dec) error +``` + +### Accounting +Tracking the total liquid stake and total liquid validator shares requires additional accounting changes in the following transactions/events: + +```go +func Delegate() { + ... + // If delegator is a liquid staking provider + // Increment total liquid staked + // Increment validator liquid shares +} + +func Undelegate() { + ... + // If delegator is a liquid staking provider + // Decrement total liquid staked + // Decrement validator liquid shares +} + +func BeginRedelegate() { + ... + // If delegator is a liquid staking provider + // Decrement source validator liquid shares + // Increment destination validator liquid shares +} + +func TokenizeShares() { + ... + // If delegator is a NOT liquid staking provider (otherwise the shares are already included) + // Increment total liquid staked + // Increment validator liquid shares +} + +func RedeemTokens() { + ... + // If delegator is a NOT liquid staking provider + // Decrement total liquid staked + // Decrement validator liquid shares +} + +func Slash() { + ... + // Decrement's the total liquid staked tokens + // The total should be adjusted by slash amount * liquid percentage + // Since a slash only modifies a validator's tokens and not their shares, + // the validator's LiquidShares do not have to be changed during a slash +} +``` + +### Transaction failure cases +With the liquid staking caps in consideration, there are additional scenarios that should cause a transaction to fail: +```go + +func Delegate() { + ... + // If delegator is a liquid staking provider + // Fail transaction if delegation exceeds global liquid staking cap + // Fail transaction if delegation exceeds validator liquid staking cap + // Fail transaction if delegation exceeds validator bond cap +} + +func Undelegate() { + ... + // If the unbonded delegation is a ValidatorBond + // Fail transaction if the reduction in validator bond would cause the + // existing liquid delegation to exceed the cap +} + +func BeginRedelegate() { + ... + // If the delegation is a ValidatorBond + // Fail transaction if the reduction in validator bond would cause the + // existing liquid delegation to exceed the cap + + // If delegator is a liquid staking provider + // Fail transaction if delegation exceeds global liquid staking cap + // Fail transaction if delegation exceeds validator liquid staking cap + // Fail transaction if delegation exceeds validator bond cap +} + +func TokenizeShares() { + ... + // If the delegation is a ValidatorBond + // Fail transaction - ValidatorBond's cannot be tokenized + + // If the sender is NOT a liquid staking provider + // Fail transaction if tokenized shares would exceed the global liquid staking cap + // Fail transaction if tokenized shares would exceed the validator liquid staking cap + // Fail transaction if tokenized shares would exceed the validator bond cap +} +``` + +### Bootstrapping total liquid stake +When upgrading to enable the liquid staking module, the total global liquid stake and total liquid validator shares must be determined. This can be done in the upgrade handler by looping through delegation records and including the delegation in the total if the delegator has a 32-length address. This is implemented by the following function: +```go +func RefreshTotalLiquidStaked() { + // Resets all validator LiquidShares to 0 + // Loops delegation records + // For each delegation, determines if the delegation was from a 32-length address + // If so, increments the global liquid staking cap and validator liquid shares +} +``` + +### Toggling the ability to tokenize shares + +```go +// Adds a lock that prevents tokenizing shares for an account +// The tokenize share lock store is implemented by keying on the account address +// and storing a timestamp as the value. The timestamp is empty when the lock is +// set and gets populated with the unlock completion time once the unlock has started +func AddTokenizeSharesLock(address sdk.AccAddress) + +// Removes the tokenize share lock for an account to enable tokenizing shares +func RemoveTokenizeSharesLock(address sdk.AccAddress) + +// Updates the timestamp associated with a lock to the time at which the lock expires +func SetTokenizeShareUnlockTime(address sdk.AccAddress, completionTime time.Time) + +// Checks if there is currently a tokenize share lock for a given account +// Returns a bool indicating if the account is locked, as well as the unlock time +// which may be empty if an unlock has not been initiated +func IsTokenizeSharesDisabled(address sdk.AccAddress) (disabled bool, unlockTime time.Time) + +// Stores a list of addresses pending tokenize share unlocking at the same time +func SetPendingTokenizeShareAuthorizations(completionTime time.Time, authorizations types.PendingTokenizeShareAuthorizations) + +// Returns a list of addresses pending tokenize share unlocking at the same time +func GetPendingTokenizeShareAuthorizations() PendingTokenizeShareAuthorizations + +// Inserts the address into a queue where it will sit for 1 unbonding period +// before the tokenize share lock is removed +// Returns the completion time +func QueueTokenizeSharesAuthorization(address sdk.AccAddress) time.Time + +// Unlocks all queued tokenize share authorizations that have matured +// (i.e. have waited the full unbonding period) +func RemoveExpiredTokenizeShareLocks(blockTime time.Time) (unlockedAddresses []string) +``` + +## References + +Please see this document for a technical spec for the LSM: https://docs.google.com/document/d/1WYPUHmQii4o-q2225D_XyqE6-1bvM7Q128Y9amqRwqY/edit#heading=h.zcpx47mn67kl \ No newline at end of file diff --git a/x/staking/README.md b/x/staking/README.md index d9637813dbba..b6ce0bbdbcd0 100644 --- a/x/staking/README.md +++ b/x/staking/README.md @@ -32,6 +32,8 @@ network. * [Redelegation](#redelegation) * [Queues](#queues) * [HistoricalInfo](#historicalinfo) + * [TotalLiquidStakedTokens](#totalliquidstakedtokens) + * [PendingTokenizeShareAuthorizations](#pendingtokenizeshareauthorizations) * [State Transitions](#state-transitions) * [Validators](#validators) * [Delegations](#delegations) @@ -45,6 +47,13 @@ network. * [MsgCancelUnbondingDelegation](#msgcancelunbondingdelegation) * [MsgBeginRedelegate](#msgbeginredelegate) * [MsgUpdateParams](#msgupdateparams) + * [MsgTokenizeShares](#msgtokenizeshares) + * [MsgRedeemTokensForShares](#msgredeemtokensforshares) + * [MsgTransferTokenizeShareRecord](#msgtransfertokenizesharerecord) + * [MsgEnableTokenizeShares](#msgenabletokenizeshares) + * [MsgDisableTokenizeShares](#msgdisabletokenizeshares) + * [MsgUnbondValidator](#msgunbondvalidator) + * [MsgValidatorBond](#msgvalidatorbond) * [Begin-Block](#begin-block) * [Historical Info Tracking](#historical-info-tracking) * [End-Block](#end-block) @@ -150,6 +159,10 @@ where `Jailed` is true are not stored within this index. last-block's bonded validators. This index remains constant during a block but is updated during the validator set update process which takes place in [`EndBlock`](#end-block). +`ValidatorBondShares` is the number of shares self bonded from the validator. + +`LiquidShares` is the number of shares either tokenized or owned by a liquid staking provider. + Each validator's state is stored in a `Validator` struct: ```protobuf reference @@ -328,6 +341,22 @@ they are in a deterministic order. The oldest HistoricalEntries will be pruned to ensure that there only exist the parameter-defined number of historical entries. + +### TotalLiquidStakedTokens + +TotalLiquidStakedTokens stores the total liquid staked tokens monitoring the progress towards the `GlobalLiquidStakingCap`. + +* TotalLiquidStakedTokens: `0x85 -> math.Int`. + + +### PendingTokenizeShareAuthorizations + +PendingTokenizeShareAuthorizations stores a queue of addresses that have their tokenize share re-enablement/unlocking in progress. When an address is enqueued, it will sit for 1 unbonding period before the tokenize share lock is removed. + +```go reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/staking.proto#L417-L421 +``` + ## State Transitions ### Validators @@ -396,6 +425,8 @@ When a delegation occurs both the validator and the delegation objects are affec * transfer the `delegation.Amount` from the delegator's account to the `BondedPool` or the `NotBondedPool` `ModuleAccount` depending if the `validator.Status` is `Bonded` or not * delete the existing record from `ValidatorByPowerIndex` * add an new updated record to the `ValidatorByPowerIndex` +* if the delegator is a liquid staking provider, + increment the `TotalLiquidStakedTokens` and the validator's `LiquidShares`. #### Begin Unbonding @@ -414,6 +445,7 @@ Delegation may be called. * get a unique `unbondingId` and map it to the `UnbondingDelegationEntry` in `UnbondingDelegationByUnbondingId` * call the `AfterUnbondingInitiated(unbondingId)` hook * add the unbonding delegation to `UnbondingDelegationQueue` with the completion time set to `UnbondingTime` +* if delegator is a liquid staking provider, decrement the `TotalLiquidStakedTokens` and the validator's `LiquidShares`. #### Cancel an `UnbondingDelegation` Entry @@ -442,6 +474,8 @@ Redelegations affect the delegation, source and destination validators. * otherwise, if the `sourceValidator.Status` is not `Bonded`, and the `destinationValidator` is `Bonded`, transfer the newly delegated tokens from the `NotBondedPool` to the `BondedPool` `ModuleAccount` * record the token amount in an new entry in the relevant `Redelegation` +* if the delegator is a liquid staking provider, + decrement the source validator's `LiquidShares` increment destination validator's `LiquidShares` From when a redelegation begins until it completes, the delegator is in a state of "pseudo-unbonding", and can still be slashed for infractions that occurred before the redelegation began. @@ -466,6 +500,7 @@ When a Validator is slashed, the following occurs: total slash amount. * The `remaingSlashAmount` is then slashed from the validator's tokens in the `BondedPool` or `NonBondedPool` depending on the validator's status. This reduces the total supply of tokens. +* Decrements the `TotalLiquidStakedTokens` by slash amount * liquid percentage. In the case of a slash due to any infraction that requires evidence to submitted (for example double-sign), the slash occurs at the block where the evidence is included, not at the block where the infraction occured. @@ -580,11 +615,18 @@ This message is expected to fail if: * the `Amount` `Coin` has a denomination different than one defined by `params.BondDenom` * the exchange rate is invalid, meaning the validator has no tokens (due to slashing) but there are outstanding shares * the amount delegated is less than the minimum allowed delegation +* the delegator is a liquid staking provider and the delegation exceeds +either the `GlobalLiquidStakingCap`, the `ValidatorLiquidStakingCap` or the validator bond cap. If an existing `Delegation` object for provided addresses does not already exist then it is created as part of this message otherwise the existing `Delegation` is updated to include the newly received shares. +If the delegation if is a validator bond, the `ValidatorBondShares` of the validator is increased. + +If the delegator is a liquid staking provider, the `TotalLiquidStakedTokens` +and the validator `LiquidShares` are incremented. + The delegator receives newly minted shares at the current exchange rate. The exchange rate is the number of existing shares in the validator divided by the number of currently delegated tokens. @@ -623,9 +665,15 @@ This message is expected to fail if: * the delegation has less shares than the ones worth of `Amount` * existing `UnbondingDelegation` has maximum entries as defined by `params.MaxEntries` * the `Amount` has a denomination different than one defined by `params.BondDenom` +* the unbonded delegation is a `ValidatorBond` and the reduction in validator bond would cause the existing liquid delegation to exceed the cap. When this message is processed the following actions occur: +* if the delegation is a validator bond, the `ValidatorBondShares` of the validator is decreased. + +* if the delegator is a liquid staking provider, the `TotalLiquidStakedTokens` +and the validator's `LiquidShares` are decreased. + * validator's `DelegatorShares` and the delegation's `Shares` are both reduced by the message `SharesAmount` * calculate the token worth of the shares remove that amount tokens held within the validator * with those removed tokens, if the validator is: @@ -690,9 +738,15 @@ This message is expected to fail if: * the source validator has a receiving redelegation which is not matured (aka. the redelegation may be transitive) * existing `Redelegation` has maximum entries as defined by `params.MaxEntries` * the `Amount` `Coin` has a denomination different than one defined by `params.BondDenom` +* the delegation is a `ValidatorBond` and the reduction in validator bond would cause the existing liquid delegation to exceed the cap. +* the delegator is a liquid staking provider and the delegation exceeds +either the `GlobalLiquidStakingCap`, the `ValidatorLiquidStakingCap` or the validator bond cap. When this message is processed the following actions occur: +* if the delegation if is a validator bond, the `ValidatorBondShares` of the source validator is decreased. +* if the delegator is a liquid staking provider, + the source validator's `LiquidShares` increased and the destination validator's `LiquidShares` is decreased. * the source validator's `DelegatorShares` and the delegations `Shares` are both reduced by the message `SharesAmount` * calculate the token worth of the shares remove that amount tokens held within the source validator. * if the source validator is: @@ -705,6 +759,201 @@ When this message is processed the following actions occur: ![Begin redelegation sequence](https://raw.githubusercontent.com/cosmos/cosmos-sdk/release/v0.46.x/docs/uml/svg/begin_redelegation_sequence.svg) +## MsgTokenizeShares + +The `MsgTokenizeShares` message allows users to tokenize their delegated tokens. Share tokens have denom using the validator address and record id of the underlying delegation with the format `{validatorAddress}/{recordId}`. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L49-L50 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L190-L199 +``` + +This message returns a response containing the number of tokens generated: + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L201-L204 +``` + +This message is expected to fail if: + +* the delegation is a `ValidatorBond` +* the delegator sender's address has disabled tokenization, meaning that the account +lock status is either `LOCKED` or `LOCK_EXPIRING`. +* the account is a vesting account and the free delegation (non-vesting delegation) is exceeding the tokenized share amount. +* the sender is NOT a liquid staking provider and the tokenized shares exceeds +either the `GlobalLiquidStakingCap`, the `ValidatorLiquidStakingCap` or the validator bond cap. + + +When this message is processed the following actions occur: + +* If delegator is a NOT liquid staking provider (otherwise the shares are already included) + * Increment the `GlobalLiquidStakingCap` + * Increment the validator's `ValidatorLiquidStakingCap` +* Unbond the delegation shares and transfer the coins back to delegator +* Create an equivalent amount of tokenized shares that the initial delegation shares +* Mint the liquid coins and send them to delegator +* Create a tokenized share record +* Get validator to whom the sender delegated his shares +* Send coins to module address and delegate them to the validator + +## MsgRedeemTokensForShares + +The `MsgRedeemTokensForShares` message allows users to redeem their native delegations from share tokens. + + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L52-L54 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L206-L213 +``` + +This message returns a response containing the amount of staked tokens redeemed: + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L215-L218 +``` + +This message is expected to fail if: + +* if the sender's balance doesn't have enough liquid tokens + + +When this message is processed the following actions occur: + +* Get the tokenized shares record +* Get the validator that issued the tokenized shares from the record +* Unbond the delegation associated with the tokenized shares +* The delegator is NOT a liquid staking provider: + * Decrease the `ValidatorLiquidStakingCap` + * Decrease the validator's `LiquidShares` +* Burn the liquid coins equivalent of the tokenized shares +* Delete the tokenized shares record +* Send equivalent amount of tokens to the delegator +* Delegate sender's tokens to the validator + +## MsgTransferTokenizeShareRecord + +The `MsgTransferTokenizeShareRecord` message enables users to transfer the ownership of rewards generated from the tokenized amount of delegation. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L56-L58 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L220-L228 +``` + +This message is expected to fail if: + +* the tokenized shares record doesn't exist +* the sender address doesn't match the owner address in the record + +When this message is processed the following actions occur: + +* the tokenized shares record is updated with the new owner address + +## MsgEnableTokenizeShares + +The `MsgEnableTokenizeShares` message begins the countdown after which tokenizing shares by the sender delegator address is re-allowed, which will complete after the unbonding period. + + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L63-L65 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L244-L250 +``` + +This message returns a response containing the time at which the lock is completely removed: + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L252-L255 +``` + +This message is expected to fail if: + +* if the sender's account lock status is either equal to `UNLOCKED` or `LOCK_EXPIRING`, +meaning that the tokenized shares aren't currently disabled. + + +When this message is processed the following actions occur: + +* queue the unlock authorization. + +## MsgDisableTokenizeShares + +The `MsgDisableTokenizeShares` message prevents the sender delegator address from tokenizing any of its delegations. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L60-L61 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L233-L239 +``` + +This message is expected to fail if: + +* the sender's account already has the `LOCKED` lock status + + +When this message is processed the following actions occur: + +* if the sender's account lock status is equal to `LOCK_EXPIRING`, +it cancels the pending unlock authorizations by removing them from the queue. +* Create a new tokenization lock for the sender's account. Note that +if there is a lock expiration in progress, it is overridden. + +## MsgValidatorBond + +The `MsgValidatorBond` message designates a delegation as a validator bond. +It enables validators to receive more liquid staking delegations + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L67-L68 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L257-L265 +``` + +This message is expected to fail if: + +* the delegator is a liquid staking provider + +When this message is processed the following actions occur: + +* If the delegation is not already a `ValidatorBond`: + * Enable the delegation's `ValidatorBond` flag + * Update validator's `ValidatorBondShares` + +## MsgUnbondValidator + +The `MsgTransferTokenizeShareRecord` message allows validator to change their +status from transfers from `Bonded` to `Unbonding` without experiencing slashing. + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L36-L38 +``` + +```protobuf reference +https://github.com/cosmos/cosmos-sdk/blob/v0.45.16-ics-lsm/proto/cosmos/staking/v1beta1/tx.proto#L165-L169 +``` + +This message is expected to fail if: + +* the validator isn't registered or is already jailed + +When this message is processed the following actions occur: + +* the validator is jailed +* the validator status changes from `Bonded` to `Unbonding` ### MsgUpdateParams @@ -927,18 +1176,91 @@ The staking module emits the following events: * [0] Time is formatted in the RFC3339 standard +### MsgTokenizeShares + +| Type | Attribute Key | Attribute Value | +| ----------------------------- | --------------------- | ----------------------- | +| tokenize_shares | delegator | {delegatorAddress} | +| tokenize_shares | validator | {validatorAddress} | +| tokenize_shares | share_owner | {shareOwnerAddress} | +| tokenize_shares | amount | {tokenizeAmount} | +| message | module | staking | +| message | action | tokenize_share | +| message | sender | {senderAddress} | + +### MsgRedeemTokensForShares + +| Type | Attribute Key | Attribute Value | +| ----------------------------- | ------------------ | ------------------------ | +| redeem_tokens_for_shares | delegator | {delegatorAddress} | +| redeem_tokens_for_shares | amount | {redeemAmount} | +| message | module | staking | +| message | action | redeem_tokens | +| message | sender | {senderAddress} | + +### MsgTransferTokenizeShareRecord + +| Type | Attribute Key | Attribute Value | +| ---------------------------------- | ---------------------------- | ----------------------------------- | +| transfer_tokenize_share_record | share_record_id | {shareRecordID} | +| transfer_tokenize_share_record | sender | {senderAddress} | +| transfer_tokenize_share_record | share_owner | {newShareOwnerAddress} | +| message | module | staking | +| message | action | transfer-tokenize-share-record | +| message | sender | {senderAddress} | + +### MsgEnableTokenizeShares + +| Type | Attribute Key | Attribute Value | +| ----------------------------- | ------------------- | ------------------------ | +| enable_tokenize_shares | delegator | {delegatorAddress} | +| message | module | staking | +| message | action | enable_tokenize_shares | +| message | sender | {senderAddress} | + +### MsgDisableTokenizeShares + +| Type | Attribute Key | Attribute Value | +| ----------------------------- | ------------------ | ------------------------ | +| disable_tokenize_shares | delegator | {delegatorAddress} | +| message | module | staking | +| message | action | disable_tokenize_shares | +| message | sender | {senderAddress} | + +### MsgValidatorBond + +| Type | Attribute Key | Attribute Value | +| -------------------- | --------------------- | ------------------------ | +| validator_bond | validator | {validatorAddress} | +| validator_bond | delegator | {delegatorAddress} | +| message | action | validator_bond | +| message | sender | {senderAddress} | + +### MsgUnbondValidator + +| Type | Attribute Key | Attribute Value | +| -------------------- | --------------------- | ------------------------ | +| unbound_validator | validator | {validatorAddress} | +| message | action | unbond_validator | +| message | sender | {senderAddress} | + + ## Parameters The staking module contains the following parameters: -| Key | Type | Example | -|-------------------|------------------|------------------------| -| UnbondingTime | string (time ns) | "259200000000000" | -| MaxValidators | uint16 | 100 | -| KeyMaxEntries | uint16 | 7 | -| HistoricalEntries | uint16 | 3 | -| BondDenom | string | "stake" | -| MinCommissionRate | string | "0.000000000000000000" | +| Key | Type | Example | +|------------------------- |------------------|--------------------------| +| UnbondingTime | string (time ns) | "259200000000000" | +| MaxValidators | uint16 | 100 | +| KeyMaxEntries | uint16 | 7 | +| HistoricalEntries | uint16 | 3 | +| BondDenom | string | "stake" | +| MinCommissionRate | string | "0.000000000000000000" | +| ValidatorBondFactor | string | "250.0000000000000000" | +| GlobalLiquidStakingCap | string | "1.000000000000000000" | +| ValidatorLiquidStakingCap | string | "0.250000000000000000" | + ## Client