docs: Staking, lockup, and cranking rewards (#66)

This commit is contained in:
Armani Ferrante 2020-12-19 16:47:44 -08:00 committed by GitHub
parent e7bcee8ecf
commit 689e382cd3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 418 additions and 0 deletions

99
docs/lockups.md Normal file
View File

@ -0,0 +1,99 @@
# Serum Lockups
WARNING: All code related to Serum Lockups is unaudited. Use at your own risk.
## Introduction
The **Lockup** program provides a simple mechanism on Solana 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.
## 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:
* 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.
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 endTimestamp = Date.now()/1000 + 60*60*24*365;
const periodCount = 2;
const depositAmount = 100 * 10**6; // 6 decimal places.
```
From these three parameters, one can deduce the total amount vested at any given time.
See how this is done via the `total_vested` function [here](https://github.com/project-serum/serum-dex/blob/master/lockup/src/accounts/vesting.rs#L72).
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. Of course, 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 next) then the program will release the funds.
## 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 program itself (in which case the authority ceases to exist). Whoever controls
that 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 SRM
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](https://github.com/project-serum/serum-dex/blob/master/lockup/src/lib.rs#L79)
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.

115
docs/node-setup.md Normal file
View File

@ -0,0 +1,115 @@
# Serum Node Setup
WARNING: All code related to Serum Nodes is unaudited. Use at your own risk.
## Introduction
Serum nodes are run by staked node leaders, who become eligible for cranking
when their node has at least 1 MSRM staked. These "cranking rewards"
are effectively transaction fees earned for operating the DEX.
For an introduction to the DEX and the idea of cranking, see
[A technical introduction to the Serum DEX](https://docs.google.com/document/d/1isGJES4jzQutI0GtQGuqtrBUqeHxl_xJNXdtOv4SdII/edit).
The way cranking rewards work is simple, instead of sending transactions directly to the DEX,
a cranker sends transactions to a cranking rewards vendor, which is an on-chain
Solana program that proxies all requests to the DEX, recording the amount of events
cranked, and then sends a reward to the cranker's wallet as a function of the number
of events processed and the reward vendor's fee rate.
(Note that, although similar in spirit, the cranking rewards vendor is an entirely different
program and account from the **Registry**'s reward vendors. Only node leaders are eligible
to crank.)
If the rewards vendor's vault becomes empty or if the node leader's Entity stake
balance ever transitions to **inactive**, then the vendor will refuse to pay
rewards to the node leader until the vault is funded and/or the node becomes **active** again.
## Install Rust
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
On Linux systems you may need to install additional dependencies. On Ubuntu,
```bash
sudo apt-get install -y pkg-config build-essential python3-pip jq
```
## Install the CLI
The CLI is a work in progress, so there's not yet a proper installer.
For now, we can use Cargo.
```bash
cargo install --git https://github.com/project-serum/serum-dex serum-cli
```
To verify the installation worked, run `serum -h`.
## Setup your CLI Config
Add your YAML config for Devnet at `~/.config/serum/cli/config.yaml`.
```yaml
---
network:
cluster: devnet
mints:
srm: 4Ghge2MMPmWXeD2FR541akGhjjgUi7RUtk7DBP5bTwGB
msrm: 5PsAVQLCrgtKqZpLdg7HsTXHMcvVCQ1c4bFHHej8Axxn
programs:
rewards_pid: nwEt8jsBDCjV5vNg9c5YN9ktyak314DCwVTTuA3Swd9
registry_pid: FigXetJcXogqm94qfmyKWy6U5KJAwtxSgJMjUHercVQp
meta_entity_pid: 8wfM5sd5Yivn4WWkcSp4pNua7ytDvjeyLVLaU3QWiLAT
lockup_pid: CiNaYvdnQ42BNdbKvvAapHxiP18pvc3Vk5WuZ59ia64x
dex_pid: F9b23Ph1JdBev2fULXTZLzaxVh2nYVdMVq9CTEaEZrid
```
When operating over multiple networks, you can specify your config file with the
`serum --config <path>` option.
## Cranking a market
Finally you can run your crank. Pick a market and run
```bash
serum crank consume-event-rewards \
--market <address> \
--log-directory <path> \
--rewards.receiver <address> \
--rewards.registry-entity <address> \
--rewards.instance <address>
```
If the given `--rewards.registry-entity` is properly staked, and if the given
`--rewards.instance` is funded, then you should see your token account
`--rewards.receiver` start to receive rewards with each event consumed.
## Finding a market to crank
You can crank any market of your choosing. To find all markets one can use the `getProgramAccounts`
API exposed by the Solana JSON RPC. In python,
```python
def find_market_addresses(program_id: str):
resp = requests.post('https://devnet.solana.com', json={
'jsonrpc': '2.0',
'method': 'getProgramAccounts',
'id': 1,
'params': [
program_id,
{
'encoding': 'base64',
'filters': [
# Base58 encoding of 0x0300000000000000
{'memcmp': {'offset': 5, 'bytes': 'W723RTUpoZ'}},
],
},
],
}).json()
return [info['pubkey'] for info in resp['result']]
```

204
docs/staking.md Normal file
View File

@ -0,0 +1,204 @@
# Serum Staking
WARNING: All code related to Serum Staking is unaudited. Use at your own risk.
## Introduction
The **Registry** program provides the central point of on-chain coordination for stakers,
providing two features: a gateway to the staking pool and a repository for node state.
As a gateway to the staking pool, the **Registry** controls who and when stakers can enter
and exit the pool. This enables controls like a mandatory 1 MSRM node deposit before entering
the pool, the staking of *locked* SRM, and a 1 week timelock for withdrawals. As a repository for
node state, the **Registry** allows other programs to
perform access control on staked accounts. This is useful for programs building on-top of the
registry. For example, a **crank-rewards** program could use Registry accounts to determine if
a node-leader is eligble for payment. A **governance** program could tally votes from
node member accounts based on their stake weight.
In short, the **Registry** allows node entities to be created, members to stake, and rewards to
be generated, while providing a foundation for other programs to build on.
Here, we'll discuss it's role in facilitating staking.
## Creating a member account.
Before being able to enter the stake pool, one must create a [Member](https://github.com/project-serum/serum-dex/blob/master/registry/src/accounts/member.rs) account with the
[Registrar](https://github.com/project-serum/serum-dex/blob/master/registry/src/accounts/registrar.rs), 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**:
* Free-balances
* Pending
* Stake
* Stake pool token
Each of these vaults provide a unit of balance isolation unique to a **Member**.
That is, although we provide a pooling mechanism, funds between **Member** accounts do not
share SPL token accounts. The only way for funds to move is for a **Member** to authorize
instructions that either exit the system or move funds between a **Member**'s own vaults.
## Depositing and Withdrawing.
Funds enter and exit the **Registry** through the `Deposit` and `Withdraw` instructions,
which transfer funds into and out of the **free-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 system. One
could even perform token transfer directly into the vault, but it's recommended to use the
instruction API, which provide additional safety checks to help ensure the funds are moved
as intended.
## Staking.
Once deposited, **Members** invoke the `Stake` instruction to transfer funds from
their **free-balances-vault** to their **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 of 1000 SRM--to start and subject to change
in future versions. This creates some restrictions on the underlying stake.
## Unstaking
Once staked, funds cannot be immediately withdrawn. Rather, the **Registry** will enforce
a one week timelock before funds are released. Upon executing the `StartStakeWithdrawal`
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
`EndStakeWithdrawal` instruction to complete the transfer out of the `pending-vault` and
into the `free-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 locked token rewards,
which of course can't be dropped directly onto a pool, since they are controlled by an additional
program controlling it's own set of accounts. So, if we did go with an AMM style pool, we'd need a separate
mechanism for handling locked token rewards. 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.
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 **Registry**
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 the DEX, which itself creates 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.
## Reward Eligibility
To be eligible for reward, a node **Entity** must have 1 MSRM staked to it, marking
it "active". If the MSRM stake balance ever drops below 1, the node will be marked as pending
deactivation, starting a week long countdown ending with the **Entity** transitioning into the
inactive state, at which point rewards cease to be distributed. As soon one enters the pending
deactivation state, 1 MSRM needs to either be restaked by an associated **Member**
or all **Members** should move to a new **Entity**. Note that transitioning to an inactive state
does not affect one's **Member** vaults. It only affects one's ability to retrieve rewards from
a vendor.
## Misc
### Entity
An **Entity** is an additional **Registry** owned account representing a collection of **Member**
accounts with an associated "node leader", who is eligible for additional rewards via "node duties".
These "duties" amount to earning what are, effectively, transaction fees. An additional document will describe nodes
and their setup, but for the purposes of staking, all a **Member** needs to know is that it
belongs to an **Entity** account, and the stake associated with that **Entity** determines
its state.
### Member Accounts
This document describes 4 vault types belonging to **Member** accounts, making up a single set of balances. However,
there are two stake pools: one for SRM holders and one for MSRM holders, so really there are 8 vault types.
Additionally there are two types of balance groups: locked and unlocked.
As a result, there are really 16 vaults for each **Member**, 8 types of vaults in 2 separate sets,
each isolated from the other, so that locked tokens don't get intermingled with unlocked tokens.
But if we're staking locked tokens, we need to ensure we don't accidently unlock tokens.
To maintain the **Lockup** program's invariant, we need a mechanism for safely entering and exiting
the system; that is, locked tokens should only be sent back to the lockup program.
As a result, we assign each set of balances, locked and unlocked, it's own unique identifier.
For the unlocked set of accounts the identifier is the **Member** account's beneficiary
(i.e. the authority of the entire account), and for the locked set of accounts it's the vesting account's program
derived address, controlled by the lockup program. Upon depositing or withdrawing from the **Registry**,
the program ensures that tokens coming into the system are from vaults owned by the correct balance
identifier. Similarly, tokens going out of the system can only go to vaults owned by the correct balance
identifier.
In future work, this setup will allow us to extend the staking program to stake arbitrary assets owned by
arbitrary programs on behalf of an account owner.