examples/lockup: Add some docs

This commit is contained in:
Armani Ferrante 2021-03-01 14:19:14 +08:00
parent 4d562b0a82
commit 0d7425be65
No known key found for this signature in database
GPG Key ID: D597A80BCF8E12B7
2 changed files with 321 additions and 0 deletions

View File

@ -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.

View File

@ -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.