Init repo

This commit is contained in:
armaniferrante 2021-03-11 07:31:15 -08:00
commit 3032a09886
No known key found for this signature in database
GPG Key ID: 58BEF301E91F7828
18 changed files with 4628 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.anchor
target

53
.travis.yml Normal file
View File

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

2
Anchor.toml Normal file
View File

@ -0,0 +1,2 @@
cluster = "localnet"
wallet = "~/.config/solana/id.json"

1006
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

4
Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = [
"programs/*"
]

38
README.md Normal file
View File

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

127
docs/lockups.md Normal file
View File

@ -0,0 +1,127 @@
# Lockups
WARNING: All code related to Lockups is unaudited. Use at your own risk.
## Introduction
The **Lockup** program provides a simple mechanism to lockup tokens
of any mint, and release those funds over time as defined by a vesting schedule.
Although these lockups track a target **beneficiary**, who will eventually receive the
funds upon vesting, a proper deployment of the program will ensure this **beneficiary**
can never actually retrieve tokens before vesting. Funds are *never* in an SPL
token wallet owned by a user, and are completely program controlled.
## Accounts
There is a single account type used by the program.
* `Vesting` - An account defining a vesting schedule, realization condition, and vault holding the tokens to be released over time.
## Creating a Vesting Account
Lockup occurs when tokens are transferred into the program creating a **Vesting**
account on behalf of a **beneficiary** via the `CreateVesting` instruction.
There are three parameters to specify:
* Start timestamp - unix timestamp (in seconds) of the time when vesting begins.
* End timestamp - unix timestamp (in seconds) of the time when all tokens will unlock.
* Period count - the amount of times vesting should occur.
* Deposit amount - the total amount to vest.
* Realizer - the program defining if and when vested tokens can be distributed to a beneficiary.
Together these parameters form a linearly unlocked vesting schedule. For example,
if one wanted to lock 100 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.

193
docs/staking.md Normal file
View File

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

179
migrations/deploy.js Normal file
View File

@ -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;
}

View File

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

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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)
}

534
programs/lockup/src/lib.rs Normal file
View File

@ -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;
}

View File

@ -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"] }

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

1355
programs/registry/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

946
tests/lockup.js Normal file
View File

@ -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));
});
});

66
tests/utils.js Normal file
View File

@ -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,
};