examples/lockup: Add some docs
This commit is contained in:
parent
4d562b0a82
commit
0d7425be65
|
@ -0,0 +1,127 @@
|
|||
# Lockups
|
||||
|
||||
WARNING: All code related to Lockups is unaudited. Use at your own risk.
|
||||
|
||||
## Introduction
|
||||
|
||||
The **Lockup** program provides a simple mechanism to lockup tokens
|
||||
of any mint, and release those funds over time as defined by a vesting schedule.
|
||||
Although these lockups track a target **beneficiary**, who will eventually receive the
|
||||
funds upon vesting, a proper deployment of the program will ensure this **beneficiary**
|
||||
can never actually retrieve tokens before vesting. Funds are *never* in an SPL
|
||||
token wallet owned by a user, and are completely program controlled.
|
||||
|
||||
## Accounts
|
||||
|
||||
There is a single account type used by the program.
|
||||
|
||||
* `Vesting` - An account defining a vesting schedule, realization condition, and vault holding the tokens to be released over time.
|
||||
|
||||
## Creating a Vesting Account
|
||||
|
||||
Lockup occurs when tokens are transferred into the program creating a **Vesting**
|
||||
account on behalf of a **beneficiary** via the `CreateVesting` instruction.
|
||||
There are three parameters to specify:
|
||||
|
||||
* Start timestamp - unix timestamp (in seconds) of the time when vesting begins.
|
||||
* End timestamp - unix timestamp (in seconds) of the time when all tokens will unlock.
|
||||
* Period count - the amount of times vesting should occur.
|
||||
* Deposit amount - the total amount to vest.
|
||||
* Realizer - the program defining if and when vested tokens can be distributed to a beneficiary.
|
||||
|
||||
Together these parameters form a linearly unlocked vesting schedule. For example,
|
||||
if one wanted to lock 100 SRM that unlocked twice, 50 SRM each time, over the next year, we'd
|
||||
use the following parameters (in JavaScript).
|
||||
|
||||
```javascript
|
||||
const startTimestamp = Date.now()/1000;
|
||||
const endTimestamp = Date.now()/1000 + 60*60*24*365;
|
||||
const periodCount = 2;
|
||||
const depositAmount = 100 * 10**6; // 6 decimal places.
|
||||
const realizer = null; // No realizer in this example.
|
||||
```
|
||||
|
||||
From these five parameters, one can deduce the total amount vested at any given time.
|
||||
|
||||
Once created, a **Vesting** account's schedule cannot be mutated.
|
||||
|
||||
## Withdrawing from a Vesting Account
|
||||
|
||||
Withdrawing is straightforward. Simply invoke the `Withdraw` instruction, specifying an
|
||||
amount to withdraw from a **Vesting** account. The **beneficiary** of the
|
||||
**Vesting** account must sign the transaction, but if enough time has passed for an
|
||||
amount to be vested, and, if the funds are indeed held in the lockup program's vault
|
||||
(a point we will get to below) then the program will release the funds.
|
||||
|
||||
## Realizing Locked Tokens
|
||||
|
||||
Optionally, vesting accounts can be created with a `realizer` program, which is
|
||||
a program implementing the lockup program's `RealizeLock` trait. In
|
||||
addition to the vesting schedule, a `realizer` program determines if and when a
|
||||
beneficiary can ever seize control over locked funds. It's effectively a function
|
||||
returning a boolean: is realized or not.
|
||||
|
||||
The uses cases for a realizer are application specific.
|
||||
For example, in the case of the staking program, when a vesting account is distributed as a reward,
|
||||
the staking program sets itself as the realizor, ensuring that the only way for the vesting account
|
||||
to be realized is if the beneficiary completely unstakes and incurs the unbonding timelock alongside
|
||||
any other consequences of unstaking (e.g., the inability to vote on governance proposals).
|
||||
This implies that, if one never unstakes, one never receives locked token rewards, adding
|
||||
an additional consideration when managing one's stake.
|
||||
|
||||
If no such `realizer` exists, tokens are realized upon account creation.
|
||||
|
||||
## Whitelisted Programs
|
||||
|
||||
Although funds cannot be freely withdrawn prior to vesting, they can be sent to/from
|
||||
other programs that are part of a **Whitelist**. These programs are completely trusted.
|
||||
Any bug or flaw in the design of a whitelisted program can lead to locked tokens being released
|
||||
ahead of schedule, so it's important to take great care when whitelisting any program.
|
||||
|
||||
This of course begs the question, who approves the whitelist? The **Lockup** program doesn't
|
||||
care. There simply exists an **authority** key that can, for example, be a democratic multisig,
|
||||
a single admin, or the zero address--in which case the authority ceases to exist, as the
|
||||
program will reject transactions signing from that address. Although the **authority** can never
|
||||
move a **Vesting** account's funds, whoever controls the **authority** key
|
||||
controls the whitelist. So when using the **Lockup** program, one should always be
|
||||
cognizant of it's whitelist governance, which ultimately anchors one's trust in the program,
|
||||
if any at all.
|
||||
|
||||
## Creating a Whitelisted Program
|
||||
|
||||
To create a whitelisted program that receives withdrawals/deposits from/to the Lockup program,
|
||||
one needs to implement the whitelist transfer interface, which assumes nothing about the
|
||||
`instruction_data` but requires accounts to be provided in a specific [order](https://github.com/project-serum/serum-dex/blob/master/registry/program/src/deposit.rs#L18).
|
||||
|
||||
We'll use staking locked SRM as a working example.
|
||||
|
||||
### Staking Locked Tokens
|
||||
|
||||
Suppose you have a vesting account with some funds you want to stake.
|
||||
|
||||
First, one must add the staking **Registry** as a whitelisted program, so that the Lockup program
|
||||
allows the movement of funds. This is done by the `WhitelistAdd` instruction.
|
||||
|
||||
Once whitelisted, **Vesting** accounts can transfer funds out of the **Lockup** program and
|
||||
into the **Registry** program by invoking the **Lockup** program's `WhitelistWithdraw`
|
||||
instruction, which, other than access control, simply relays the instruction from the
|
||||
**Lockup** program to the **Registry** program along with accounts, signing the
|
||||
Cross-Program-Invocation (CPI) with the **Lockup**'s program-derived-address to allow
|
||||
the transfer of funds, which ultimately is done by the **Registry**. *It is the Registry's responsibility
|
||||
to track where these funds came from, keep them locked, and eventually send them back.*
|
||||
|
||||
When creating this instruction on the client, there are two parameters to provide:
|
||||
the maximum `amount` available for transfer and the opaque CPI `instruction_data`.
|
||||
In the example, here, it would be the Borsh serialized instruction data for the
|
||||
**Registry**'s `Deposit` instruction.
|
||||
|
||||
The other direction follows, similarly. One invokes the `WhitelistDeposit` instruction
|
||||
on the **LockupProgram**, relaying the transaction to the **Registry**, which ultimately
|
||||
transfer funds back into the lockup program on behalf of the **Vesting** account.
|
||||
|
||||
## Major version upgrades.
|
||||
|
||||
Assuming the `authority` account is set on the **Lockup** program, we can use this Whitelist
|
||||
mechanism to do major version upgrades of the lockup program. One can whitelist the
|
||||
new **Lockup** program, and then all **Vesting** accounts would invidiually perform the migration
|
||||
by transferring their funds to the new proigram via the `WhitelistWithdraw` instruction.
|
|
@ -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.
|
Loading…
Reference in New Issue