|
@@ -0,0 +1,194 @@
|
|
|
+# Staking
|
|
|
+
|
|
|
+WARNING: All code related to staking is unaudited. Use at your own risk.
|
|
|
+
|
|
|
+## Introduction
|
|
|
+
|
|
|
+The **Registry** program provides an on-chain mechanism for a group of stakers to
|
|
|
+
|
|
|
+* Share rewards proprtionally amongst a staking pool
|
|
|
+* Govern on chain protocols with stake weighted voting
|
|
|
+* Stake and earn locked tokens
|
|
|
+
|
|
|
+The program makes little assumptions about the form of stake or rewards.
|
|
|
+In the same way you can make a new SPL token with its own mint, you can create a new stake
|
|
|
+pool. Although the token being staked must be a predefined mint upon pool initialization,
|
|
|
+rewards on a particular pool can be arbitrary SPL tokens, or, in the case of locked rewards,
|
|
|
+program controlled accounts.
|
|
|
+Rewards can come from an arbitrary
|
|
|
+wallet, e.g. automatically from a fee earning program,
|
|
|
+or manually from a wallet owned by an individual. The specifics are token and protocol
|
|
|
+dependent. For example, in the case of SRM, rewards will be generated from the DEX's
|
|
|
+weekly fees, where 80% of the fees go to a buy and burn, and 20% to stakers.
|
|
|
+
|
|
|
+Similarly, the specifics of governance are not assumed by the staking program. However, a
|
|
|
+governance system can use this program as a primitive to implement stake weighted voting.
|
|
|
+
|
|
|
+Here we cover how staking works at somewhat of a low level with the goal of allowing one
|
|
|
+to understand, contribute to, or modify the code.
|
|
|
+
|
|
|
+## Accounts
|
|
|
+
|
|
|
+Accounts are the pieces of state owned by a Solana program. For reference while reading, here are all
|
|
|
+accounts used by the **Registry** program.
|
|
|
+
|
|
|
+* `Registrar` - Analagous to an SPL token `Mint`, the `Registrar` defines a staking instance. It has its own pool, and it's own set of rewards distributed amongst its own set of stakers.
|
|
|
+* `Member` - Analogous to an SPL token `Account`, `Member` accounts represent a **beneficiary**'s (i.e. a wallet's) stake state. This account has several vaults, all of which represent the funds belonging to an individual user.
|
|
|
+* `PendingWithdrawal` - A transfer out of the staking pool (poorly named since it's not a withdrawal out of the program. But a withdrawal out of the staking pool and into a `Member`'s freely available balances).
|
|
|
+* `RewardVendor` - A reward that has been dropped onto stakers and is distributed pro rata to staked `Member` beneficiaries.
|
|
|
+* `RewardEventQueue` - A ring buffer of all rewards available to stakers. Each entry is the address of a `RewardVendor`.
|
|
|
+
|
|
|
+## Creating a member account.
|
|
|
+
|
|
|
+Before being able to enter the stake pool, one must create a **Member** account with the
|
|
|
+**Registrar**, providing identity to the **Registry** program. By default, each member has
|
|
|
+four types of token vaults making up a set of balances owned by the program on behalf of a
|
|
|
+**Member**:
|
|
|
+
|
|
|
+* Available balances: a zero-interest earning token account with no restrictions.
|
|
|
+* Pending: unstaked funds incurring an unbonding timelock.
|
|
|
+* Stake: the total amount of tokens staked.
|
|
|
+* Stake pool token: the total amount of pool tokens created from staking (`stake = stake-pool-token * stake-pool-token-price`).
|
|
|
+
|
|
|
+Each of these vaults provide a unit of balance isolation unique to a **Member**.
|
|
|
+That is, although the stake program appears to provide a pooling mechanism, funds between
|
|
|
+**Member** accounts are not commingled. They do not share SPL token accounts, and the only
|
|
|
+way for funds to move is for a **Member**'s beneficiary to authorize instructions that either exit the
|
|
|
+system or move funds between a **Member**'s own vaults.
|
|
|
+
|
|
|
+## Depositing and Withdrawing.
|
|
|
+
|
|
|
+Funds initially enter and exit the program through the `Deposit` and `Withdraw` instructions,
|
|
|
+which transfer funds into and out of the **available balances** vault.
|
|
|
+As the name suggests, all funds in this vault are freely available, unrestricted, and
|
|
|
+earn zero interest. The vault is purely a gateway for funds to enter the program.
|
|
|
+
|
|
|
+## Staking.
|
|
|
+
|
|
|
+Once deposited, a **Member** beneficiary invokes the `Stake` instruction to transfer funds from
|
|
|
+their **available-balances-vault** to one's **stake-vault**, creating newly minted
|
|
|
+**stake-pool-tokens** as proof of the stake deposit. These new tokens represent
|
|
|
+one's proportional right to all rewards distributed to the staking pool and are offered
|
|
|
+by the **Registry** program at a fixed price, e.g., of 500 SRM.
|
|
|
+
|
|
|
+## Unstaking
|
|
|
+
|
|
|
+Once staked, funds cannot be immediately withdrawn. Rather, the **Registrar** will enforce
|
|
|
+a one week timelock before funds are released. Upon executing the `StartUnstake`
|
|
|
+instruction, three operations execute. 1) The given amount of stake pool tokens will be burned.
|
|
|
+2) Staked funds proportional to the stake pool tokens burned will be transferred from the
|
|
|
+**Member**'s **stake-vault** to the **Member**'s **pending-vault**. 3) A `PendingWithdrawal`
|
|
|
+account will be created as proof of the stake withdrawal, stamping the current block's
|
|
|
+`unix_timestamp` onto the account. When the timelock period ends, a **Member** can invoke the
|
|
|
+`EndUnstake` instruction to complete the transfer out of the `pending-vault` and
|
|
|
+into the `available-balances`, providing the previously printed `PendingWithdrawal`
|
|
|
+receipt to the program as proof that the timelock has passed. At this point, the exit
|
|
|
+from the stake pool is complete, and the funds are ready to be used again.
|
|
|
+
|
|
|
+## Reward Design Motivation
|
|
|
+
|
|
|
+Feel free to skip this section and jump to the **Reward Vendors** section if you want to
|
|
|
+just see how rewards work.
|
|
|
+
|
|
|
+One could imagine several ways to drop rewards onto a staking pool, each with their own downsides.
|
|
|
+Of course what you want is, for a given reward amount, to atomically snapshot the state
|
|
|
+of the staking pool and to distribute it proportionally to all stake holders. Effectively,
|
|
|
+an on chain program such as
|
|
|
+
|
|
|
+```python
|
|
|
+for account in stake_pool:
|
|
|
+ account.token_amount += total_reward * (account.stake_pool_token.amount / stake_pool_token.supply)
|
|
|
+ ```
|
|
|
+
|
|
|
+Surprisingly, such a mechanism is not immediately obvious.
|
|
|
+
|
|
|
+First, the above program is a non starter. Not only does the SPL token
|
|
|
+program not have the ability to iterate through all accounts for a given mint within a program,
|
|
|
+but, since Solana transactions require the specification of all accounts being accessed
|
|
|
+in a transaction (this is how it achieves parallelism), such a transaction's size would be
|
|
|
+well over the limit. So modifying global state atomically in a single transaction is out of the
|
|
|
+question.
|
|
|
+
|
|
|
+So if you can't do this on chain, one can try doing it off chain. One could write an program to
|
|
|
+snapshot the pool state, and just airdrop tokens onto the pool. This works, but
|
|
|
+adds an additional layer of trust. Who snapshots the pool state? At what time?
|
|
|
+How do you know they calculated the rewards correctly? What happens if my reward was not given?
|
|
|
+This is not auditable or verifiable. And if you want to answer these questions, requires
|
|
|
+complex off-chain protocols that require either fancy cryptography or effectively
|
|
|
+recreating a BFT system off chain.
|
|
|
+
|
|
|
+Another solution considerered was to use a uniswap-style AMM pool (without the swapping).
|
|
|
+This has a lot of advantages. First it's easy to reason about and implement in a single transaction.
|
|
|
+To drop rewards gloablly onto the pool, one can deposit funds directly into the pool, in which case
|
|
|
+the reward is automatically received by owners of the staking pool token upon redemption, a process
|
|
|
+known as "gulping"--since dropping rewards increases the total value of the pool
|
|
|
+while their proportion of the pool remained constant.
|
|
|
+
|
|
|
+However, there are enough downsides with using an AMM style pool to offset the convience.
|
|
|
+Unfortunately, we lose the nice balance isolation property **Member** accounts have, because
|
|
|
+tokens have to be pooled into the same vault, which is an additional security concern that could
|
|
|
+easily lead to loss of funds, e.g., if there's a bug in the redemption calculation. Moreover, dropping
|
|
|
+arbitrary tokens onto the pool is a challenge. Not only do you have to create new pool vaults for
|
|
|
+every new token you drop onto the pool, but you also need to have stakers purchase those tokens to enter
|
|
|
+the pool. So not only are we staking SRM, but we're also staking other tokens. An additional oddity is that
|
|
|
+as rewards are dropped onto the pool, the price to enter the pool monotonically increases. Remember, entering this
|
|
|
+type of pool requires "creating" pool tokens, i.e., depositing enough tokens so that you don't dilute
|
|
|
+any other member. So if a single pool token represents one SRM. And if a single SRM is dropped onto every
|
|
|
+member of the pool, all the existing member's shares are now worth two SRM. So to enter the pool without
|
|
|
+dilution, one would have to "create" at a price of 2 SRM per share. This means that rewarding
|
|
|
+stakers becomes more expensive over time. One could of course solve this problem by implementing
|
|
|
+arbitrary `n:m` pool token splits, which leads us right back to the problem of mutating global account
|
|
|
+state for an SPL token.
|
|
|
+
|
|
|
+Furthermore, we haven't even touched upon dropping arbitrary program accounts as rewards, for exmaple,
|
|
|
+locked token rewards, which of course can't be dropped directly onto an AMM stylepool, since they are not tokens.
|
|
|
+So, if we did go with an AMM style pool, we'd need a separate mechanism for handling more general rewards like
|
|
|
+locked token accounts. Ideally, we'd have a single mechanism for both.
|
|
|
+
|
|
|
+## Reward Vendors
|
|
|
+
|
|
|
+Instead of trying to *push* rewards to users via a direct transfer or airdrop, we can use a *polling* model
|
|
|
+where users effectively event source a log on demand, proviidng a proof one is eligible for the reward.
|
|
|
+
|
|
|
+When a reward is created, we do two things:
|
|
|
+
|
|
|
+1) Create a **Reward Vendor** account with an associated token vault holding the reward.
|
|
|
+2) Assign the **Reward Vendor** the next available position in a **Reward Event Queue**. Then, to retrieve
|
|
|
+a reward, a staker invokes the `ClaimReward` command, providing a proof that the funds were
|
|
|
+staked at the time of the reward being dropped, and in response, the program transfers or,
|
|
|
+some might say, *vends* the proportion of the dropped reward to the polling **Member**. The
|
|
|
+operation completes by incrementing the **Member**'s queue cursor, ensuring that a given
|
|
|
+reward can only be processed once.
|
|
|
+
|
|
|
+This allows us to provide a way of dropping rewards to the stake pool in a way that is
|
|
|
+on chain and verifiable. Of course, it requires an external trigger, some account willing to
|
|
|
+transfer funds to a new **RewardVendor**, but that is outside of the scope of the staking
|
|
|
+program. The reward dropper can be an off chain BFT committee, or it can be an on-chain multisig.
|
|
|
+It can be a charitable individual, or funds can flow directly from a fee paying program such as the DEX,
|
|
|
+which itself can create a Reward Vendor from fees collected. It doesn't matter to the **Registry** program.
|
|
|
+
|
|
|
+Note that this solution also allows for rewards to be denominated in any token, not just SRM.
|
|
|
+Since rewards are paid out by the vendor immediately and to a token account of the **Member**'s
|
|
|
+choosing, it *just works*. Even more, this extends to arbitrary program accounts, particularly
|
|
|
+**Locked SRM**. A **Reward Vendor** needs to additionally know the accounts and instruction data
|
|
|
+to relay to the program, but otherwise, the mechanism is the same. The details of **Locked SRM** will
|
|
|
+be explained in an additional document.
|
|
|
+
|
|
|
+### Realizing Locked Rewards
|
|
|
+
|
|
|
+In addition to a vesting schedule, locked rewards are subject to a realization condition defined by the
|
|
|
+staking program. Specifically, locked tokens are **realized** upon completely unstaking. So if one never
|
|
|
+unstakes and incurs the unbonding timelock, one never receives locked token rewards.
|
|
|
+
|
|
|
+## Misc
|
|
|
+
|
|
|
+### Member Accounts
|
|
|
+
|
|
|
+This document describes 4 vault types belonging to **Member** accounts.
|
|
|
+However there are two types of balance groups: locked and unlocked.
|
|
|
+As a result, there are really 8 vaults for each **Member**, 4 types of vaults in 2 separate sets,
|
|
|
+each isolated from the other, so that locked tokens don't get mixed with unlocked tokens.
|
|
|
+
|
|
|
+## Future Work
|
|
|
+
|
|
|
+* Arbitrary program accounts as rewards. With the current design, it should be straightforward to generalize locked token rewards to arbitrary program accounts from arbitrary programs.
|