Browse Source

examples/lockup: Add some docs

Armani Ferrante 4 years ago
parent
commit
0d7425be65
2 changed files with 321 additions and 0 deletions
  1. 127 0
      examples/lockup/docs/lockups.md
  2. 194 0
      examples/lockup/docs/staking.md

+ 127 - 0
examples/lockup/docs/lockups.md

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

+ 194 - 0
examples/lockup/docs/staking.md

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