docs: Staking, lockup, and cranking rewards (#66)
This commit is contained in:
parent
e7bcee8ecf
commit
689e382cd3
|
@ -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.
|
|
@ -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']]
|
||||
```
|
|
@ -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.
|
Loading…
Reference in New Issue