Init repo
This commit is contained in:
commit
3032a09886
|
@ -0,0 +1,2 @@
|
|||
.anchor
|
||||
target
|
|
@ -0,0 +1,53 @@
|
|||
dist: bionic
|
||||
language: rust
|
||||
rust:
|
||||
- nightly
|
||||
cache: cargo
|
||||
env:
|
||||
- NODE_VERSION="14.7.0"
|
||||
|
||||
before_deploy:
|
||||
- anchor build --verifiable
|
||||
- echo "### SHA256 Checksums" > release_notes.md
|
||||
- sha256sum target/deploy/lockup.so > lockup_binary.txt
|
||||
- sha256sum target/idl/lockup.json > lockup_idl.txt
|
||||
- sha256sum target/deploy/registry.so > registry_binary.txt
|
||||
- sha256sum target/idl/registry.json > registry_idl.txt
|
||||
- cat *.txt >> release_notes.md
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
edge: true
|
||||
file:
|
||||
- "target/deploy/lockup.so"
|
||||
- "target/deploy/registry.so"
|
||||
- "target/idl/lockup.json"
|
||||
- "target/idl/registry.json"
|
||||
release_notes_file: release_notes.md
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
api_key:
|
||||
secure: MRahuKj/FhxUwkkvqiI3wJYWKzJ0PVl25ZfFhp5lA7xyYYj/heQOdX1rE8I3MkyBOWlSNAN89JXKQ61czOrkpjK/vjBt7/49iCkWuBd+ZQ0SOjrdFubAMl4ypd3C56v28Q/Rh5bAgm8IiJNeCidfWjiu36ibjAHMAxkwAssp76AV0hboWMJx6i4i8W/iFC8hQhiFa4npkTkrCtL4Vt8qY0fwqNRRpMZBIz22ZglYbhWpkaPMeikFun7Fjn9dvT0PM/xtcjTYOf4sxdjItpYjR0fUF+thuR+z4McgeYko3AZG9Sv8RMvw6yU1Hpq/Okk1wXcxNHyDtz/YriwiPgVzcIW2SGW2YxXh8YZEQFJuVodM8udYjFuNHy+qcNDiCvcoNIj2zYP3iWEVpiv4a3Hr33T/+iGTqLkjlnHcLKI8m2ykbHFtmNEmg6P4faayYkDSeEKRMZSDuA+CKh07LVlBQFyIRB3tfw3+tdBGQXQojgxAwuxnfANhScMpSdjZtdJCS912ijzVeSGa6C33+/fpAqQtCQqwJx+Bl6Bytvq+nBSjojWJZUqvE53IFwD5/bSd6FUIyAQMnQ6t8dOF+OWx0a1rFtpfLYYKZwei8kZlWNd4BLs+V0jkyHTzy0Cztre/EcmXdAHxAX8XrcrWt9sC1gwpApdrtZ20zZhEHsdgY/k=
|
||||
|
||||
_defaults: &defaults
|
||||
before_install:
|
||||
- nvm install $NODE_VERSION
|
||||
- npm install -g mocha
|
||||
- npm install -g @project-serum/anchor
|
||||
- npm install -g @project-serum/serum
|
||||
- npm install -g @project-serum/common
|
||||
- npm install -g @solana/spl-token
|
||||
- sudo apt-get install -y pkg-config build-essential libudev-dev
|
||||
- sh -c "$(curl -sSfL https://release.solana.com/v1.5.5/install)"
|
||||
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
|
||||
- export NODE_PATH="/home/travis/.nvm/versions/node/v$NODE_VERSION/lib/node_modules/:$NODE_PATH"
|
||||
- yes | solana-keygen new
|
||||
- cargo install --git https://github.com/project-serum/anchor anchor-cli --locked
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- <<: *defaults
|
||||
name: Runs the tests
|
||||
script:
|
||||
- anchor test
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -0,0 +1,38 @@
|
|||
# Stake
|
||||
|
||||
Programs for staking and lockups. For a technical introduction, see the [docs](./docs).
|
||||
|
||||
## Note
|
||||
|
||||
* **This code is unaudited. Use at your own risk.**
|
||||
|
||||
## Developing
|
||||
|
||||
[Anchor](https://github.com/project-serum/anchor) is used for developoment, and it's
|
||||
recommended workflow is used here. To get started, see the [guide](https://project-serum.github.io/anchor/getting-started/introduction.html).
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
anchor build --verifiable
|
||||
```
|
||||
|
||||
The `--verifiable` flag should be used before deploying so that your build artifacts
|
||||
can be deterministically generated with docker.
|
||||
|
||||
### Test
|
||||
|
||||
```
|
||||
anchor test
|
||||
```
|
||||
|
||||
### Verify
|
||||
|
||||
To verify the program deployed on Solana matches your local source code, change directory
|
||||
into the program you want to verify, e.g., `cd programs/registry`, and run
|
||||
|
||||
```bash
|
||||
anchor verify <program-id | write-buffer>
|
||||
```
|
||||
|
||||
A list of build artifacts can be found under [releases](https://github.com/project-serum/stake/releases).
|
|
@ -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 SPL tokens that unlocked twice, 50 each time, over the next year, one
|
||||
would 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 mentioned 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).
|
||||
|
||||
Take staking locked tokens 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, one 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,193 @@
|
|||
# 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.
|
||||
|
||||
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 staking is covered 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 SPL tokens.
|
||||
|
||||
## 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, it loses 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, effectively requiring one to stake other unintended 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 SPL token. And if an additional SPL token is dropped onto every
|
||||
member of the pool, all the existing member's shares are now worth two SPL tokens. So to enter the pool without
|
||||
dilution, one would have to "create" at a price of 2 SPL tokens 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 right back to the problem of mutating global account
|
||||
state for an SPL token.
|
||||
|
||||
Furthermore, dropping arbitrary program accounts as rewards hasn't even been covered, for example,
|
||||
locked token rewards, which of course can't be dropped directly onto an AMM style pool, since they are not tokens.
|
||||
So, if one did go with an AMM style pool, one would need a separate mechanism for handling more general rewards like
|
||||
locked token accounts. Ideally, there would be a single mechanism for both.
|
||||
|
||||
## Reward Vendors
|
||||
|
||||
Instead of trying to *push* rewards to users via a direct transfer or airdrop, one can use a *polling* model
|
||||
where users effectively event source a log on demand, providing a proof one is eligible for the reward.
|
||||
|
||||
When a reward is created, the program must 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 the program to drop rewards on 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 the token being staked.
|
||||
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** tokens. 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** tokens 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.
|
|
@ -0,0 +1,179 @@
|
|||
// deploy.js is a simple deploy script to initialize a program. This is run
|
||||
// immediately after a deploy.
|
||||
|
||||
const serumCmn = require("@project-serum/common");
|
||||
const anchor = require("@project-serum/anchor");
|
||||
const PublicKey = anchor.web3.PublicKey;
|
||||
|
||||
module.exports = async function (provider) {
|
||||
// Configure client to use the provider.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
// Setup genesis state.
|
||||
const registrarConfigs = await genesis(provider);
|
||||
|
||||
// Program clients.
|
||||
const lockup = anchor.workspace.Lockup;
|
||||
const registry = anchor.workspace.Registry;
|
||||
|
||||
// Registry state constructor.
|
||||
await registry.state.rpc.new({
|
||||
accounts: {
|
||||
lockupProgram: lockup.programId,
|
||||
},
|
||||
});
|
||||
|
||||
// Lockup state constructor.
|
||||
await lockup.state.rpc.new({
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
|
||||
// Delete the default whitelist entries.
|
||||
const defaultEntry = { programId: new anchor.web3.PublicKey() };
|
||||
await lockup.state.rpc.whitelistDelete(defaultEntry, {
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
|
||||
// Whitelist the registry.
|
||||
await lockup.state.rpc.whitelistAdd(
|
||||
{ programId: registry.programId },
|
||||
{
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize all registrars.
|
||||
const cfgKeys = Object.keys(registrarConfigs);
|
||||
for (let k = 0; k < cfgKeys.length; k += 1) {
|
||||
let r = registrarConfigs[cfgKeys[k]];
|
||||
const registrar = await registrarInit(
|
||||
registry,
|
||||
r.withdrawalTimelock,
|
||||
r.stakeRate,
|
||||
r.rewardQLen,
|
||||
new anchor.web3.PublicKey(r.mint)
|
||||
);
|
||||
r["registrar"] = registrar.toString();
|
||||
}
|
||||
|
||||
// Generate code for whitelisting on UIs.
|
||||
const code = generateCode(registry, lockup, registrarConfigs);
|
||||
console.log("Generated whitelisted UI addresses:", code);
|
||||
};
|
||||
|
||||
function generateCode(registry, lockup, registrarConfigs) {
|
||||
const registrars = Object.keys(registrarConfigs)
|
||||
.map((cfg) => `${cfg}: new PublicKey('${registrarConfigs[cfg].registrar}')`)
|
||||
.join(",");
|
||||
|
||||
const mints = Object.keys(registrarConfigs)
|
||||
.map((cfg) => `${cfg}: new PublicKey('${registrarConfigs[cfg].mint}')`)
|
||||
.join(",");
|
||||
|
||||
return `{
|
||||
registryProgramId: new PublicKey('${registry.programId}'),
|
||||
lockupProgramId: new PublicKey('${lockup.programId}'),
|
||||
registrars: { ${registrars} },
|
||||
mints: { ${mints} },
|
||||
}`;
|
||||
}
|
||||
|
||||
async function genesis(provider) {
|
||||
if (
|
||||
provider.connection._rpcEndpoint === "https://api.mainnet-beta.solana.com"
|
||||
) {
|
||||
return {
|
||||
srm: {
|
||||
withdrawalTimelock: 60 * 60 * 24 * 7, // 1 week.
|
||||
stakeRate: 500 * 10 ** 6, // 500 SRM.
|
||||
rewardQLen: 150,
|
||||
mint: "SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt",
|
||||
},
|
||||
msrm: {
|
||||
withdrawalTimelock: 60 * 60 * 24 * 7, // 1 week.
|
||||
stakeRate: 1,
|
||||
rewardQLen: 150,
|
||||
mint: "MSRMcoVyrFxnSgo5uXwone5SKcGhT1KEJMFEkMEWf9L",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const [token1Mint, _god1] = await serumCmn.createMintAndVault(
|
||||
provider,
|
||||
new anchor.BN(10000000000000),
|
||||
undefined,
|
||||
6
|
||||
);
|
||||
const [token2Mint, _god2] = await serumCmn.createMintAndVault(
|
||||
provider,
|
||||
new anchor.BN(10000000000),
|
||||
undefined,
|
||||
0
|
||||
);
|
||||
return {
|
||||
token1: {
|
||||
withdrawalTimelock: 60 * 60 * 24 * 7,
|
||||
stakeRate: 1000 * 10 ** 6,
|
||||
rewardQLen: 150,
|
||||
mint: token1Mint.toString(),
|
||||
},
|
||||
token2: {
|
||||
withdrawalTimelock: 60 * 60 * 24 * 7,
|
||||
stakeRate: 1,
|
||||
rewardQLen: 150,
|
||||
mint: token2Mint.toString(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function registrarInit(
|
||||
registry,
|
||||
_withdrawalTimelock,
|
||||
_stakeRate,
|
||||
rewardQLen,
|
||||
mint
|
||||
) {
|
||||
const registrar = new anchor.web3.Account();
|
||||
const rewardQ = new anchor.web3.Account();
|
||||
const withdrawalTimelock = new anchor.BN(_withdrawalTimelock);
|
||||
const stakeRate = new anchor.BN(_stakeRate);
|
||||
const [
|
||||
registrarSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[registrar.publicKey.toBuffer()],
|
||||
registry.programId
|
||||
);
|
||||
const poolMint = await serumCmn.createMint(
|
||||
registry.provider,
|
||||
registrarSigner
|
||||
);
|
||||
await registry.rpc.initialize(
|
||||
mint,
|
||||
registry.provider.wallet.publicKey,
|
||||
nonce,
|
||||
withdrawalTimelock,
|
||||
stakeRate,
|
||||
rewardQLen,
|
||||
{
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
poolMint,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
signers: [registrar, rewardQ],
|
||||
instructions: [
|
||||
await registry.account.registrar.createInstruction(registrar),
|
||||
await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
|
||||
],
|
||||
}
|
||||
);
|
||||
return registrar.publicKey;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "lockup"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "lockup"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
cpi = ["no-entrypoint"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.2.1"
|
||||
anchor-spl = "0.2.1"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,84 @@
|
|||
//! Utility functions for calculating unlock schedules for a vesting account.
|
||||
|
||||
use crate::Vesting;
|
||||
|
||||
pub fn available_for_withdrawal(vesting: &Vesting, current_ts: i64) -> u64 {
|
||||
std::cmp::min(outstanding_vested(vesting, current_ts), balance(vesting))
|
||||
}
|
||||
|
||||
// The amount of funds currently in the vault.
|
||||
fn balance(vesting: &Vesting) -> u64 {
|
||||
vesting
|
||||
.outstanding
|
||||
.checked_sub(vesting.whitelist_owned)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// The amount of outstanding locked tokens vested. Note that these
|
||||
// tokens might have been transferred to whitelisted programs.
|
||||
fn outstanding_vested(vesting: &Vesting, current_ts: i64) -> u64 {
|
||||
total_vested(vesting, current_ts)
|
||||
.checked_sub(withdrawn_amount(vesting))
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Returns the amount withdrawn from this vesting account.
|
||||
fn withdrawn_amount(vesting: &Vesting) -> u64 {
|
||||
vesting
|
||||
.start_balance
|
||||
.checked_sub(vesting.outstanding)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
// Returns the total vested amount up to the given ts, assuming zero
|
||||
// withdrawals and zero funds sent to other programs.
|
||||
fn total_vested(vesting: &Vesting, current_ts: i64) -> u64 {
|
||||
if current_ts < vesting.start_ts {
|
||||
0
|
||||
} else if current_ts >= vesting.end_ts {
|
||||
vesting.start_balance
|
||||
} else {
|
||||
linear_unlock(vesting, current_ts).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn linear_unlock(vesting: &Vesting, current_ts: i64) -> Option<u64> {
|
||||
// Signed division not supported.
|
||||
let current_ts = current_ts as u64;
|
||||
let start_ts = vesting.start_ts as u64;
|
||||
let end_ts = vesting.end_ts as u64;
|
||||
|
||||
// If we can't perfectly partition the vesting window,
|
||||
// push the start of the window back so that we can.
|
||||
//
|
||||
// This has the effect of making the first vesting period shorter
|
||||
// than the rest.
|
||||
let shifted_start_ts =
|
||||
start_ts.checked_sub(end_ts.checked_sub(start_ts)? % vesting.period_count)?;
|
||||
|
||||
// Similarly, if we can't perfectly divide up the vesting rewards
|
||||
// then make the first period act as a cliff, earning slightly more than
|
||||
// subsequent periods.
|
||||
let reward_overflow = vesting.start_balance % vesting.period_count;
|
||||
|
||||
// Reward per period ignoring the overflow.
|
||||
let reward_per_period =
|
||||
(vesting.start_balance.checked_sub(reward_overflow)?).checked_div(vesting.period_count)?;
|
||||
|
||||
// Number of vesting periods that have passed.
|
||||
let current_period = {
|
||||
let period_secs =
|
||||
(end_ts.checked_sub(shifted_start_ts)?).checked_div(vesting.period_count)?;
|
||||
let current_period_count =
|
||||
(current_ts.checked_sub(shifted_start_ts)?).checked_div(period_secs)?;
|
||||
std::cmp::min(current_period_count, vesting.period_count)
|
||||
};
|
||||
|
||||
if current_period == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
|
||||
current_period
|
||||
.checked_mul(reward_per_period)?
|
||||
.checked_add(reward_overflow)
|
||||
}
|
|
@ -0,0 +1,534 @@
|
|||
//! A relatively advanced example of a lockup program. If you're new to Anchor,
|
||||
//! it's suggested to start with the other examples.
|
||||
|
||||
#![feature(proc_macro_hygiene)]
|
||||
|
||||
use anchor_lang::prelude::*;
|
||||
use anchor_lang::solana_program::instruction::Instruction;
|
||||
use anchor_lang::solana_program::program;
|
||||
use anchor_spl::token::{self, TokenAccount, Transfer};
|
||||
|
||||
mod calculator;
|
||||
|
||||
#[program]
|
||||
pub mod lockup {
|
||||
use super::*;
|
||||
|
||||
#[state]
|
||||
pub struct Lockup {
|
||||
/// The key with the ability to change the whitelist.
|
||||
pub authority: Pubkey,
|
||||
/// List of programs locked tokens can be sent to. These programs
|
||||
/// are completely trusted to maintain the locked property.
|
||||
pub whitelist: Vec<WhitelistEntry>,
|
||||
}
|
||||
|
||||
impl Lockup {
|
||||
pub const WHITELIST_SIZE: usize = 10;
|
||||
|
||||
pub fn new(ctx: Context<Auth>) -> Result<Self> {
|
||||
let mut whitelist = vec![];
|
||||
whitelist.resize(Self::WHITELIST_SIZE, Default::default());
|
||||
Ok(Lockup {
|
||||
authority: *ctx.accounts.authority.key,
|
||||
whitelist,
|
||||
})
|
||||
}
|
||||
|
||||
#[access_control(whitelist_auth(self, &ctx))]
|
||||
pub fn whitelist_add(&mut self, ctx: Context<Auth>, entry: WhitelistEntry) -> Result<()> {
|
||||
if self.whitelist.len() == Self::WHITELIST_SIZE {
|
||||
return Err(ErrorCode::WhitelistFull.into());
|
||||
}
|
||||
if self.whitelist.contains(&entry) {
|
||||
return Err(ErrorCode::WhitelistEntryAlreadyExists.into());
|
||||
}
|
||||
self.whitelist.push(entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(whitelist_auth(self, &ctx))]
|
||||
pub fn whitelist_delete(
|
||||
&mut self,
|
||||
ctx: Context<Auth>,
|
||||
entry: WhitelistEntry,
|
||||
) -> Result<()> {
|
||||
if !self.whitelist.contains(&entry) {
|
||||
return Err(ErrorCode::WhitelistEntryNotFound.into());
|
||||
}
|
||||
self.whitelist.retain(|e| e != &entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(whitelist_auth(self, &ctx))]
|
||||
pub fn set_authority(&mut self, ctx: Context<Auth>, new_authority: Pubkey) -> Result<()> {
|
||||
self.authority = new_authority;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[access_control(CreateVesting::accounts(&ctx, nonce))]
|
||||
pub fn create_vesting(
|
||||
ctx: Context<CreateVesting>,
|
||||
beneficiary: Pubkey,
|
||||
deposit_amount: u64,
|
||||
nonce: u8,
|
||||
start_ts: i64,
|
||||
end_ts: i64,
|
||||
period_count: u64,
|
||||
realizor: Option<Realizor>,
|
||||
) -> Result<()> {
|
||||
if deposit_amount == 0 {
|
||||
return Err(ErrorCode::InvalidDepositAmount.into());
|
||||
}
|
||||
if !is_valid_schedule(start_ts, end_ts, period_count) {
|
||||
return Err(ErrorCode::InvalidSchedule.into());
|
||||
}
|
||||
let vesting = &mut ctx.accounts.vesting;
|
||||
vesting.beneficiary = beneficiary;
|
||||
vesting.mint = ctx.accounts.vault.mint;
|
||||
vesting.vault = *ctx.accounts.vault.to_account_info().key;
|
||||
vesting.period_count = period_count;
|
||||
vesting.start_balance = deposit_amount;
|
||||
vesting.end_ts = end_ts;
|
||||
vesting.start_ts = start_ts;
|
||||
vesting.created_ts = ctx.accounts.clock.unix_timestamp;
|
||||
vesting.outstanding = deposit_amount;
|
||||
vesting.whitelist_owned = 0;
|
||||
vesting.grantor = *ctx.accounts.depositor_authority.key;
|
||||
vesting.nonce = nonce;
|
||||
vesting.realizor = realizor;
|
||||
|
||||
token::transfer(ctx.accounts.into(), deposit_amount)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[access_control(is_realized(&ctx))]
|
||||
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
|
||||
// Has the given amount vested?
|
||||
if amount
|
||||
> calculator::available_for_withdrawal(
|
||||
&ctx.accounts.vesting,
|
||||
ctx.accounts.clock.unix_timestamp,
|
||||
)
|
||||
{
|
||||
return Err(ErrorCode::InsufficientWithdrawalBalance.into());
|
||||
}
|
||||
|
||||
// Transfer funds out.
|
||||
let seeds = &[
|
||||
ctx.accounts.vesting.to_account_info().key.as_ref(),
|
||||
&[ctx.accounts.vesting.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let cpi_ctx = CpiContext::from(&*ctx.accounts).with_signer(signer);
|
||||
token::transfer(cpi_ctx, amount)?;
|
||||
|
||||
// Bookeeping.
|
||||
let vesting = &mut ctx.accounts.vesting;
|
||||
vesting.outstanding -= amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sends funds from the lockup program to a whitelisted program.
|
||||
pub fn whitelist_withdraw<'a, 'b, 'c, 'info>(
|
||||
ctx: Context<'a, 'b, 'c, 'info, WhitelistWithdraw<'info>>,
|
||||
instruction_data: Vec<u8>,
|
||||
amount: u64,
|
||||
) -> Result<()> {
|
||||
let before_amount = ctx.accounts.transfer.vault.amount;
|
||||
whitelist_relay_cpi(
|
||||
&ctx.accounts.transfer,
|
||||
ctx.remaining_accounts,
|
||||
instruction_data,
|
||||
)?;
|
||||
let after_amount = ctx.accounts.transfer.vault.reload()?.amount;
|
||||
|
||||
// CPI safety checks.
|
||||
let withdraw_amount = before_amount - after_amount;
|
||||
if withdraw_amount > amount {
|
||||
return Err(ErrorCode::WhitelistWithdrawLimit)?;
|
||||
}
|
||||
|
||||
// Bookeeping.
|
||||
ctx.accounts.transfer.vesting.whitelist_owned += withdraw_amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Sends funds from a whitelisted program back to the lockup program.
|
||||
pub fn whitelist_deposit<'a, 'b, 'c, 'info>(
|
||||
ctx: Context<'a, 'b, 'c, 'info, WhitelistDeposit<'info>>,
|
||||
instruction_data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let before_amount = ctx.accounts.transfer.vault.amount;
|
||||
whitelist_relay_cpi(
|
||||
&ctx.accounts.transfer,
|
||||
ctx.remaining_accounts,
|
||||
instruction_data,
|
||||
)?;
|
||||
let after_amount = ctx.accounts.transfer.vault.reload()?.amount;
|
||||
|
||||
// CPI safety checks.
|
||||
let deposit_amount = after_amount - before_amount;
|
||||
if deposit_amount <= 0 {
|
||||
return Err(ErrorCode::InsufficientWhitelistDepositAmount)?;
|
||||
}
|
||||
if deposit_amount > ctx.accounts.transfer.vesting.whitelist_owned {
|
||||
return Err(ErrorCode::WhitelistDepositOverflow)?;
|
||||
}
|
||||
|
||||
// Bookkeeping.
|
||||
ctx.accounts.transfer.vesting.whitelist_owned -= deposit_amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Convenience function for UI's to calculate the withdrawable amount.
|
||||
pub fn available_for_withdrawal(ctx: Context<AvailableForWithdrawal>) -> Result<()> {
|
||||
let available = calculator::available_for_withdrawal(
|
||||
&ctx.accounts.vesting,
|
||||
ctx.accounts.clock.unix_timestamp,
|
||||
);
|
||||
// Log as string so that JS can read as a BN.
|
||||
msg!(&format!("{{ \"result\": \"{}\" }}", available));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Auth<'info> {
|
||||
#[account(signer)]
|
||||
authority: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct CreateVesting<'info> {
|
||||
// Vesting.
|
||||
#[account(init)]
|
||||
vesting: ProgramAccount<'info, Vesting>,
|
||||
#[account(mut)]
|
||||
vault: CpiAccount<'info, TokenAccount>,
|
||||
// Depositor.
|
||||
#[account(mut)]
|
||||
depositor: AccountInfo<'info>,
|
||||
#[account(signer)]
|
||||
depositor_authority: AccountInfo<'info>,
|
||||
// Misc.
|
||||
#[account("token_program.key == &token::ID")]
|
||||
token_program: AccountInfo<'info>,
|
||||
rent: Sysvar<'info, Rent>,
|
||||
clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
impl<'info> CreateVesting<'info> {
|
||||
fn accounts(ctx: &Context<CreateVesting>, nonce: u8) -> Result<()> {
|
||||
let vault_authority = Pubkey::create_program_address(
|
||||
&[
|
||||
ctx.accounts.vesting.to_account_info().key.as_ref(),
|
||||
&[nonce],
|
||||
],
|
||||
ctx.program_id,
|
||||
)
|
||||
.map_err(|_| ErrorCode::InvalidProgramAddress)?;
|
||||
if ctx.accounts.vault.owner != vault_authority {
|
||||
return Err(ErrorCode::InvalidVaultOwner)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// All accounts not included here, i.e., the "remaining accounts" should be
|
||||
// ordered according to the realization interface.
|
||||
#[derive(Accounts)]
|
||||
pub struct Withdraw<'info> {
|
||||
// Vesting.
|
||||
#[account(mut, has_one = beneficiary, has_one = vault)]
|
||||
vesting: ProgramAccount<'info, Vesting>,
|
||||
#[account(signer)]
|
||||
beneficiary: AccountInfo<'info>,
|
||||
#[account(mut)]
|
||||
vault: CpiAccount<'info, TokenAccount>,
|
||||
#[account(seeds = [vesting.to_account_info().key.as_ref(), &[vesting.nonce]])]
|
||||
vesting_signer: AccountInfo<'info>,
|
||||
// Withdraw receiving target..
|
||||
#[account(mut)]
|
||||
token: CpiAccount<'info, TokenAccount>,
|
||||
// Misc.
|
||||
#[account("token_program.key == &token::ID")]
|
||||
token_program: AccountInfo<'info>,
|
||||
clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct WhitelistWithdraw<'info> {
|
||||
transfer: WhitelistTransfer<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct WhitelistDeposit<'info> {
|
||||
transfer: WhitelistTransfer<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct WhitelistTransfer<'info> {
|
||||
lockup: ProgramState<'info, Lockup>,
|
||||
#[account(signer)]
|
||||
beneficiary: AccountInfo<'info>,
|
||||
whitelisted_program: AccountInfo<'info>,
|
||||
|
||||
// Whitelist interface.
|
||||
#[account(mut, has_one = beneficiary, has_one = vault)]
|
||||
vesting: ProgramAccount<'info, Vesting>,
|
||||
#[account(mut, "&vault.owner == vesting_signer.key")]
|
||||
vault: CpiAccount<'info, TokenAccount>,
|
||||
#[account(seeds = [vesting.to_account_info().key.as_ref(), &[vesting.nonce]])]
|
||||
vesting_signer: AccountInfo<'info>,
|
||||
#[account("token_program.key == &token::ID")]
|
||||
token_program: AccountInfo<'info>,
|
||||
#[account(mut)]
|
||||
whitelisted_program_vault: AccountInfo<'info>,
|
||||
whitelisted_program_vault_authority: AccountInfo<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct AvailableForWithdrawal<'info> {
|
||||
vesting: ProgramAccount<'info, Vesting>,
|
||||
clock: Sysvar<'info, Clock>,
|
||||
}
|
||||
|
||||
#[account]
|
||||
pub struct Vesting {
|
||||
/// The owner of this Vesting account.
|
||||
pub beneficiary: Pubkey,
|
||||
/// The mint of the SPL token locked up.
|
||||
pub mint: Pubkey,
|
||||
/// Address of the account's token vault.
|
||||
pub vault: Pubkey,
|
||||
/// The owner of the token account funding this account.
|
||||
pub grantor: Pubkey,
|
||||
/// The outstanding SRM deposit backing this vesting account. All
|
||||
/// withdrawals will deduct this balance.
|
||||
pub outstanding: u64,
|
||||
/// The starting balance of this vesting account, i.e., how much was
|
||||
/// originally deposited.
|
||||
pub start_balance: u64,
|
||||
/// The unix timestamp at which this vesting account was created.
|
||||
pub created_ts: i64,
|
||||
/// The time at which vesting begins.
|
||||
pub start_ts: i64,
|
||||
/// The time at which all tokens are vested.
|
||||
pub end_ts: i64,
|
||||
/// The number of times vesting will occur. For example, if vesting
|
||||
/// is once a year over seven years, this will be 7.
|
||||
pub period_count: u64,
|
||||
/// The amount of tokens in custody of whitelisted programs.
|
||||
pub whitelist_owned: u64,
|
||||
/// Signer nonce.
|
||||
pub nonce: u8,
|
||||
/// The program that determines when the locked account is **realized**.
|
||||
/// In addition to the lockup schedule, the program provides the ability
|
||||
/// for applications to determine when locked tokens are considered earned.
|
||||
/// For example, when earning locked tokens via the staking program, one
|
||||
/// cannot receive the tokens until unstaking. As a result, if one never
|
||||
/// unstakes, one would never actually receive the locked tokens.
|
||||
pub realizor: Option<Realizor>,
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||||
pub struct Realizor {
|
||||
/// Program to invoke to check a realization condition. This program must
|
||||
/// implement the `RealizeLock` trait.
|
||||
pub program: Pubkey,
|
||||
/// Address of an arbitrary piece of metadata interpretable by the realizor
|
||||
/// program. For example, when a vesting account is allocated, the program
|
||||
/// can define its realization condition as a function of some account
|
||||
/// state. The metadata is the address of that account.
|
||||
///
|
||||
/// In the case of staking, the metadata is a `Member` account address. When
|
||||
/// the realization condition is checked, the staking program will check the
|
||||
/// `Member` account defined by the `metadata` has no staked tokens.
|
||||
pub metadata: Pubkey,
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize, PartialEq, Default, Copy, Clone)]
|
||||
pub struct WhitelistEntry {
|
||||
pub program_id: Pubkey,
|
||||
}
|
||||
|
||||
#[error]
|
||||
pub enum ErrorCode {
|
||||
#[msg("Vesting end must be greater than the current unix timestamp.")]
|
||||
InvalidTimestamp,
|
||||
#[msg("The number of vesting periods must be greater than zero.")]
|
||||
InvalidPeriod,
|
||||
#[msg("The vesting deposit amount must be greater than zero.")]
|
||||
InvalidDepositAmount,
|
||||
#[msg("The Whitelist entry is not a valid program address.")]
|
||||
InvalidWhitelistEntry,
|
||||
#[msg("Invalid program address. Did you provide the correct nonce?")]
|
||||
InvalidProgramAddress,
|
||||
#[msg("Invalid vault owner.")]
|
||||
InvalidVaultOwner,
|
||||
#[msg("Vault amount must be zero.")]
|
||||
InvalidVaultAmount,
|
||||
#[msg("Insufficient withdrawal balance.")]
|
||||
InsufficientWithdrawalBalance,
|
||||
#[msg("Whitelist is full")]
|
||||
WhitelistFull,
|
||||
#[msg("Whitelist entry already exists")]
|
||||
WhitelistEntryAlreadyExists,
|
||||
#[msg("Balance must go up when performing a whitelist deposit")]
|
||||
InsufficientWhitelistDepositAmount,
|
||||
#[msg("Cannot deposit more than withdrawn")]
|
||||
WhitelistDepositOverflow,
|
||||
#[msg("Tried to withdraw over the specified limit")]
|
||||
WhitelistWithdrawLimit,
|
||||
#[msg("Whitelist entry not found.")]
|
||||
WhitelistEntryNotFound,
|
||||
#[msg("You do not have sufficient permissions to perform this action.")]
|
||||
Unauthorized,
|
||||
#[msg("You are unable to realize projected rewards until unstaking.")]
|
||||
UnableToWithdrawWhileStaked,
|
||||
#[msg("The given lock realizor doesn't match the vesting account.")]
|
||||
InvalidLockRealizor,
|
||||
#[msg("You have not realized this vesting account.")]
|
||||
UnrealizedVesting,
|
||||
#[msg("Invalid vesting schedule given.")]
|
||||
InvalidSchedule,
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c, 'info> From<&mut CreateVesting<'info>>
|
||||
for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
|
||||
{
|
||||
fn from(accounts: &mut CreateVesting<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
||||
let cpi_accounts = Transfer {
|
||||
from: accounts.depositor.clone(),
|
||||
to: accounts.vault.to_account_info(),
|
||||
authority: accounts.depositor_authority.clone(),
|
||||
};
|
||||
let cpi_program = accounts.token_program.clone();
|
||||
CpiContext::new(cpi_program, cpi_accounts)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b, 'c, 'info> From<&Withdraw<'info>> for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
||||
fn from(accounts: &Withdraw<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
|
||||
let cpi_accounts = Transfer {
|
||||
from: accounts.vault.to_account_info(),
|
||||
to: accounts.token.to_account_info(),
|
||||
authority: accounts.vesting_signer.to_account_info(),
|
||||
};
|
||||
let cpi_program = accounts.token_program.to_account_info();
|
||||
CpiContext::new(cpi_program, cpi_accounts)
|
||||
}
|
||||
}
|
||||
|
||||
#[access_control(is_whitelisted(transfer))]
|
||||
pub fn whitelist_relay_cpi<'info>(
|
||||
transfer: &WhitelistTransfer<'info>,
|
||||
remaining_accounts: &[AccountInfo<'info>],
|
||||
instruction_data: Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let mut meta_accounts = vec![
|
||||
AccountMeta::new_readonly(*transfer.vesting.to_account_info().key, false),
|
||||
AccountMeta::new(*transfer.vault.to_account_info().key, false),
|
||||
AccountMeta::new_readonly(*transfer.vesting_signer.to_account_info().key, true),
|
||||
AccountMeta::new_readonly(*transfer.token_program.to_account_info().key, false),
|
||||
AccountMeta::new(
|
||||
*transfer.whitelisted_program_vault.to_account_info().key,
|
||||
false,
|
||||
),
|
||||
AccountMeta::new_readonly(
|
||||
*transfer
|
||||
.whitelisted_program_vault_authority
|
||||
.to_account_info()
|
||||
.key,
|
||||
false,
|
||||
),
|
||||
];
|
||||
meta_accounts.extend(remaining_accounts.iter().map(|a| {
|
||||
if a.is_writable {
|
||||
AccountMeta::new(*a.key, a.is_signer)
|
||||
} else {
|
||||
AccountMeta::new_readonly(*a.key, a.is_signer)
|
||||
}
|
||||
}));
|
||||
let relay_instruction = Instruction {
|
||||
program_id: *transfer.whitelisted_program.to_account_info().key,
|
||||
accounts: meta_accounts,
|
||||
data: instruction_data.to_vec(),
|
||||
};
|
||||
|
||||
let seeds = &[
|
||||
transfer.vesting.to_account_info().key.as_ref(),
|
||||
&[transfer.vesting.nonce],
|
||||
];
|
||||
let signer = &[&seeds[..]];
|
||||
let mut accounts = transfer.to_account_infos();
|
||||
accounts.extend_from_slice(&remaining_accounts);
|
||||
program::invoke_signed(&relay_instruction, &accounts, signer).map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn is_whitelisted<'info>(transfer: &WhitelistTransfer<'info>) -> Result<()> {
|
||||
if !transfer.lockup.whitelist.contains(&WhitelistEntry {
|
||||
program_id: *transfer.whitelisted_program.key,
|
||||
}) {
|
||||
return Err(ErrorCode::WhitelistEntryNotFound.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn whitelist_auth(lockup: &Lockup, ctx: &Context<Auth>) -> Result<()> {
|
||||
if &lockup.authority != ctx.accounts.authority.key {
|
||||
return Err(ErrorCode::Unauthorized.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_valid_schedule(start_ts: i64, end_ts: i64, period_count: u64) -> bool {
|
||||
if end_ts <= start_ts {
|
||||
return false;
|
||||
}
|
||||
if period_count > (end_ts - start_ts) as u64 {
|
||||
return false;
|
||||
}
|
||||
if period_count == 0 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
// Returns Ok if the locked vesting account has been "realized". Realization
|
||||
// is application dependent. For example, in the case of staking, one must first
|
||||
// unstake before being able to earn locked tokens.
|
||||
fn is_realized(ctx: &Context<Withdraw>) -> Result<()> {
|
||||
if let Some(realizor) = &ctx.accounts.vesting.realizor {
|
||||
let cpi_program = {
|
||||
let p = ctx.remaining_accounts[0].clone();
|
||||
if p.key != &realizor.program {
|
||||
return Err(ErrorCode::InvalidLockRealizor.into());
|
||||
}
|
||||
p
|
||||
};
|
||||
let cpi_accounts = ctx.remaining_accounts.to_vec()[1..].to_vec();
|
||||
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
|
||||
let vesting = (*ctx.accounts.vesting).clone();
|
||||
realize_lock::is_realized(cpi_ctx, vesting).map_err(|_| ErrorCode::UnrealizedVesting)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// RealizeLock defines the interface an external program must implement if
|
||||
/// they want to define a "realization condition" on a locked vesting account.
|
||||
/// This condition must be satisfied *even if a vesting schedule has
|
||||
/// completed*. Otherwise the user can never earn the locked funds. For example,
|
||||
/// in the case of the staking program, one cannot received a locked reward
|
||||
/// until one has completely unstaked.
|
||||
#[interface]
|
||||
pub trait RealizeLock<'info, T: Accounts<'info>> {
|
||||
fn is_realized(ctx: Context<T>, v: Vesting) -> ProgramResult;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "registry"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
edition = "2018"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "registry"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
cpi = ["no-entrypoint"]
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = "0.2.1"
|
||||
anchor-spl = "0.2.1"
|
||||
lockup = { path = "../lockup", features = ["cpi"] }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,946 @@
|
|||
const assert = require("assert");
|
||||
const anchor = require("@project-serum/anchor");
|
||||
const serumCmn = require("@project-serum/common");
|
||||
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
|
||||
const utils = require("./utils");
|
||||
|
||||
describe("Lockup and Registry", () => {
|
||||
// Read the provider from the configured environmnet.
|
||||
const provider = anchor.Provider.env();
|
||||
|
||||
// Configure the client to use the provider.
|
||||
anchor.setProvider(provider);
|
||||
|
||||
const lockup = anchor.workspace.Lockup;
|
||||
const registry = anchor.workspace.Registry;
|
||||
|
||||
let lockupAddress = null;
|
||||
const WHITELIST_SIZE = 10;
|
||||
|
||||
let mint = null;
|
||||
let god = null;
|
||||
|
||||
it("Sets up initial test state", async () => {
|
||||
const [_mint, _god] = await serumCmn.createMintAndVault(
|
||||
provider,
|
||||
new anchor.BN(1000000)
|
||||
);
|
||||
mint = _mint;
|
||||
god = _god;
|
||||
});
|
||||
|
||||
it("Is initialized!", async () => {
|
||||
await lockup.state.rpc.new({
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
|
||||
lockupAddress = await lockup.state.address();
|
||||
const lockupAccount = await lockup.state();
|
||||
|
||||
assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
|
||||
assert.ok(lockupAccount.whitelist.length === WHITELIST_SIZE);
|
||||
lockupAccount.whitelist.forEach((e) => {
|
||||
assert.ok(e.programId.equals(new anchor.web3.PublicKey()));
|
||||
});
|
||||
});
|
||||
|
||||
it("Deletes the default whitelisted addresses", async () => {
|
||||
const defaultEntry = { programId: new anchor.web3.PublicKey() };
|
||||
await lockup.state.rpc.whitelistDelete(defaultEntry, {
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Sets a new authority", async () => {
|
||||
const newAuthority = new anchor.web3.Account();
|
||||
await lockup.state.rpc.setAuthority(newAuthority.publicKey, {
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
|
||||
let lockupAccount = await lockup.state();
|
||||
assert.ok(lockupAccount.authority.equals(newAuthority.publicKey));
|
||||
|
||||
await lockup.state.rpc.setAuthority(provider.wallet.publicKey, {
|
||||
accounts: {
|
||||
authority: newAuthority.publicKey,
|
||||
},
|
||||
signers: [newAuthority],
|
||||
});
|
||||
|
||||
lockupAccount = await lockup.state();
|
||||
assert.ok(lockupAccount.authority.equals(provider.wallet.publicKey));
|
||||
});
|
||||
|
||||
const entries = [];
|
||||
|
||||
it("Adds to the whitelist", async () => {
|
||||
const generateEntry = async () => {
|
||||
let programId = new anchor.web3.Account().publicKey;
|
||||
return {
|
||||
programId,
|
||||
};
|
||||
};
|
||||
|
||||
for (let k = 0; k < WHITELIST_SIZE; k += 1) {
|
||||
entries.push(await generateEntry());
|
||||
}
|
||||
|
||||
const accounts = {
|
||||
authority: provider.wallet.publicKey,
|
||||
};
|
||||
|
||||
await lockup.state.rpc.whitelistAdd(entries[0], { accounts });
|
||||
|
||||
let lockupAccount = await lockup.state();
|
||||
|
||||
assert.ok(lockupAccount.whitelist.length === 1);
|
||||
assert.deepEqual(lockupAccount.whitelist, [entries[0]]);
|
||||
|
||||
for (let k = 1; k < WHITELIST_SIZE; k += 1) {
|
||||
await lockup.state.rpc.whitelistAdd(entries[k], { accounts });
|
||||
}
|
||||
|
||||
lockupAccount = await lockup.state();
|
||||
|
||||
assert.deepEqual(lockupAccount.whitelist, entries);
|
||||
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
const e = await generateEntry();
|
||||
await lockup.state.rpc.whitelistAdd(e, { accounts });
|
||||
},
|
||||
(err) => {
|
||||
assert.equal(err.code, 108);
|
||||
assert.equal(err.msg, "Whitelist is full");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("Removes from the whitelist", async () => {
|
||||
await lockup.state.rpc.whitelistDelete(entries[0], {
|
||||
accounts: {
|
||||
authority: provider.wallet.publicKey,
|
||||
},
|
||||
});
|
||||
let lockupAccount = await lockup.state();
|
||||
assert.deepEqual(lockupAccount.whitelist, entries.slice(1));
|
||||
});
|
||||
|
||||
const vesting = new anchor.web3.Account();
|
||||
let vestingAccount = null;
|
||||
let vestingSigner = null;
|
||||
|
||||
it("Creates a vesting account", async () => {
|
||||
const startTs = new anchor.BN(Date.now() / 1000);
|
||||
const endTs = new anchor.BN(startTs.toNumber() + 5);
|
||||
const periodCount = new anchor.BN(2);
|
||||
const beneficiary = provider.wallet.publicKey;
|
||||
const depositAmount = new anchor.BN(100);
|
||||
|
||||
const vault = new anchor.web3.Account();
|
||||
let [
|
||||
_vestingSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[vesting.publicKey.toBuffer()],
|
||||
lockup.programId
|
||||
);
|
||||
vestingSigner = _vestingSigner;
|
||||
|
||||
await lockup.rpc.createVesting(
|
||||
beneficiary,
|
||||
depositAmount,
|
||||
nonce,
|
||||
startTs,
|
||||
endTs,
|
||||
periodCount,
|
||||
null, // Lock realizor is None.
|
||||
{
|
||||
accounts: {
|
||||
vesting: vesting.publicKey,
|
||||
vault: vault.publicKey,
|
||||
depositor: god,
|
||||
depositorAuthority: provider.wallet.publicKey,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
signers: [vesting, vault],
|
||||
instructions: [
|
||||
await lockup.account.vesting.createInstruction(vesting),
|
||||
...(await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
vault.publicKey,
|
||||
mint,
|
||||
vestingSigner
|
||||
)),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
vestingAccount = await lockup.account.vesting(vesting.publicKey);
|
||||
|
||||
assert.ok(vestingAccount.beneficiary.equals(provider.wallet.publicKey));
|
||||
assert.ok(vestingAccount.mint.equals(mint));
|
||||
assert.ok(vestingAccount.grantor.equals(provider.wallet.publicKey));
|
||||
assert.ok(vestingAccount.outstanding.eq(depositAmount));
|
||||
assert.ok(vestingAccount.startBalance.eq(depositAmount));
|
||||
assert.ok(vestingAccount.whitelistOwned.eq(new anchor.BN(0)));
|
||||
assert.equal(vestingAccount.nonce, nonce);
|
||||
assert.ok(vestingAccount.createdTs.gt(new anchor.BN(0)));
|
||||
assert.ok(vestingAccount.startTs.eq(startTs));
|
||||
assert.ok(vestingAccount.endTs.eq(endTs));
|
||||
assert.ok(vestingAccount.realizor === null);
|
||||
});
|
||||
|
||||
it("Fails to withdraw from a vesting account before vesting", async () => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await lockup.rpc.withdraw(new anchor.BN(100), {
|
||||
accounts: {
|
||||
vesting: vesting.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
token: god,
|
||||
vault: vestingAccount.vault,
|
||||
vestingSigner: vestingSigner,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
assert.equal(err.code, 107);
|
||||
assert.equal(err.msg, "Insufficient withdrawal balance.");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("Waits for a vesting period to pass", async () => {
|
||||
await serumCmn.sleep(10 * 1000);
|
||||
});
|
||||
|
||||
it("Withdraws from the vesting account", async () => {
|
||||
const token = await serumCmn.createTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
await lockup.rpc.withdraw(new anchor.BN(100), {
|
||||
accounts: {
|
||||
vesting: vesting.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
token,
|
||||
vault: vestingAccount.vault,
|
||||
vestingSigner,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
});
|
||||
|
||||
vestingAccount = await lockup.account.vesting(vesting.publicKey);
|
||||
assert.ok(vestingAccount.outstanding.eq(new anchor.BN(0)));
|
||||
|
||||
const vaultAccount = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
vestingAccount.vault
|
||||
);
|
||||
assert.ok(vaultAccount.amount.eq(new anchor.BN(0)));
|
||||
|
||||
const tokenAccount = await serumCmn.getTokenAccount(provider, token);
|
||||
assert.ok(tokenAccount.amount.eq(new anchor.BN(100)));
|
||||
});
|
||||
|
||||
const registrar = new anchor.web3.Account();
|
||||
const rewardQ = new anchor.web3.Account();
|
||||
const withdrawalTimelock = new anchor.BN(4);
|
||||
const stakeRate = new anchor.BN(2);
|
||||
const rewardQLen = 170;
|
||||
let registrarAccount = null;
|
||||
let registrarSigner = null;
|
||||
let nonce = null;
|
||||
let poolMint = null;
|
||||
|
||||
it("Creates registry genesis", async () => {
|
||||
const [
|
||||
_registrarSigner,
|
||||
_nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[registrar.publicKey.toBuffer()],
|
||||
registry.programId
|
||||
);
|
||||
registrarSigner = _registrarSigner;
|
||||
nonce = _nonce;
|
||||
poolMint = await serumCmn.createMint(provider, registrarSigner);
|
||||
});
|
||||
|
||||
it("Initializes registry's global state", async () => {
|
||||
await registry.state.rpc.new({
|
||||
accounts: { lockupProgram: lockup.programId },
|
||||
});
|
||||
|
||||
const state = await registry.state();
|
||||
assert.ok(state.lockupProgram.equals(lockup.programId));
|
||||
|
||||
// Should not allow a second initializatoin.
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await registry.state.rpc.new(lockup.programId);
|
||||
},
|
||||
(err) => {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("Initializes the registrar", async () => {
|
||||
await registry.rpc.initialize(
|
||||
mint,
|
||||
provider.wallet.publicKey,
|
||||
nonce,
|
||||
withdrawalTimelock,
|
||||
stakeRate,
|
||||
rewardQLen,
|
||||
{
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
poolMint,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
signers: [registrar, rewardQ],
|
||||
instructions: [
|
||||
await registry.account.registrar.createInstruction(registrar),
|
||||
await registry.account.rewardQueue.createInstruction(rewardQ, 8250),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
registrarAccount = await registry.account.registrar(registrar.publicKey);
|
||||
|
||||
assert.ok(registrarAccount.authority.equals(provider.wallet.publicKey));
|
||||
assert.equal(registrarAccount.nonce, nonce);
|
||||
assert.ok(registrarAccount.mint.equals(mint));
|
||||
assert.ok(registrarAccount.poolMint.equals(poolMint));
|
||||
assert.ok(registrarAccount.stakeRate.eq(stakeRate));
|
||||
assert.ok(registrarAccount.rewardEventQ.equals(rewardQ.publicKey));
|
||||
assert.ok(registrarAccount.withdrawalTimelock.eq(withdrawalTimelock));
|
||||
});
|
||||
|
||||
const member = new anchor.web3.Account();
|
||||
let memberAccount = null;
|
||||
let memberSigner = null;
|
||||
let balances = null;
|
||||
let balancesLocked = null;
|
||||
|
||||
it("Creates a member", async () => {
|
||||
const [
|
||||
_memberSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[registrar.publicKey.toBuffer(), member.publicKey.toBuffer()],
|
||||
registry.programId
|
||||
);
|
||||
memberSigner = _memberSigner;
|
||||
|
||||
const [mainTx, _balances] = await utils.createBalanceSandbox(
|
||||
provider,
|
||||
registrarAccount,
|
||||
memberSigner
|
||||
);
|
||||
const [lockedTx, _balancesLocked] = await utils.createBalanceSandbox(
|
||||
provider,
|
||||
registrarAccount,
|
||||
memberSigner
|
||||
);
|
||||
|
||||
balances = _balances;
|
||||
balancesLocked = _balancesLocked;
|
||||
|
||||
const tx = registry.transaction.createMember(nonce, {
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
memberSigner,
|
||||
balances,
|
||||
balancesLocked,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
instructions: [await registry.account.member.createInstruction(member)],
|
||||
});
|
||||
|
||||
const signers = [member, provider.wallet.payer];
|
||||
|
||||
const allTxs = [mainTx, lockedTx, { tx, signers }];
|
||||
|
||||
let txSigs = await provider.sendAll(allTxs);
|
||||
|
||||
memberAccount = await registry.account.member(member.publicKey);
|
||||
|
||||
assert.ok(memberAccount.registrar.equals(registrar.publicKey));
|
||||
assert.ok(memberAccount.beneficiary.equals(provider.wallet.publicKey));
|
||||
assert.ok(memberAccount.metadata.equals(new anchor.web3.PublicKey()));
|
||||
assert.equal(
|
||||
JSON.stringify(memberAccount.balances),
|
||||
JSON.stringify(balances)
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(memberAccount.balancesLocked),
|
||||
JSON.stringify(balancesLocked)
|
||||
);
|
||||
assert.ok(memberAccount.rewardsCursor === 0);
|
||||
assert.ok(memberAccount.lastStakeTs.eq(new anchor.BN(0)));
|
||||
});
|
||||
|
||||
it("Deposits (unlocked) to a member", async () => {
|
||||
const depositAmount = new anchor.BN(120);
|
||||
await registry.rpc.deposit(depositAmount, {
|
||||
accounts: {
|
||||
depositor: god,
|
||||
depositorAuthority: provider.wallet.publicKey,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
vault: memberAccount.balances.vault,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
member: member.publicKey,
|
||||
},
|
||||
});
|
||||
|
||||
const memberVault = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vault
|
||||
);
|
||||
assert.ok(memberVault.amount.eq(depositAmount));
|
||||
});
|
||||
|
||||
it("Stakes to a member (unlocked)", async () => {
|
||||
const stakeAmount = new anchor.BN(10);
|
||||
await registry.rpc.stake(stakeAmount, false, {
|
||||
accounts: {
|
||||
// Stake instance.
|
||||
registrar: registrar.publicKey,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
poolMint,
|
||||
// Member.
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
balances,
|
||||
balancesLocked,
|
||||
// Program signers.
|
||||
memberSigner,
|
||||
registrarSigner,
|
||||
// Misc.
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
},
|
||||
});
|
||||
|
||||
const vault = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vault
|
||||
);
|
||||
const vaultStake = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vaultStake
|
||||
);
|
||||
const spt = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.spt
|
||||
);
|
||||
|
||||
assert.ok(vault.amount.eq(new anchor.BN(100)));
|
||||
assert.ok(vaultStake.amount.eq(new anchor.BN(20)));
|
||||
assert.ok(spt.amount.eq(new anchor.BN(10)));
|
||||
});
|
||||
|
||||
const unlockedVendor = new anchor.web3.Account();
|
||||
const unlockedVendorVault = new anchor.web3.Account();
|
||||
let unlockedVendorSigner = null;
|
||||
|
||||
it("Drops an unlocked reward", async () => {
|
||||
const rewardKind = {
|
||||
unlocked: {},
|
||||
};
|
||||
const rewardAmount = new anchor.BN(200);
|
||||
const expiry = new anchor.BN(Date.now() / 1000 + 5);
|
||||
const [
|
||||
_vendorSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[registrar.publicKey.toBuffer(), unlockedVendor.publicKey.toBuffer()],
|
||||
registry.programId
|
||||
);
|
||||
unlockedVendorSigner = _vendorSigner;
|
||||
|
||||
await registry.rpc.dropReward(
|
||||
rewardKind,
|
||||
rewardAmount,
|
||||
expiry,
|
||||
provider.wallet.publicKey,
|
||||
nonce,
|
||||
{
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
poolMint,
|
||||
|
||||
vendor: unlockedVendor.publicKey,
|
||||
vendorVault: unlockedVendorVault.publicKey,
|
||||
|
||||
depositor: god,
|
||||
depositorAuthority: provider.wallet.publicKey,
|
||||
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
signers: [unlockedVendorVault, unlockedVendor],
|
||||
instructions: [
|
||||
...(await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
unlockedVendorVault.publicKey,
|
||||
mint,
|
||||
unlockedVendorSigner
|
||||
)),
|
||||
await registry.account.rewardVendor.createInstruction(unlockedVendor),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const vendorAccount = await registry.account.rewardVendor(
|
||||
unlockedVendor.publicKey
|
||||
);
|
||||
|
||||
assert.ok(vendorAccount.registrar.equals(registrar.publicKey));
|
||||
assert.ok(vendorAccount.vault.equals(unlockedVendorVault.publicKey));
|
||||
assert.ok(vendorAccount.nonce === nonce);
|
||||
assert.ok(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
|
||||
assert.ok(vendorAccount.expiryTs.eq(expiry));
|
||||
assert.ok(vendorAccount.expiryReceiver.equals(provider.wallet.publicKey));
|
||||
assert.ok(vendorAccount.total.eq(rewardAmount));
|
||||
assert.ok(vendorAccount.expired === false);
|
||||
assert.ok(vendorAccount.rewardEventQCursor === 0);
|
||||
assert.deepEqual(vendorAccount.kind, rewardKind);
|
||||
|
||||
const rewardQAccount = await registry.account.rewardQueue(
|
||||
rewardQ.publicKey
|
||||
);
|
||||
assert.ok(rewardQAccount.head === 1);
|
||||
assert.ok(rewardQAccount.tail === 0);
|
||||
const e = rewardQAccount.events[0];
|
||||
assert.ok(e.vendor.equals(unlockedVendor.publicKey));
|
||||
assert.equal(e.locked, false);
|
||||
});
|
||||
|
||||
it("Collects an unlocked reward", async () => {
|
||||
const token = await serumCmn.createTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
await registry.rpc.claimReward({
|
||||
accounts: {
|
||||
to: token,
|
||||
cmn: {
|
||||
registrar: registrar.publicKey,
|
||||
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
balances,
|
||||
balancesLocked,
|
||||
|
||||
vendor: unlockedVendor.publicKey,
|
||||
vault: unlockedVendorVault.publicKey,
|
||||
vendorSigner: unlockedVendorSigner,
|
||||
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let tokenAccount = await serumCmn.getTokenAccount(provider, token);
|
||||
assert.ok(tokenAccount.amount.eq(new anchor.BN(200)));
|
||||
|
||||
const memberAccount = await registry.account.member(member.publicKey);
|
||||
assert.ok(memberAccount.rewardsCursor == 1);
|
||||
});
|
||||
|
||||
const lockedVendor = new anchor.web3.Account();
|
||||
const lockedVendorVault = new anchor.web3.Account();
|
||||
let lockedVendorSigner = null;
|
||||
let lockedRewardAmount = null;
|
||||
let lockedRewardKind = null;
|
||||
|
||||
it("Drops a locked reward", async () => {
|
||||
lockedRewardKind = {
|
||||
locked: {
|
||||
startTs: new anchor.BN(Date.now() / 1000),
|
||||
endTs: new anchor.BN(Date.now() / 1000 + 6),
|
||||
periodCount: new anchor.BN(2),
|
||||
},
|
||||
};
|
||||
lockedRewardAmount = new anchor.BN(200);
|
||||
const expiry = new anchor.BN(Date.now() / 1000 + 5);
|
||||
const [
|
||||
_vendorSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[registrar.publicKey.toBuffer(), lockedVendor.publicKey.toBuffer()],
|
||||
registry.programId
|
||||
);
|
||||
lockedVendorSigner = _vendorSigner;
|
||||
|
||||
await registry.rpc.dropReward(
|
||||
lockedRewardKind,
|
||||
lockedRewardAmount,
|
||||
expiry,
|
||||
provider.wallet.publicKey,
|
||||
nonce,
|
||||
{
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
poolMint,
|
||||
|
||||
vendor: lockedVendor.publicKey,
|
||||
vendorVault: lockedVendorVault.publicKey,
|
||||
|
||||
depositor: god,
|
||||
depositorAuthority: provider.wallet.publicKey,
|
||||
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
signers: [lockedVendorVault, lockedVendor],
|
||||
instructions: [
|
||||
...(await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
lockedVendorVault.publicKey,
|
||||
mint,
|
||||
lockedVendorSigner
|
||||
)),
|
||||
await registry.account.rewardVendor.createInstruction(lockedVendor),
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
const vendorAccount = await registry.account.rewardVendor(
|
||||
lockedVendor.publicKey
|
||||
);
|
||||
|
||||
assert.ok(vendorAccount.registrar.equals(registrar.publicKey));
|
||||
assert.ok(vendorAccount.vault.equals(lockedVendorVault.publicKey));
|
||||
assert.ok(vendorAccount.nonce === nonce);
|
||||
assert.ok(vendorAccount.poolTokenSupply.eq(new anchor.BN(10)));
|
||||
assert.ok(vendorAccount.expiryTs.eq(expiry));
|
||||
assert.ok(vendorAccount.expiryReceiver.equals(provider.wallet.publicKey));
|
||||
assert.ok(vendorAccount.total.eq(lockedRewardAmount));
|
||||
assert.ok(vendorAccount.expired === false);
|
||||
assert.ok(vendorAccount.rewardEventQCursor === 1);
|
||||
assert.equal(
|
||||
JSON.stringify(vendorAccount.kind),
|
||||
JSON.stringify(lockedRewardKind)
|
||||
);
|
||||
|
||||
const rewardQAccount = await registry.account.rewardQueue(
|
||||
rewardQ.publicKey
|
||||
);
|
||||
assert.ok(rewardQAccount.head === 2);
|
||||
assert.ok(rewardQAccount.tail === 0);
|
||||
const e = rewardQAccount.events[1];
|
||||
assert.ok(e.vendor.equals(lockedVendor.publicKey));
|
||||
assert.ok(e.locked === true);
|
||||
});
|
||||
|
||||
let vendoredVesting = null;
|
||||
let vendoredVestingVault = null;
|
||||
let vendoredVestingSigner = null;
|
||||
|
||||
it("Claims a locked reward", async () => {
|
||||
vendoredVesting = new anchor.web3.Account();
|
||||
vendoredVestingVault = new anchor.web3.Account();
|
||||
let [
|
||||
_vendoredVestingSigner,
|
||||
nonce,
|
||||
] = await anchor.web3.PublicKey.findProgramAddress(
|
||||
[vendoredVesting.publicKey.toBuffer()],
|
||||
lockup.programId
|
||||
);
|
||||
vendoredVestingSigner = _vendoredVestingSigner;
|
||||
const remainingAccounts = lockup.instruction.createVesting
|
||||
.accounts({
|
||||
vesting: vendoredVesting.publicKey,
|
||||
vault: vendoredVestingVault.publicKey,
|
||||
depositor: lockedVendorVault.publicKey,
|
||||
depositorAuthority: lockedVendorSigner,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
})
|
||||
// Change the signer status on the vendor signer since it's signed by the program, not the
|
||||
// client.
|
||||
.map((meta) =>
|
||||
meta.pubkey === lockedVendorSigner ? { ...meta, isSigner: false } : meta
|
||||
);
|
||||
|
||||
await registry.rpc.claimRewardLocked(nonce, {
|
||||
accounts: {
|
||||
registry: await registry.state.address(),
|
||||
lockupProgram: lockup.programId,
|
||||
cmn: {
|
||||
registrar: registrar.publicKey,
|
||||
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
balances,
|
||||
balancesLocked,
|
||||
|
||||
vendor: lockedVendor.publicKey,
|
||||
vault: lockedVendorVault.publicKey,
|
||||
vendorSigner: lockedVendorSigner,
|
||||
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
},
|
||||
remainingAccounts,
|
||||
signers: [vendoredVesting, vendoredVestingVault],
|
||||
instructions: [
|
||||
await lockup.account.vesting.createInstruction(vendoredVesting),
|
||||
...(await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
vendoredVestingVault.publicKey,
|
||||
mint,
|
||||
vendoredVestingSigner
|
||||
)),
|
||||
],
|
||||
});
|
||||
|
||||
const lockupAccount = await lockup.account.vesting(
|
||||
vendoredVesting.publicKey
|
||||
);
|
||||
|
||||
assert.ok(lockupAccount.beneficiary.equals(provider.wallet.publicKey));
|
||||
assert.ok(lockupAccount.mint.equals(mint));
|
||||
assert.ok(lockupAccount.vault.equals(vendoredVestingVault.publicKey));
|
||||
assert.ok(lockupAccount.outstanding.eq(lockedRewardAmount));
|
||||
assert.ok(lockupAccount.startBalance.eq(lockedRewardAmount));
|
||||
assert.ok(lockupAccount.endTs.eq(lockedRewardKind.locked.endTs));
|
||||
assert.ok(
|
||||
lockupAccount.periodCount.eq(lockedRewardKind.locked.periodCount)
|
||||
);
|
||||
assert.ok(lockupAccount.whitelistOwned.eq(new anchor.BN(0)));
|
||||
assert.ok(lockupAccount.realizor.program.equals(registry.programId));
|
||||
assert.ok(lockupAccount.realizor.metadata.equals(member.publicKey));
|
||||
});
|
||||
|
||||
it("Waits for the lockup period to pass", async () => {
|
||||
await serumCmn.sleep(10 * 1000);
|
||||
});
|
||||
|
||||
it("Should fail to unlock an unrealized lockup reward", async () => {
|
||||
const token = await serumCmn.createTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
const withdrawAmount = new anchor.BN(10);
|
||||
await lockup.rpc.withdraw(withdrawAmount, {
|
||||
accounts: {
|
||||
vesting: vendoredVesting.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
token,
|
||||
vault: vendoredVestingVault.publicKey,
|
||||
vestingSigner: vendoredVestingSigner,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
// TODO: trait methods generated on the client. Until then, we need to manually
|
||||
// specify the account metas here.
|
||||
remainingAccounts: [
|
||||
{ pubkey: registry.programId, isWritable: false, isSigner: false },
|
||||
{ pubkey: member.publicKey, isWritable: false, isSigner: false },
|
||||
{ pubkey: balances.spt, isWritable: false, isSigner: false },
|
||||
{ pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
|
||||
],
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
// Solana doesn't propagate errors across CPI. So we receive the registry's error code,
|
||||
// not the lockup's.
|
||||
const errorCode = "custom program error: 0x78";
|
||||
assert.ok(err.toString().split(errorCode).length === 2);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const pendingWithdrawal = new anchor.web3.Account();
|
||||
|
||||
it("Unstakes (unlocked)", async () => {
|
||||
const unstakeAmount = new anchor.BN(10);
|
||||
|
||||
await registry.rpc.startUnstake(unstakeAmount, false, {
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
rewardEventQ: rewardQ.publicKey,
|
||||
poolMint,
|
||||
|
||||
pendingWithdrawal: pendingWithdrawal.publicKey,
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
balances,
|
||||
balancesLocked,
|
||||
|
||||
memberSigner,
|
||||
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
rent: anchor.web3.SYSVAR_RENT_PUBKEY,
|
||||
},
|
||||
signers: [pendingWithdrawal],
|
||||
instructions: [
|
||||
await registry.account.pendingWithdrawal.createInstruction(
|
||||
pendingWithdrawal
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const vaultPw = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vaultPw
|
||||
);
|
||||
const vaultStake = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vaultStake
|
||||
);
|
||||
const spt = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.spt
|
||||
);
|
||||
|
||||
assert.ok(vaultPw.amount.eq(new anchor.BN(20)));
|
||||
assert.ok(vaultStake.amount.eq(new anchor.BN(0)));
|
||||
assert.ok(spt.amount.eq(new anchor.BN(0)));
|
||||
});
|
||||
|
||||
const tryEndUnstake = async () => {
|
||||
await registry.rpc.endUnstake({
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
pendingWithdrawal: pendingWithdrawal.publicKey,
|
||||
|
||||
vault: balances.vault,
|
||||
vaultPw: balances.vaultPw,
|
||||
|
||||
memberSigner,
|
||||
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it("Fails to end unstaking before timelock", async () => {
|
||||
await assert.rejects(
|
||||
async () => {
|
||||
await tryEndUnstake();
|
||||
},
|
||||
(err) => {
|
||||
assert.equal(err.code, 109);
|
||||
assert.equal(err.msg, "The unstake timelock has not yet expired.");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("Waits for the unstake period to end", async () => {
|
||||
await serumCmn.sleep(5000);
|
||||
});
|
||||
|
||||
it("Unstake finalizes (unlocked)", async () => {
|
||||
await tryEndUnstake();
|
||||
|
||||
const vault = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vault
|
||||
);
|
||||
const vaultPw = await serumCmn.getTokenAccount(
|
||||
provider,
|
||||
memberAccount.balances.vaultPw
|
||||
);
|
||||
|
||||
assert.ok(vault.amount.eq(new anchor.BN(120)));
|
||||
assert.ok(vaultPw.amount.eq(new anchor.BN(0)));
|
||||
});
|
||||
|
||||
it("Withdraws deposits (unlocked)", async () => {
|
||||
const token = await serumCmn.createTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
const withdrawAmount = new anchor.BN(100);
|
||||
await registry.rpc.withdraw(withdrawAmount, {
|
||||
accounts: {
|
||||
registrar: registrar.publicKey,
|
||||
member: member.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
vault: memberAccount.balances.vault,
|
||||
memberSigner,
|
||||
depositor: token,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
},
|
||||
});
|
||||
|
||||
const tokenAccount = await serumCmn.getTokenAccount(provider, token);
|
||||
assert.ok(tokenAccount.amount.eq(withdrawAmount));
|
||||
});
|
||||
|
||||
it("Should succesfully unlock a locked reward after unstaking", async () => {
|
||||
const token = await serumCmn.createTokenAccount(
|
||||
provider,
|
||||
mint,
|
||||
provider.wallet.publicKey
|
||||
);
|
||||
|
||||
const withdrawAmount = new anchor.BN(7);
|
||||
await lockup.rpc.withdraw(withdrawAmount, {
|
||||
accounts: {
|
||||
vesting: vendoredVesting.publicKey,
|
||||
beneficiary: provider.wallet.publicKey,
|
||||
token,
|
||||
vault: vendoredVestingVault.publicKey,
|
||||
vestingSigner: vendoredVestingSigner,
|
||||
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
|
||||
clock: anchor.web3.SYSVAR_CLOCK_PUBKEY,
|
||||
},
|
||||
// TODO: trait methods generated on the client. Until then, we need to manually
|
||||
// specify the account metas here.
|
||||
remainingAccounts: [
|
||||
{ pubkey: registry.programId, isWritable: false, isSigner: false },
|
||||
{ pubkey: member.publicKey, isWritable: false, isSigner: false },
|
||||
{ pubkey: balances.spt, isWritable: false, isSigner: false },
|
||||
{ pubkey: balancesLocked.spt, isWritable: false, isSigner: false },
|
||||
],
|
||||
});
|
||||
const tokenAccount = await serumCmn.getTokenAccount(provider, token);
|
||||
assert.ok(tokenAccount.amount.eq(withdrawAmount));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
const anchor = require("@project-serum/anchor");
|
||||
const serumCmn = require("@project-serum/common");
|
||||
|
||||
async function createBalanceSandbox(provider, r, registrySigner) {
|
||||
const spt = new anchor.web3.Account();
|
||||
const vault = new anchor.web3.Account();
|
||||
const vaultStake = new anchor.web3.Account();
|
||||
const vaultPw = new anchor.web3.Account();
|
||||
|
||||
const lamports = await provider.connection.getMinimumBalanceForRentExemption(
|
||||
165
|
||||
);
|
||||
|
||||
const createSptIx = await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
spt.publicKey,
|
||||
r.poolMint,
|
||||
registrySigner,
|
||||
lamports
|
||||
);
|
||||
const createVaultIx = await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
vault.publicKey,
|
||||
r.mint,
|
||||
registrySigner,
|
||||
lamports
|
||||
);
|
||||
const createVaultStakeIx = await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
vaultStake.publicKey,
|
||||
r.mint,
|
||||
registrySigner,
|
||||
lamports
|
||||
);
|
||||
const createVaultPwIx = await serumCmn.createTokenAccountInstrs(
|
||||
provider,
|
||||
vaultPw.publicKey,
|
||||
r.mint,
|
||||
registrySigner,
|
||||
lamports
|
||||
);
|
||||
let tx0 = new anchor.web3.Transaction();
|
||||
tx0.add(
|
||||
...createSptIx,
|
||||
...createVaultIx,
|
||||
...createVaultStakeIx,
|
||||
...createVaultPwIx
|
||||
);
|
||||
let signers0 = [spt, vault, vaultStake, vaultPw];
|
||||
|
||||
const tx = { tx: tx0, signers: signers0 };
|
||||
|
||||
return [
|
||||
tx,
|
||||
{
|
||||
spt: spt.publicKey,
|
||||
vault: vault.publicKey,
|
||||
vaultStake: vaultStake.publicKey,
|
||||
vaultPw: vaultPw.publicKey,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createBalanceSandbox,
|
||||
};
|
Loading…
Reference in New Issue