Compare commits
92 Commits
master
...
@solana/sp
Author | SHA1 | Date |
---|---|---|
|
9482f8d4e3 | |
|
1526301d0d | |
|
3045d7b1df | |
|
ea4b7e62fc | |
|
51cc3ed6fe | |
|
9aa238e0b4 | |
|
00eb817073 | |
|
4e1892adba | |
|
3714c70d12 | |
|
a05fd7185d | |
|
cf2fcde121 | |
|
3d04aa0109 | |
|
7320cf404b | |
|
975b524487 | |
|
a539ef021f | |
|
1efc90c5c9 | |
|
0ce5b41b9f | |
|
1f36ca301b | |
|
4068b77f53 | |
|
512497aa39 | |
|
d9fd11a8f3 | |
|
9e4764faf7 | |
|
0ed080a50f | |
|
3dd6767297 | |
|
b99c9b375c | |
|
9c63bc0b06 | |
|
5a357a50df | |
|
0f4f2b8de9 | |
|
60ef11e26b | |
|
5e320ba976 | |
|
636407d7a9 | |
|
11e207cc85 | |
|
14bdbdc3ac | |
|
7d666b86ce | |
|
81ab529311 | |
|
c9a5289aa0 | |
|
ca6d57991a | |
|
99aaab0993 | |
|
1e28a427a4 | |
|
61a53abf6f | |
|
1e47030549 | |
|
9ad4168253 | |
|
dfc5cc5a23 | |
|
c149b0a46e | |
|
804a61e558 | |
|
3613ffe3b0 | |
|
08c4cb530a | |
|
fdba05714d | |
|
53c86493e6 | |
|
40ebfc6917 | |
|
30671aa5b3 | |
|
cf8eeb0720 | |
|
df994bf426 | |
|
6fee08be2f | |
|
a5c4b1e071 | |
|
589da55e29 | |
|
ebc16782bb | |
|
18468b513f | |
|
7a0e5aa14e | |
|
9dd807c893 | |
|
71e5e556c4 | |
|
d3e26d089b | |
|
48a0f81ab6 | |
|
9281b6e828 | |
|
96901b1299 | |
|
b8a773fddd | |
|
4374d8dee4 | |
|
6a52ba7d92 | |
|
24ea32aa48 | |
|
1ff9f789e7 | |
|
529b070e2b | |
|
13f8430b77 | |
|
b581ab3319 | |
|
31d5640706 | |
|
34571b624f | |
|
2b3f71ead5 | |
|
2ef336fd0a | |
|
78ab468781 | |
|
0d5146e30c | |
|
34583bf748 | |
|
fa5c34dc84 | |
|
4d1bb013fb | |
|
3ab19ba514 | |
|
e8f59e42ba | |
|
7f89183c0d | |
|
17dd53d5e8 | |
|
8f325dcd2d | |
|
0e2b08066b | |
|
c01665832a | |
|
ee52f1d499 | |
|
092432f1e1 | |
|
f309df4f35 |
|
@ -8,3 +8,4 @@ node_modules
|
|||
hfuzz_target
|
||||
hfuzz_workspace
|
||||
**/*.so
|
||||
**/.DS_Store
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -11,12 +11,12 @@ members = [
|
|||
"feature-proposal/cli",
|
||||
"libraries/math",
|
||||
"memo/program",
|
||||
"name-service/program",
|
||||
"record/program",
|
||||
"shared-memory/program",
|
||||
"stake-pool/cli",
|
||||
"stake-pool/program",
|
||||
"token-lending/program",
|
||||
"token-lending/client",
|
||||
"token-swap/program",
|
||||
"token-swap/program/fuzz",
|
||||
"token/cli",
|
||||
|
@ -29,3 +29,6 @@ exclude = [
|
|||
"themis/program_ristretto",
|
||||
"token/perf-monitor", # TODO: Rework perf-monitor to use solana-program-test, avoiding the need to link directly with the BPF VM
|
||||
]
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
|
|
@ -12,12 +12,12 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,7 +13,7 @@ test-bpf = []
|
|||
[dependencies]
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
@ -21,8 +21,8 @@ arbitrary = { version = "0.4", features = ["derive"], optional = true }
|
|||
borsh = "0.8.2"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||
stable_version="$RUST_STABLE_VERSION"
|
||||
else
|
||||
stable_version=1.50.0
|
||||
stable_version=1.51.0
|
||||
fi
|
||||
|
||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||
else
|
||||
nightly_version=2021-02-18
|
||||
nightly_version=2021-04-18
|
||||
fi
|
||||
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
if [[ -n $SOLANA_VERSION ]]; then
|
||||
solana_version="$SOLANA_VERSION"
|
||||
else
|
||||
solana_version=v1.5.15
|
||||
solana_version=v1.6.2
|
||||
fi
|
||||
|
||||
export solana_version="$solana_version"
|
||||
|
|
|
@ -8,6 +8,7 @@ module.exports = {
|
|||
"token-lending",
|
||||
"associated-token-account",
|
||||
"memo",
|
||||
"name-service",
|
||||
"shared-memory",
|
||||
"stake-pool",
|
||||
"feature-proposal",
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
---
|
||||
title: Name Service
|
||||
---
|
||||
|
||||
A SPL program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, twitter handles, ipfs cid's etc..
|
||||
|
||||
This program could be used for dns, pubkey etc lookups via a browser extension
|
||||
for example, the goal is to create an easy way to identify Solana public keys
|
||||
with various links.
|
||||
|
||||
Broader use cases are also imaginable.
|
||||
|
||||
Key points:
|
||||
- A Name is a string that maps to a record (program derived account) which can hold data.
|
||||
- Each name is of a certain class and has a certain owner, both are identified
|
||||
by their pubkeys. The class of a name needs to sign the issuance of it.
|
||||
- A name can have a parent name that is identified by the address of its record.
|
||||
The owner of the parent name (when it exists) needs to sign the issuance of
|
||||
the child name.
|
||||
- The data of a name registry is controlled byb the class keypair or, when it is
|
||||
set to `Pubkey::default()`, by the name owner keypair.
|
||||
- Only the owner can delete a name registry.
|
||||
|
||||
|
||||
Remarks and use cases:
|
||||
- Domain name declarations: One could arbitrarly set-up a class that we can call
|
||||
Top-Level-Domain names. Names in this class can only be issued with the
|
||||
permission of the class keypair, ie the administrator, who can enforce that
|
||||
TLD names are of the type `".something"`. From then on one could create and
|
||||
own the TLD `".sol"` and create a class of ".sol" sub-domains, administrating
|
||||
the issuance of the `"something.sol"` sub-domains that way (by setting tha
|
||||
parent name to the address of the `".sol"` registry).
|
||||
|
||||
An off-chain browser extension could then, similarly to DNS, parse the user SPL
|
||||
name service URL input and descend the chain of names, verifying that the names
|
||||
exist with the correct parenthood, and finally use the data of the last child
|
||||
name (or also a combination of the parents data) in order to resolve this call
|
||||
towards a real DNS URL or any kind of data.
|
||||
|
||||
Although the ownership and class system makes the administration a given class
|
||||
centralized, the creation of new classes is permissionless and as a class owner
|
||||
any kind of decentralized governance signing program could be used.
|
||||
|
||||
- Twitter handles can be added as names of one specific name class. The class
|
||||
authority of will therefore hold the right to add a twitter handle name. This
|
||||
enables the verification of twitter accounts for example by asking the user to
|
||||
tweet his pubkey or a signed message. A bot that holds the private issuing
|
||||
authority key can then sign the Create instruction (with a metadata_authority
|
||||
that is the tweeted pubkey) and send it back to the user who will then submit
|
||||
it to the program.
|
||||
In this case the class will still be able to control the data of the name registry, and not the user for example.
|
||||
|
||||
Therefore, another way of using this program would be to create a name
|
||||
(`"verified-twitter-handles"` for example) with the `Pubkey::default()` class
|
||||
and with the owner being the authority. That way verified twitter names could be
|
||||
issued as child names of this parent by the owner, leaving the user as being to
|
||||
able to modify the data of his twitter name registry.
|
|
@ -3,7 +3,7 @@ title: Stake Pool Program
|
|||
---
|
||||
|
||||
A program for pooling together SOL to be staked by an off-chain agent running
|
||||
a Delegation bot which redistributes the stakes across the network and tries
|
||||
a Delegation Bot which redistributes the stakes across the network and tries
|
||||
to maximize censorship resistance and rewards.
|
||||
|
||||
## Overview
|
||||
|
@ -14,7 +14,7 @@ inflation rate, total number of SOL staked on the network, and an individual
|
|||
validator’s uptime and commission (fee).
|
||||
|
||||
Stake pools are an alternative method of earning staking rewards. This on-chain
|
||||
program pools together SOL to be staked by a manager, allowing SOL holders to
|
||||
program pools together SOL to be staked by a staker, allowing SOL holders to
|
||||
stake and earn rewards without managing stakes.
|
||||
|
||||
Additional information regarding staking and stake programming is available at:
|
||||
|
@ -24,16 +24,18 @@ Additional information regarding staking and stake programming is available at:
|
|||
|
||||
## Motivation
|
||||
|
||||
This document is intended for stake pool managers who want to create or manage
|
||||
stake pools, and users who want to provide staked SOL into an existing stake
|
||||
pool.
|
||||
This document is intended for the main actors of the stake pool system:
|
||||
|
||||
* manager: creates and manages the stake pool, earns fees, can update the fee, staker, and manager
|
||||
* staker: adds and removes validators to the pool, rebalances stake among validators
|
||||
* user: provides staked SOL into an existing stake pool
|
||||
|
||||
In its current iteration, the stake pool only processes totally active stakes.
|
||||
Deposits must come from fully active stakes, and withdrawals return a fully
|
||||
active stake account.
|
||||
|
||||
This means that stake pool managers and users must be comfortable with creating
|
||||
and delegating stakes, which are more advanced operations than sending and
|
||||
This means that stake pool managers, stakers, and users must be comfortable with
|
||||
creating and delegating stakes, which are more advanced operations than sending and
|
||||
receiving SPL tokens and SOL. Additional information on stake operations are
|
||||
available at:
|
||||
|
||||
|
@ -46,27 +48,28 @@ like [Token Swap](token-swap.md).
|
|||
|
||||
## Operation
|
||||
|
||||
A stake pool manager creates a stake pool and includes validators that will
|
||||
A stake pool manager creates a stake pool, and the staker includes validators that will
|
||||
receive delegations from the pool by creating "validator stake accounts" and
|
||||
activating a delegation on them. Once a validator stake account's delegation is
|
||||
active, the stake pool manager adds it to the stake pool.
|
||||
active, the staker adds it to the stake pool.
|
||||
|
||||
At this point, users can participate with deposits. They must delegate a stake
|
||||
account to the one of the validators in the stake pool. Once it's active, the
|
||||
user can deposit their stake into the pool in exchange for SPL staking derivatives
|
||||
representing their fractional ownership in pool. A percentage of the user's
|
||||
deposit goes to the pool manager as a fee.
|
||||
representing their fractional ownership in pool. A percentage of the rewards
|
||||
earned by the pool goes to the pool manager as a fee.
|
||||
|
||||
Over time, as the stake pool accrues staking rewards, the user's fractional
|
||||
Over time, as the stakes in the stake pool accrue staking rewards, the user's fractional
|
||||
ownership will be worth more than their initial deposit. Whenever the user chooses,
|
||||
they can withdraw their SPL staking derivatives in exchange for an activated stake.
|
||||
|
||||
The stake pool manager can add and remove validators, or rebalance the pool by
|
||||
withdrawing stakes from the pool, deactivating them, reactivating them on another
|
||||
validator, then depositing back into the pool.
|
||||
The stake pool staker can add and remove validators, or rebalance the pool by
|
||||
decreasing the stake on a validator, waiting an epoch to move it into the stake
|
||||
pool's reserve account, then increasing the stake on another validator.
|
||||
|
||||
These manager operations require SPL staking derivatives and staked SOL, so the
|
||||
stake pool manager will need liquidity on hand to properly manage the pool.
|
||||
The staker operation to add a new validator requires roughly 1.003 SOL to create
|
||||
the stake account on a validator, so the stake pool staker will need liquidity
|
||||
on hand to fully manage the pool stakes.
|
||||
|
||||
## Background
|
||||
|
||||
|
@ -130,32 +133,105 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
|||
solana config set --keypair usb://ledger/
|
||||
```
|
||||
|
||||
### Stake Pool Administrator Examples
|
||||
#### Run Locally
|
||||
|
||||
If you would like to test a stake pool locally without having to wait for stakes
|
||||
to activate and deactivate, you can run the stake pool locally using the
|
||||
`solana-test-validator` tool with shorter epochs, and pulling the current program
|
||||
from devnet, testnet, or mainnet.
|
||||
|
||||
```sh
|
||||
$ solana-test-validator -c poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj --url devnet --slots-per-epoch 32
|
||||
$ solana config set --url http://127.0.0.1:8899
|
||||
```
|
||||
|
||||
### Stake Pool Manager Examples
|
||||
|
||||
#### Create a stake pool
|
||||
|
||||
The pool administrator manages the stake accounts in a stake pool, and in exchange
|
||||
receives a fee in the form of SPL token staking derivatives. The administrator
|
||||
sets the fee on creation. Let's create a pool with a 3% fee:
|
||||
The stake pool manager controls the stake pool from a high level, and in exchange
|
||||
receives a fee in the form of SPL token staking derivatives. The manager
|
||||
sets the fee on creation. Let's create a pool with a 3% fee and a maximum of 1000
|
||||
validator stake accounts:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100
|
||||
Creating mint Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
||||
Creating pool fee collection account 3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5
|
||||
Creating stake pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Signature: 5HdDoPssqwyLjt2QvhRbnSATZqFLGKha92zMuJiBUpKeKYKGURRV41N5ydCQxqnFjCud3xv85Z6ghErppNJzaYM8
|
||||
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100 --max-validators 1000
|
||||
Creating reserve stake 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J
|
||||
Creating mint D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ
|
||||
Creating pool fee collection account 5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE
|
||||
Signature: 2dvCtHMcqxibckhvVgFQeFCRb7VcHbuFLRf71Aqd9PtzFzdbG3gAkNpxYznfpKDx2vTRrVtwW81sZAx5U3Frb5Uu
|
||||
Creating stake pool EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Signature: 2kYDVyJp8FVrLmEZyW9ivMYcXEsgWm4hFyhp5omxVtonjhYG6WS1S85sPTCdsQWe3idof6ZqsY8F3oaMXwrEkAYK
|
||||
```
|
||||
|
||||
The unique stake pool identifier is `3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC`.
|
||||
The unique stake pool identifier is `EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1`.
|
||||
|
||||
The identifier for the SPL token for staking derivatives is
|
||||
`Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS`. The stake pool has full control
|
||||
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ`. The stake pool has full control
|
||||
over the mint.
|
||||
|
||||
The pool creator's fee account identifier is
|
||||
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. When users deposit warmed up
|
||||
stake accounts into the stake pool, the program will transfer 3% of their
|
||||
contribution into this account in the form of SPL token staking derivatives.
|
||||
`5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE`. Every epoch, as stake accounts
|
||||
in the stake pool earn rewards, the program will mint SPL token staking derivatives
|
||||
equal to 3% of the gains on that epoch into this account. If no gains were observed,
|
||||
nothing will be deposited.
|
||||
|
||||
The reserve stake account identifier is `33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J`.
|
||||
This account holds onto additional stake used when rebalancing between validators.
|
||||
|
||||
For a stake pool with 1000 validators, the cost to create a stake pool is less
|
||||
than 0.5 SOL.
|
||||
|
||||
#### Set manager
|
||||
|
||||
The stake pool manager may pass their administrator privileges to another account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-manager 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
At the same time, they may also change the SPL token account that receives fees
|
||||
every epoch. The mint for the provided token account must be the SPL token mint,
|
||||
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ` in our example.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-fee-receiver HoCsh97wRxRXVjtG7dyfsXSwH9VxdDzC7GvAsBE1eqJz
|
||||
Signature: 4aK8yzYvPBkP4PyuXTcCm529kjEH6tTt4ixc5D5ZyCrHwc4pvxAHj6wcr4cpAE1e3LddE87J1GLD466aiifcXoAY
|
||||
```
|
||||
|
||||
#### Set fee
|
||||
|
||||
The stake pool manager may update the fee assessed every epoch, passing the
|
||||
numerator and denominator for the fraction that make up the fee. For a fee of
|
||||
10%, they could run:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-fee EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 10 100
|
||||
Signature: 5yPXfVj5cbKBfZiEVi2UR5bXzVDuc2c3ruBwSjkAqpvxPHigwGHiS1mXQVE4qwok5moMWT5RNYAMvkE9bnfQ1i93
|
||||
```
|
||||
|
||||
#### Set staker
|
||||
|
||||
In order to manage the stake accounts, the stake pool manager or
|
||||
staker can set the staker authority of the stake pool's managed accounts.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-staker EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
Now, the new staker can perform any normal stake pool operations, including
|
||||
adding and removing validators and rebalancing stake.
|
||||
|
||||
Important security note: the stake pool program only gives staking authority to
|
||||
the pool staker and always retains withdraw authority. Therefore, a malicious
|
||||
stake pool staker cannot steal funds from the stake pool.
|
||||
|
||||
Note: to avoid "disturbing the manager", the staker can also reassign their stake
|
||||
authority.
|
||||
|
||||
### Stake Pool Staker Examples
|
||||
|
||||
#### Create a validator stake account
|
||||
|
||||
|
@ -170,7 +246,7 @@ lists, we choose some validators at random and start with identity
|
|||
delegated to that vote account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
|
||||
```
|
||||
|
@ -179,13 +255,13 @@ In order to maximize censorship resistance, we want to distribute our SOL to as
|
|||
many validators as possible, so let's add a few more.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
||||
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
|
||||
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
|
||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
||||
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
||||
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
||||
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
|
||||
```
|
||||
|
@ -235,22 +311,21 @@ We created new validator stake accounts in the last step and staked them. Once
|
|||
the stake activates, we can add them to the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool add-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Creating account to receive tokens Gu8xqzYFg2sPHWHhUivKNBeF9uikiauihLs9hLzziKu7
|
||||
$ spl-stake-pool add-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV
|
||||
```
|
||||
|
||||
Users can start depositing their activated stakes into the stake pool, as
|
||||
long as they are delegated to the same vote account, which was
|
||||
`FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` in this example. You can also
|
||||
`FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN` in this example. You can also
|
||||
double-check that at any time using the Solana command-line utility.
|
||||
|
||||
```sh
|
||||
$ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Balance: 0.002282881 SOL
|
||||
Rent Exempt Reserve: 0.00228288 SOL
|
||||
Delegated Stake: 0.000000001 SOL
|
||||
Active Stake: 0.000000001 SOL
|
||||
Delegated Stake: 1.000000000 SOL
|
||||
Active Stake: 1.000000000 SOL
|
||||
Activating Stake: 0 SOL
|
||||
Stake activates starting from epoch: 161
|
||||
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
|
@ -260,26 +335,31 @@ Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
|||
|
||||
#### Remove validator stake account
|
||||
|
||||
If the stake pool manager wants to stop delegating to a vote account, they can
|
||||
totally remove the validator stake account from the stake pool by providing
|
||||
staking derivatives, just like `withdraw`.
|
||||
If the stake pool staker wants to stop delegating to a vote account, they can
|
||||
totally remove the validator stake account from the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool remove-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||
```
|
||||
|
||||
The difference with `withdraw` is that the validator stake account is totally
|
||||
removed from the stake pool and now belongs to the administrator.
|
||||
removed from the stake pool and now belongs to the administrator. The authority
|
||||
for the withdrawn stake account can also be specified using the `--new-authority` flag:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G --new-authority 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||
```
|
||||
|
||||
We can check the removed stake account:
|
||||
|
||||
```sh
|
||||
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||
Balance: 1.002282881 SOL
|
||||
Balance: 1.002282880 SOL
|
||||
Rent Exempt Reserve: 0.00228288 SOL
|
||||
Delegated Stake: 1.000000001 SOL
|
||||
Active Stake: 1.000000001 SOL
|
||||
Delegated Stake: 1.000000000 SOL
|
||||
Active Stake: 1.000000000 SOL
|
||||
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
|
@ -291,7 +371,7 @@ removal of staked SOL from the pool.
|
|||
We can also double-check that the stake pool no longer shows the stake account:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||
|
@ -300,14 +380,14 @@ Total: ◎15.849959206
|
|||
|
||||
#### Rebalance the stake pool
|
||||
|
||||
As time goes on, deposits and withdrawals will happen to all of the stake accounts
|
||||
managed by the pool, and the stake pool manager may want to rebalance the stakes.
|
||||
As time goes on, users will deposit to and withdraw from all of the stake accounts
|
||||
managed by the pool, and the stake pool staker may want to rebalance the stakes.
|
||||
|
||||
For example, let's say the manager wants the same delegation to every validator
|
||||
For example, let's say the staker wants the same delegation to every validator
|
||||
in the pool. When they look at the state of the pool, they see:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||
|
@ -315,75 +395,63 @@ Total: ◎15.849959206
|
|||
```
|
||||
|
||||
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
|
||||
has too much allocated. For their strategy, the manager wants the `15.849959206`
|
||||
has too much allocated. For their strategy, the staker wants the `15.849959206`
|
||||
SOL to be distributed evenly, meaning around `5.283319735` in each account. They need
|
||||
to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and
|
||||
`1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`.
|
||||
|
||||
First, they need to withdraw a total of `6.153483916` from
|
||||
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`. Using the `spl-token` utility,
|
||||
let's check the total supply of pool tokens:
|
||||
##### Decrease validator stake
|
||||
|
||||
First, they need to decrease the amount on stake account
|
||||
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`, delegated to
|
||||
`HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz`, by total of `6.153483916` SOL.
|
||||
|
||||
They decrease that amount of SOL:
|
||||
|
||||
```sh
|
||||
$ spl-token supply Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
||||
0.034692168
|
||||
$ spl-stake-pool decrease-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz 6.153483916
|
||||
Signature: ZpQGwT85rJ8Y9afdkXhKo3TVv4xgTz741mmZj2vW7mihYseAkFsazWxza2y8eNGY4HDJm15c1cStwyiQzaM3RpH
|
||||
```
|
||||
|
||||
Given a total pool token supply of `0.034692168` and total staked SOL amount of
|
||||
`15.849959206`, let's calculate how many pool tokens to withdraw from the pool:
|
||||
Internally, this instruction splits and deactivates 6.153483916 SOL from the
|
||||
validator stake account `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie` into a
|
||||
transient stake account, owned and managed entirely by the stake pool.
|
||||
|
||||
```
|
||||
sol_to_withdraw * total_pool_tokens / total_sol_staked = pool_tokens_to_withdraw
|
||||
6.153483916 * 0.034692168 / 15.849959206 ~ 0.013468659
|
||||
```
|
||||
Once the stake is deactivated during the next epoch, the `update` command will
|
||||
automatically merge the transient stake account into a reserve stake account,
|
||||
also entirely owned and managed by the stake pool.
|
||||
|
||||
They withdraw that amount of pool tokens:
|
||||
##### Increase validator stake
|
||||
|
||||
Now that the reserve stake account has enough to perform the rebalance, the staker
|
||||
can increase the stake on the two other validators,
|
||||
`8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm` and
|
||||
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
|
||||
|
||||
They add 4.281036854 SOL to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.013468659 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Withdrawing from account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie, amount ◎6.153483855, 0.013468659 pool tokens
|
||||
Creating account to receive stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
||||
Signature: z8a5ZRfWdj8Fcsr3ttCJ731wFKyhZNcqoKEdV1RBCkzr3tHGQNCC56qvRVJ6oxyCVDqWZ3KL1Bkyn3sDpjYPDku
|
||||
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm 4.281036854
|
||||
Signature: 3GJACzjUGLPjcd9RLUW86AfBLWKapZRkxnEMc2yHT6erYtcKBgCapzyrVH6VN8Utxj7e2mtvzcigwLm6ZafXyTMw
|
||||
```
|
||||
|
||||
Because of rounding in the calculation a few lines above, it looks like we receive
|
||||
less than we should. If we play that back the other way, we'll see that all is well:
|
||||
|
||||
```
|
||||
pool_tokens_to_withdraw * total_sol_staked / total_pool_tokens = sol_to_withdraw
|
||||
0.013468659 * 15.849959206 / 0.034692168 ~ 6.153483855
|
||||
```
|
||||
|
||||
Next, they deactivate the new received stake:
|
||||
And they add 1.872447062 SOL to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`:
|
||||
|
||||
```sh
|
||||
$ solana deactivate-stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
||||
Signature: 4SuwZK5JvYkYVkM5yfu2x8x6iou6558teMwzphGECLmstMVoWbSvngUH48Ra24PrxtgUDyVDA8SXYS1qMyx3fjMj
|
||||
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 1.872447062
|
||||
Signature: 4zaKYu3MQ3as8reLbuHKaXN8FNaHvpHuiZtsJeARo67UKMo6wUUoWE88Fy8N4EYQYicuwULTNffcUD3a9jY88PoU
|
||||
```
|
||||
|
||||
Once the stake is deactivated during the next epoch, they split the stake
|
||||
and activate it on the other two validator vote accounts. For brevity, those
|
||||
commands are omitted.
|
||||
Internally, this instruction also uses transient stake accounts. This time, the
|
||||
stake pool splits from the reserve stake, into the transient stake account,
|
||||
then activates it to the appropriate validator.
|
||||
|
||||
Eventually, we are left with stake account `4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK`
|
||||
with `4.281036854` delegated to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`
|
||||
and stake account `GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf` with `1.872447062`
|
||||
delegated to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
|
||||
|
||||
Once the new stakes are ready, the manager deposits them back into the stake pool:
|
||||
```sh
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: jKsdEr3zxF2zZs78rmrP3PmQiTwE7v15ieEuxp4db1VQe9owXVGM8nM3dJqVRHXPsS4frQW4gJ6xBfTTk2HvKDX
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
||||
Signature: 3JXvTvea6F4Epd2krSxnTRZPB4gLZ8GqisFE58Z4ocV92fDN1HRMVPoPhJtYcfuF12vyQZUueKwVmkvL6Wgf2evc
|
||||
```
|
||||
|
||||
Leaving them with a rebalanced stake pool!
|
||||
One to two epochs later, once the transient stakes activate, the `update` command
|
||||
automatically merges the transient stakes into the validator stake account, leaving
|
||||
a fully rebalanced stake pool:
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
|
||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
|
||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
|
||||
|
@ -391,33 +459,7 @@ Total: ◎15.851269888
|
|||
```
|
||||
|
||||
Due to staking rewards that accrued during the rebalancing process, the pool is
|
||||
not prefectly balanced. This is completely normal.
|
||||
|
||||
#### Set staking authority
|
||||
|
||||
In order to manage the stake accounts more directly, the stake pool owner can
|
||||
set the stake authority of the stake pool's managed accounts.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool set-staking-auth 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN --new-staker 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
|
||||
Now, the new staking authority can perform any normal staking operations,
|
||||
including deactivating or re-staking.
|
||||
|
||||
Important security note: the stake pool program only gives staking authority to
|
||||
the pool owner and always retains withdraw authority. Therefore, a malicious
|
||||
stake pool manager cannot steal funds from the stake pool.
|
||||
|
||||
#### Set owner
|
||||
|
||||
The stake pool owner may pass their administrator privileges to another account.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --new-owner 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||
```
|
||||
not perfectly balanced. This is completely normal.
|
||||
|
||||
### User Examples
|
||||
|
||||
|
@ -429,7 +471,7 @@ command-line utility has a special instruction for finding out which vote
|
|||
accounts are already associated with the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
|
||||
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
|
||||
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
|
||||
|
@ -441,13 +483,13 @@ If the manager has recently created the stake pool, and there are no stake
|
|||
accounts present yet, the command-line utility will inform us.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
No accounts found.
|
||||
```
|
||||
|
||||
#### Deposit stake
|
||||
|
||||
Stake pools only accept deposits from fully staked accounts, so we must first
|
||||
Stake pools only accept deposits from active accounts, so we must first
|
||||
create stake accounts and delegate them to one of the validators managed by the
|
||||
stake pool. Using the `list` command from the previous section, we see that
|
||||
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's
|
||||
|
@ -473,17 +515,19 @@ Two epochs later, when the stake is fully active and has received one epoch of
|
|||
rewards, we can deposit the stake into the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
||||
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
||||
```
|
||||
|
||||
The CLI will default to using the fee payer's
|
||||
[Associated Token Account](associated-token-account.md) for stake pool tokens.
|
||||
Alternatively, you can create an SPL token account yourself and pass it as the
|
||||
`token-receiver` for the command.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
||||
```
|
||||
|
@ -505,7 +549,8 @@ In order to calculate the proper value of these stake pool tokens, we must updat
|
|||
the total value managed by the stake pool every epoch.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Updating stake pool...
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
|
||||
|
@ -513,13 +558,33 @@ If another user already updated the stake pool balance for the current epoch, we
|
|||
see a different output.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
||||
Stake pool balances are up to date, no update required.
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||
Update not required
|
||||
```
|
||||
|
||||
If no one updates the stake pool in the current epoch, the deposit and withdraw
|
||||
instructions will fail. The update instruction is permissionless, so any user
|
||||
can run it before depositing or withdrawing.
|
||||
can run it before depositing or withdrawing. As a convenience, the CLI attempts
|
||||
to update before running any instruction on the stake pool.
|
||||
|
||||
If the stake pool transient stakes are in an unexpected state, and merges are
|
||||
not possible, there is the option to only update the stake pool balances without
|
||||
performing merges using the `--no-merge` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --no-merge
|
||||
Updating stake pool...
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
|
||||
Later on, whenever the transient stakes are ready to be merged, it is possible to
|
||||
force another update in the same epoch using the `--force` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --force
|
||||
Updating stake pool...
|
||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||
```
|
||||
|
||||
#### Withdraw stake
|
||||
|
||||
|
@ -529,7 +594,7 @@ staking derivative SPL tokens in exchange for an activated stake account.
|
|||
Let's withdraw 0.02 staking derivative tokens from the stake pool.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
|
@ -550,15 +615,58 @@ Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
|||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||
```
|
||||
|
||||
Alternatively, the user can specify an existing stake account to receive their
|
||||
stake using the `stake-receiver` parameter.
|
||||
Alternatively, the user can specify an existing uninitialized stake account to
|
||||
receive their stake using the `--stake-receiver` parameter.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
By default, the withdraw command uses the fee payer's associated token account to
|
||||
source the derivative tokens. It's possible to specify the SPL token account using
|
||||
the `--pool-account` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --pool-account 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
By default, the withdraw command will withdraw from the largest validator stake
|
||||
accounts in the pool. It's also possible to specify a specific vote account for
|
||||
the withdraw using the `--vote-account` flag.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --vote-account 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||
```
|
||||
|
||||
Note that the associated validator stake account must have enough lamports to
|
||||
satisfy the pool token amount requested.
|
||||
|
||||
##### Special case: exiting pool with a delinquent staker
|
||||
|
||||
With the reserve stake, it's possible for a delinquent or malicious staker to
|
||||
move all stake into the reserve through `decrease-validator-stake`, so the
|
||||
staking derivatives will not gain rewards, and the stake pool users will not
|
||||
be able to withdraw their funds.
|
||||
|
||||
To get around this case, it is also possible to withdraw from the stake pool's
|
||||
reserve, but only if all of the validator stake accounts are at the minimum amount of
|
||||
`1 SOL + stake account rent exemption`.
|
||||
|
||||
```sh
|
||||
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02 --use-reserve
|
||||
Withdrawing from account 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J, amount 8.867176377 SOL, 0.02 pool tokens
|
||||
Creating account to receive stake 9E5YzXXu9NDhtMxWJKCwe2M8Sdz6vL6bcBS92U76PVtE
|
||||
Signature: 4aZaeT9Azcq23PdKcjbQLseNveZVAQ4xMabBGQspfX316cE62Q2hoES373ExbT9y2JUhug7SgdybNaCjuZ6uqNYf
|
||||
```
|
||||
|
||||
## Appendix
|
||||
|
||||
### Activated stakes
|
||||
|
@ -569,6 +677,22 @@ are not equivalent to inactive, activating, or deactivating stakes due to the
|
|||
time cost of staking. Otherwise, malicious actors can deposit stake in one state
|
||||
and withdraw it in another state without waiting.
|
||||
|
||||
### Transient stake accounts
|
||||
|
||||
Each validator gets one transient stake account, so the staker can only
|
||||
perform one action at a time on a validator. It's impossible to increase
|
||||
and decrease the stake on a validator at the same time. The staker must wait for
|
||||
the existing transient stake account to get merged during an `update` instruction
|
||||
before performing a new action.
|
||||
|
||||
### Reserve stake account
|
||||
|
||||
Every stake pool is initialized with an undelegated reserve stake account, used
|
||||
to hold undelegated stake in process of rebalancing. After the staker decreases
|
||||
the stake on a validator, one epoch later, the update operation will merge the
|
||||
decreased stake into the reserve. Conversely, whenever the staker increases the
|
||||
stake on a validator, the lamports are drawn from the reserve stake account.
|
||||
|
||||
### Staking Credits Observed on Deposit
|
||||
|
||||
A deposited stake account's "credits observed" must match the destination
|
||||
|
|
|
@ -83,6 +83,15 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
|||
solana config set --keypair usb://ledger/
|
||||
```
|
||||
|
||||
#### Airdrop SOL
|
||||
|
||||
Creating tokens and accounts requires SOL for account rent deposits and
|
||||
transaction fees. If the cluster you are targeting offers a faucet, you can get
|
||||
a little SOL for testing:
|
||||
```
|
||||
solana airdrop 1
|
||||
```
|
||||
|
||||
### Example: Creating your own fungible token
|
||||
|
||||
```sh
|
||||
|
@ -109,7 +118,7 @@ Signature: 42Sa5eK9dMEQyvD9GMHuKxXf55WLZ7tfjabUKDhNoZRAxj9MsnN7omriWMEHXLea3aYpj
|
|||
|
||||
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
|
||||
```sh
|
||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
0
|
||||
```
|
||||
|
||||
|
@ -126,7 +135,7 @@ The token `supply` and account `balance` now reflect the result of minting:
|
|||
```sh
|
||||
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
100
|
||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||
100
|
||||
```
|
||||
|
||||
|
@ -166,7 +175,7 @@ address by running `solana address` and provides it to the sender.
|
|||
|
||||
The sender then runs:
|
||||
```
|
||||
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
$ spl-token transfer AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
Transfer 50 tokens
|
||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
|
@ -184,7 +193,7 @@ The receiver obtains their wallet address by running `solana address` and provid
|
|||
The sender then runs to fund the receiver's associated token account, at the
|
||||
sender's expense, and then transfers 50 tokens into it:
|
||||
```
|
||||
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
$ spl-token transfer --fund-recipient AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
Transfer 50 tokens
|
||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||
|
@ -228,9 +237,9 @@ CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe
|
|||
|
||||
### Example: Create a non-fungible token
|
||||
|
||||
Create the token type,
|
||||
Create the token type with nine decimal places,
|
||||
```
|
||||
$ spl-token create-token
|
||||
$ spl-token create-token --decimals 9
|
||||
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
||||
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
|
||||
```
|
||||
|
@ -520,7 +529,7 @@ There is a rich set of JSON RPC methods available for use with SPL Token:
|
|||
|
||||
See https://docs.solana.com/apps/jsonrpc-api for more details.
|
||||
|
||||
Additionally the versatile `getProgramAcccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
||||
Additionally the versatile `getProgramAccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
||||
|
||||
### Finding all token accounts for a specific mint
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -15,11 +15,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -12,11 +12,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -10,11 +10,11 @@ edition = "2018"
|
|||
[dependencies]
|
||||
chrono = "0.4.19"
|
||||
clap = "2.33.3"
|
||||
solana-clap-utils = "1.6.2"
|
||||
solana-cli-config = "1.6.2"
|
||||
solana-client = "1.6.2"
|
||||
solana-logger = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-clap-utils = "1.6.7"
|
||||
solana-cli-config = "1.6.7"
|
||||
solana-client = "1.6.7"
|
||||
solana-logger = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
|
||||
|
||||
[[bin]]
|
||||
|
|
|
@ -12,15 +12,15 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.7.1"
|
||||
borsh = "0.8"
|
||||
borsh-derive = "0.8.1"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -155,13 +155,12 @@ pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::borsh_utils;
|
||||
|
||||
#[test]
|
||||
fn test_get_packed_len() {
|
||||
assert_eq!(
|
||||
FeatureProposalInstruction::get_packed_len(),
|
||||
borsh_utils::get_packed_len::<FeatureProposalInstruction>()
|
||||
solana_program::borsh::get_packed_len::<FeatureProposalInstruction>()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#![deny(missing_docs)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
pub mod borsh_utils;
|
||||
mod entrypoint;
|
||||
pub mod instruction;
|
||||
pub mod processor;
|
||||
|
|
|
@ -59,13 +59,12 @@ impl Pack for FeatureProposal {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::borsh_utils;
|
||||
|
||||
#[test]
|
||||
fn test_get_packed_len() {
|
||||
assert_eq!(
|
||||
FeatureProposal::get_packed_len(),
|
||||
borsh_utils::get_packed_len::<FeatureProposal>()
|
||||
solana_program::borsh::get_packed_len::<FeatureProposal>()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,18 +12,18 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.7.1"
|
||||
borsh = "0.8"
|
||||
borsh-derive = "0.8.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "0.10"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -1,35 +1,42 @@
|
|||
//! Approximation calculations
|
||||
|
||||
use {
|
||||
num_traits::{CheckedAdd, CheckedDiv, One, Zero},
|
||||
std::cmp::Eq,
|
||||
num_traits::{CheckedShl, CheckedShr, PrimInt},
|
||||
std::cmp::Ordering,
|
||||
};
|
||||
|
||||
const SQRT_ITERATIONS: u8 = 50;
|
||||
/// Calculate square root of the given number
|
||||
///
|
||||
/// Code lovingly adapted from the excellent work at:
|
||||
/// https://github.com/derekdreery/integer-sqrt-rs
|
||||
///
|
||||
/// The algorithm is based on the implementation in:
|
||||
/// https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Binary_numeral_system_(base_2)
|
||||
pub fn sqrt<T: PrimInt + CheckedShl + CheckedShr>(radicand: T) -> Option<T> {
|
||||
match radicand.cmp(&T::zero()) {
|
||||
Ordering::Less => return None, // fail for less than 0
|
||||
Ordering::Equal => return Some(T::zero()), // do nothing for 0
|
||||
_ => {}
|
||||
}
|
||||
|
||||
/// Perform square root
|
||||
pub fn sqrt<T: CheckedAdd + CheckedDiv + One + Zero + Eq + Copy>(radicand: T) -> Option<T> {
|
||||
if radicand == T::zero() {
|
||||
return Some(T::zero());
|
||||
}
|
||||
// A good initial guess is the average of the interval that contains the
|
||||
// input number. For all numbers, that will be between 1 and the given number.
|
||||
let one = T::one();
|
||||
let two = one.checked_add(&one)?;
|
||||
let mut guess = radicand.checked_div(&two)?.checked_add(&one)?;
|
||||
let mut last_guess = guess;
|
||||
for _ in 0..SQRT_ITERATIONS {
|
||||
// x_k+1 = (x_k + radicand / x_k) / 2
|
||||
guess = last_guess
|
||||
.checked_add(&radicand.checked_div(&last_guess)?)?
|
||||
.checked_div(&two)?;
|
||||
if last_guess == guess {
|
||||
break;
|
||||
// Compute bit, the largest power of 4 <= n
|
||||
let max_shift: u32 = T::zero().leading_zeros() - 1;
|
||||
let shift: u32 = (max_shift - radicand.leading_zeros()) & !1;
|
||||
let mut bit = T::one().checked_shl(shift)?;
|
||||
|
||||
let mut n = radicand;
|
||||
let mut result = T::zero();
|
||||
while bit != T::zero() {
|
||||
let result_with_bit = result.checked_add(&bit)?;
|
||||
if n >= result_with_bit {
|
||||
n = n.checked_sub(&result_with_bit)?;
|
||||
result = result.checked_shr(1)?.checked_add(&bit)?;
|
||||
} else {
|
||||
last_guess = guess;
|
||||
result = result.checked_shr(1)?;
|
||||
}
|
||||
bit = bit.checked_shr(2)?;
|
||||
}
|
||||
Some(guess)
|
||||
Some(result)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -43,7 +43,16 @@ pub enum MathInstruction {
|
|||
/// The multipier
|
||||
multiplier: u64,
|
||||
},
|
||||
/// Multiply two float valies
|
||||
/// Divide two u64 values
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
U64Divide {
|
||||
/// The dividend
|
||||
dividend: u64,
|
||||
/// The divisor
|
||||
divisor: u64,
|
||||
},
|
||||
/// Multiply two float values
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
F32Multiply {
|
||||
|
@ -52,7 +61,7 @@ pub enum MathInstruction {
|
|||
/// The multipier
|
||||
multiplier: f32,
|
||||
},
|
||||
/// Divide two float valies
|
||||
/// Divide two float values
|
||||
///
|
||||
/// No accounts required for this instruction
|
||||
F32Divide {
|
||||
|
@ -114,6 +123,17 @@ pub fn u64_multiply(multiplicand: u64, multiplier: u64) -> Instruction {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create PreciseSquareRoot instruction
|
||||
pub fn u64_divide(dividend: u64, divisor: u64) -> Instruction {
|
||||
Instruction {
|
||||
program_id: id(),
|
||||
accounts: vec![],
|
||||
data: MathInstruction::U64Divide { dividend, divisor }
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create PreciseSquareRoot instruction
|
||||
pub fn f32_multiply(multiplicand: f32, multiplier: f32) -> Instruction {
|
||||
Instruction {
|
||||
|
|
|
@ -3,9 +3,36 @@
|
|||
use {
|
||||
crate::{approximations::sqrt, instruction::MathInstruction, precise_number::PreciseNumber},
|
||||
borsh::BorshDeserialize,
|
||||
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, msg, pubkey::Pubkey},
|
||||
solana_program::{
|
||||
account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units, msg,
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
};
|
||||
|
||||
/// u64_multiply
|
||||
#[inline(never)]
|
||||
fn u64_multiply(multiplicand: u64, multiplier: u64) -> u64 {
|
||||
multiplicand * multiplier
|
||||
}
|
||||
|
||||
/// u64_divide
|
||||
#[inline(never)]
|
||||
fn u64_divide(dividend: u64, divisor: u64) -> u64 {
|
||||
dividend / divisor
|
||||
}
|
||||
|
||||
/// f32_multiply
|
||||
#[inline(never)]
|
||||
fn f32_multiply(multiplicand: f32, multiplier: f32) -> f32 {
|
||||
multiplicand * multiplier
|
||||
}
|
||||
|
||||
/// f32_divide
|
||||
#[inline(never)]
|
||||
fn f32_divide(dividend: f32, divisor: f32) -> f32 {
|
||||
dividend / divisor
|
||||
}
|
||||
|
||||
/// Instruction processor
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
|
@ -17,19 +44,25 @@ pub fn process_instruction(
|
|||
MathInstruction::PreciseSquareRoot { radicand } => {
|
||||
msg!("Calculating square root using PreciseNumber");
|
||||
let radicand = PreciseNumber::new(radicand as u128).unwrap();
|
||||
sol_log_compute_units();
|
||||
let result = radicand.sqrt().unwrap().to_imprecise().unwrap() as u64;
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::SquareRootU64 { radicand } => {
|
||||
msg!("Calculating u64 square root");
|
||||
sol_log_compute_units();
|
||||
let result = sqrt(radicand).unwrap();
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::SquareRootU128 { radicand } => {
|
||||
msg!("Calculating u128 square root");
|
||||
sol_log_compute_units();
|
||||
let result = sqrt(radicand).unwrap();
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -38,7 +71,17 @@ pub fn process_instruction(
|
|||
multiplier,
|
||||
} => {
|
||||
msg!("Calculating U64 Multiply");
|
||||
let result = multiplicand * multiplier;
|
||||
sol_log_compute_units();
|
||||
let result = u64_multiply(multiplicand, multiplier);
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::U64Divide { dividend, divisor } => {
|
||||
msg!("Calculating U64 Divide");
|
||||
sol_log_compute_units();
|
||||
let result = u64_divide(dividend, divisor);
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result);
|
||||
Ok(())
|
||||
}
|
||||
|
@ -47,13 +90,17 @@ pub fn process_instruction(
|
|||
multiplier,
|
||||
} => {
|
||||
msg!("Calculating f32 Multiply");
|
||||
let result = multiplicand * multiplier;
|
||||
sol_log_compute_units();
|
||||
let result = f32_multiply(multiplicand, multiplier);
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result as u64);
|
||||
Ok(())
|
||||
}
|
||||
MathInstruction::F32Divide { dividend, divisor } => {
|
||||
msg!("Calculating f32 Divide");
|
||||
let result = dividend / divisor;
|
||||
sol_log_compute_units();
|
||||
let result = f32_divide(dividend, divisor);
|
||||
sol_log_compute_units();
|
||||
msg!("{}", result as u64);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ async fn test_sqrt_u128() {
|
|||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
// Dial down the BPF compute budget to detect if the operation gets bloated in the future
|
||||
pc.set_bpf_compute_max_units(5_500);
|
||||
pc.set_bpf_compute_max_units(4_000);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
|
@ -78,8 +78,7 @@ async fn test_sqrt_u128() {
|
|||
async fn test_sqrt_u128_max() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
// This is pretty big too!
|
||||
pc.set_bpf_compute_max_units(90_000);
|
||||
pc.set_bpf_compute_max_units(6_000);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
|
@ -103,6 +102,20 @@ async fn test_u64_multiply() {
|
|||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_u64_divide() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
||||
pc.set_bpf_compute_max_units(1650);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
|
||||
|
||||
let mut transaction =
|
||||
Transaction::new_with_payer(&[instruction::u64_divide(3, 1)], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_f32_multiply() {
|
||||
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "spl-memo"
|
||||
version = "3.0.0"
|
||||
version = "3.0.1"
|
||||
description = "Solana Program Library Memo"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
|
@ -12,11 +12,11 @@ no-entrypoint = []
|
|||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -117,6 +117,7 @@ async fn test_memo_signing() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_memo_compute_limits() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
src/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
target
|
||||
.vscode
|
||||
hfuzz*
|
||||
third_party/
|
||||
|
||||
*/node_modules
|
||||
js/dist
|
||||
js/lib
|
||||
js/docs
|
||||
js/src/secret.ts
|
|
@ -0,0 +1,10 @@
|
|||
# Name Service program
|
||||
|
||||
A spl program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, twitter handles, ipfs cid's, metadata, etc..
|
||||
|
||||
This program provides an interface and implementation that third parties can
|
||||
utilize to create and use their own version of a name service of any kind.
|
||||
|
||||
Full documentation is available at https://spl.solana.com/name-service
|
||||
|
||||
JavaScript binding are available in the `./js` directory.
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "spl-name-service",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/solana-labs/solana-program-library"
|
||||
},
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "tsc && node --trace-warnings dist/test.js",
|
||||
"build": "tsc",
|
||||
"prepublish": "tsc",
|
||||
"lint": "yarn pretty && eslint .",
|
||||
"lint:fix": "yarn pretty:fix && eslint . --fix",
|
||||
"pretty": "prettier --check 'src/*.[jt]s'",
|
||||
"pretty:fix": "prettier --write 'src/*.[jt]s'",
|
||||
"doc": "yarn typedoc src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/node": "^14.14.20",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.17.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.2.1",
|
||||
"save-dev": "0.0.1-security",
|
||||
"ts-node": "^9.1.1",
|
||||
"tslib": "^2.2.0",
|
||||
"typedoc": "^0.20.35",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@project-serum/sol-wallet-adapter": "^0.1.5",
|
||||
"@solana/spl-token": "0.1.3",
|
||||
"@solana/web3.js": "^1.2.6",
|
||||
"bip32": "^2.0.6",
|
||||
"bn.js": "^5.1.3",
|
||||
"@bonfida/borsh-js": "^0.3.1",
|
||||
"bs58": "4.0.1",
|
||||
"buffer-layout": "^1.2.0",
|
||||
"core-util-is": "^1.0.2",
|
||||
"crypto": "^1.0.1",
|
||||
"crypto-ts": "^1.0.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"webpack-dev-server": "^3.11.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
import {
|
||||
Account,
|
||||
Connection,
|
||||
PublicKey,
|
||||
SystemProgram,
|
||||
TransactionInstruction,
|
||||
} from "@solana/web3.js";
|
||||
import { Numberu64 } from "./utils";
|
||||
import { updateInstruction, transferInstruction, createInstruction, deleteInstruction } from "./instructions";
|
||||
import { createHash, HashOptions } from 'crypto';
|
||||
import { getHashedName, getNameAccountKey, getNameOwner, Numberu32 } from ".";
|
||||
import { hash } from "tweetnacl";
|
||||
import { NameRegistryState } from "./state";
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
export const NAME_PROGRAM_ID = new PublicKey(
|
||||
"Gh9eN9nDuS3ysmAkKf4QJ6yBzf3YNqsn6MD8Ms3TsXmA"
|
||||
);
|
||||
export const HASH_PREFIX = "SPL Name Service";
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Creates a name account with the given rent budget, allocated space, owner and class.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the new account
|
||||
* @param space The space in bytes allocated to the account
|
||||
* @param payerKey The allocation cost payer
|
||||
* @param nameOwner The pubkey to be set as owner of the new name account
|
||||
* @param lamports The budget to be set for the name account. If not specified, it'll be the minimum for rent exemption
|
||||
* @param nameClass The class of this new name
|
||||
* @param parentName The parent name of the new name. If specified its owner needs to sign
|
||||
* @returns
|
||||
*/
|
||||
export async function createNameRegistry(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
space: number,
|
||||
payerKey: PublicKey,
|
||||
nameOwner: PublicKey,
|
||||
lamports?: number,
|
||||
nameClass?: PublicKey,
|
||||
parentName?: PublicKey,
|
||||
): Promise<TransactionInstruction> {
|
||||
let hashed_name = await getHashedName(name);
|
||||
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, parentName);
|
||||
|
||||
let balance = lamports
|
||||
? lamports
|
||||
: await connection.getMinimumBalanceForRentExemption(space);
|
||||
|
||||
let nameParentOwner: PublicKey | undefined;
|
||||
if (!!parentName) {
|
||||
let parentAccount = await getNameOwner(connection, parentName);
|
||||
}
|
||||
|
||||
let createNameInstr = createInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
SystemProgram.programId,
|
||||
nameAccountKey,
|
||||
nameOwner,
|
||||
payerKey,
|
||||
hashed_name,
|
||||
new Numberu64(balance),
|
||||
new Numberu32(space),
|
||||
nameClass,
|
||||
parentName,
|
||||
nameParentOwner
|
||||
);
|
||||
|
||||
return createNameInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the data of the given name registry.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name registry to update
|
||||
* @param offset The offset to which the data should be written into the registry
|
||||
* @param input_data The data to be written
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
*/
|
||||
export async function updateNameRegistryData(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
offset: number,
|
||||
input_data: Buffer,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey,
|
||||
): Promise<TransactionInstruction> {
|
||||
let hashed_name = await getHashedName(name);
|
||||
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
|
||||
|
||||
let signer: PublicKey;
|
||||
if (!!nameClass) {
|
||||
signer = nameClass;
|
||||
} else {
|
||||
signer = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
|
||||
}
|
||||
|
||||
let updateInstr = updateInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
new Numberu32(offset),
|
||||
input_data,
|
||||
signer
|
||||
);
|
||||
|
||||
return updateInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cahnge the owner of a given name account.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name account
|
||||
* @param newOwner The new owner to be set
|
||||
* @param curentNameOwner the current name Owner
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
* @returns
|
||||
*/
|
||||
export async function transferNameOwnership(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
newOwner: PublicKey,
|
||||
currentNameOwner: PublicKey,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey,
|
||||
): Promise<TransactionInstruction> {
|
||||
let hashed_name = await getHashedName(name);
|
||||
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
|
||||
|
||||
let curentNameOwner: PublicKey;
|
||||
if (!!nameClass) {
|
||||
curentNameOwner = nameClass;
|
||||
} else {
|
||||
curentNameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
|
||||
}
|
||||
|
||||
let transferInstr = transferInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
newOwner,
|
||||
curentNameOwner,
|
||||
nameClass
|
||||
);
|
||||
|
||||
return transferInstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the name account and transfer the rent to the target.
|
||||
*
|
||||
* @param connection The solana connection object to the RPC node
|
||||
* @param name The name of the name account
|
||||
* @param refundTargetKey The refund destination address
|
||||
* @param nameClass The class of this name, if it exsists
|
||||
* @param nameParent The parent name of this name, if it exists
|
||||
* @returns
|
||||
*/
|
||||
export async function deleteNameRegistry(
|
||||
connection: Connection,
|
||||
name: string,
|
||||
refundTargetKey: PublicKey,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey,
|
||||
): Promise<TransactionInstruction> {
|
||||
let hashed_name = await getHashedName(name);
|
||||
let nameAccountKey = await getNameAccountKey(hashed_name, nameClass, nameParent);
|
||||
|
||||
let nameOwner: PublicKey;
|
||||
if (!!nameClass) {
|
||||
nameOwner = nameClass;
|
||||
} else {
|
||||
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey)).owner;
|
||||
}
|
||||
|
||||
let changeAuthoritiesInstr = deleteInstruction(
|
||||
NAME_PROGRAM_ID,
|
||||
nameAccountKey,
|
||||
refundTargetKey,
|
||||
nameOwner
|
||||
);
|
||||
|
||||
return changeAuthoritiesInstr;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./utils";
|
||||
export * from "./bindings";
|
|
@ -0,0 +1,204 @@
|
|||
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
||||
import { Schema, serialize } from "@bonfida/borsh-js";
|
||||
import { Numberu64, Numberu32 } from "./utils";
|
||||
|
||||
export function createInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
systemProgramId: PublicKey,
|
||||
nameKey: PublicKey,
|
||||
nameOwnerKey: PublicKey,
|
||||
payerKey: PublicKey,
|
||||
hashed_name: Buffer,
|
||||
lamports: Numberu64,
|
||||
space: Numberu32,
|
||||
nameClassKey?: PublicKey,
|
||||
nameParent?: PublicKey,
|
||||
nameParentOwner?: PublicKey
|
||||
): TransactionInstruction {
|
||||
|
||||
let buffers = [
|
||||
Buffer.from(Int8Array.from([0])),
|
||||
new Numberu32(hashed_name.length).toBuffer(),
|
||||
hashed_name,
|
||||
lamports.toBuffer(),
|
||||
space.toBuffer(),
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
|
||||
let keys = [
|
||||
{
|
||||
pubkey: systemProgramId,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: payerKey,
|
||||
isSigner: true,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameOwnerKey,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (!!nameClassKey) {
|
||||
keys.push({
|
||||
pubkey: nameClassKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
} else {
|
||||
keys.push({
|
||||
pubkey: new PublicKey(Buffer.alloc(32)),
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
if (!!nameParent) {
|
||||
keys.push({
|
||||
pubkey: nameParent,
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
} else {
|
||||
keys.push({
|
||||
pubkey: new PublicKey(Buffer.alloc(32)),
|
||||
isSigner: false,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
if (!!nameParentOwner) {
|
||||
keys.push({
|
||||
pubkey: nameParentOwner,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function updateInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
offset: Numberu32,
|
||||
input_data: Buffer,
|
||||
nameUpdateSigner: PublicKey,
|
||||
): TransactionInstruction {
|
||||
let buffers = [
|
||||
Buffer.from(Int8Array.from([1])),
|
||||
offset.toBuffer(),
|
||||
new Numberu32(input_data.length).toBuffer(),
|
||||
input_data
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
let keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameUpdateSigner,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function transferInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
newOwnerKey: PublicKey,
|
||||
currentNameOwnerKey: PublicKey,
|
||||
nameClassKey?: PublicKey,
|
||||
): TransactionInstruction {
|
||||
let buffers = [
|
||||
Buffer.from(Int8Array.from([2])),
|
||||
newOwnerKey.toBuffer()
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
|
||||
let keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: currentNameOwnerKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
];
|
||||
|
||||
if (!!nameClassKey) {
|
||||
keys.push({
|
||||
pubkey: nameClassKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteInstruction(
|
||||
nameProgramId: PublicKey,
|
||||
nameAccountKey: PublicKey,
|
||||
refundTargetKey: PublicKey,
|
||||
nameOwnerKey: PublicKey,
|
||||
): TransactionInstruction {
|
||||
let buffers = [
|
||||
Buffer.from(Int8Array.from([3])),
|
||||
];
|
||||
|
||||
const data = Buffer.concat(buffers);
|
||||
let keys = [
|
||||
{
|
||||
pubkey: nameAccountKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
{
|
||||
pubkey: nameOwnerKey,
|
||||
isSigner: true,
|
||||
isWritable: false,
|
||||
},
|
||||
{
|
||||
pubkey: refundTargetKey,
|
||||
isSigner: false,
|
||||
isWritable: true,
|
||||
},
|
||||
];
|
||||
|
||||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId: nameProgramId,
|
||||
data,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
import { PublicKey, Connection } from "@solana/web3.js";
|
||||
import { Schema, deserializeUnchecked } from "@bonfida/borsh-js";
|
||||
|
||||
export class NameRegistryState {
|
||||
parentName: PublicKey;
|
||||
owner: PublicKey;
|
||||
class: PublicKey;
|
||||
data: Buffer;
|
||||
|
||||
static schema: Schema = new Map([
|
||||
[
|
||||
NameRegistryState,
|
||||
{
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
['parentName', [32]],
|
||||
['owner', [32]],
|
||||
['class', [32]],
|
||||
['data', ['u8']],
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
constructor(obj: {
|
||||
parentName: Uint8Array;
|
||||
owner: Uint8Array;
|
||||
class: Uint8Array;
|
||||
data: Uint8Array;
|
||||
}) {
|
||||
this.parentName = new PublicKey(obj.parentName);
|
||||
this.owner = new PublicKey(obj.owner);
|
||||
this.class = new PublicKey(obj.class);
|
||||
this.data = Buffer.from(obj.data);
|
||||
}
|
||||
|
||||
static async retrieve(
|
||||
connection: Connection,
|
||||
nameAccountKey: PublicKey,
|
||||
): Promise<NameRegistryState> {
|
||||
let nameAccount = await connection.getAccountInfo(
|
||||
nameAccountKey,
|
||||
'processed',
|
||||
);
|
||||
if (!nameAccount) {
|
||||
throw new Error('Invalid name account provided');
|
||||
}
|
||||
|
||||
let res: NameRegistryState = deserializeUnchecked(
|
||||
this.schema,
|
||||
NameRegistryState,
|
||||
nameAccount.data,
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
import { serialize } from "@bonfida/borsh-js";
|
||||
import { Connection, Account, PublicKey, AccountInfo } from "@solana/web3.js";
|
||||
import { transferNameOwnership, updateNameRegistryData, createNameRegistry, deleteNameRegistry } from "./bindings";
|
||||
import { readFile } from "fs/promises";
|
||||
import { Numberu64, signAndSendTransactionInstructions } from "./utils";
|
||||
import { sign } from "tweetnacl";
|
||||
import { getHashedName, getNameAccountKey, Numberu32 } from ".";
|
||||
import { NameRegistryState } from "./state";
|
||||
|
||||
const ENDPOINT = 'https://devnet.solana.com/';
|
||||
// const ENDPOINT = 'https://solana-api.projectserum.com/';
|
||||
|
||||
export async function test() {
|
||||
let connection = new Connection(ENDPOINT);
|
||||
let secretKey = JSON.parse(
|
||||
(await readFile('/home/lcchy-work/.config/solana/id_devnet.json')).toString()
|
||||
);
|
||||
let adminAccount = new Account(secretKey);
|
||||
|
||||
let root_name = ".sol";
|
||||
|
||||
// let create_instruction = await createNameRegistry(
|
||||
// connection,
|
||||
// root_name,
|
||||
// 1000,
|
||||
// adminAccount.publicKey,
|
||||
// adminAccount.publicKey,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [create_instruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
|
||||
// let input_data = Buffer.from("Du");
|
||||
// let updateInstruction = await updateNameRegistryData(
|
||||
// connection,
|
||||
// root_name,
|
||||
// 0,
|
||||
// input_data,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [updateInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
// let transferInstruction = await transferNameOwnership(
|
||||
// connection,
|
||||
// root_name,
|
||||
// adminAccount.publicKey,
|
||||
// adminAccount.publicKey,
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [transferInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
// let deleteInstruction = await deleteNameRegistry(
|
||||
// connection,
|
||||
// root_name,
|
||||
// adminAccount.publicKey
|
||||
// );
|
||||
|
||||
// console.log(
|
||||
// await signAndSendTransactionInstructions(
|
||||
// connection,
|
||||
// [adminAccount],
|
||||
// adminAccount,
|
||||
// [deleteInstruction]
|
||||
// )
|
||||
// );
|
||||
|
||||
let hashed_root_name = await getHashedName(root_name);
|
||||
let nameAccountKey = await getNameAccountKey(hashed_root_name);
|
||||
console.log(await NameRegistryState.retrieve(connection, nameAccountKey));
|
||||
}
|
||||
|
||||
test();
|
|
@ -0,0 +1,132 @@
|
|||
import {
|
||||
PublicKey,
|
||||
TransactionInstruction,
|
||||
Connection,
|
||||
Account,
|
||||
Transaction,
|
||||
AccountInfo,
|
||||
} from "@solana/web3.js";
|
||||
import assert from "assert";
|
||||
import BN from "bn.js";
|
||||
import { createHash } from "crypto";
|
||||
import { HASH_PREFIX, NAME_PROGRAM_ID } from ".";
|
||||
import { NameRegistryState} from "./state";
|
||||
|
||||
export class Numberu32 extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer {
|
||||
const a = super.toArray().reverse();
|
||||
const b = Buffer.from(a);
|
||||
if (b.length === 4) {
|
||||
return b;
|
||||
}
|
||||
assert(b.length < 4, "Numberu32 too large");
|
||||
|
||||
const zeroPad = Buffer.alloc(4);
|
||||
b.copy(zeroPad);
|
||||
return zeroPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Numberu64 from Buffer representation
|
||||
*/
|
||||
static fromBuffer(buffer): any {
|
||||
assert(buffer.length === 4, `Invalid buffer length: ${buffer.length}`);
|
||||
return new BN(
|
||||
[...buffer]
|
||||
.reverse()
|
||||
.map((i) => `00${i.toString(16)}`.slice(-2))
|
||||
.join(""),
|
||||
16
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class Numberu64 extends BN {
|
||||
/**
|
||||
* Convert to Buffer representation
|
||||
*/
|
||||
toBuffer(): Buffer {
|
||||
const a = super.toArray().reverse();
|
||||
const b = Buffer.from(a);
|
||||
if (b.length === 8) {
|
||||
return b;
|
||||
}
|
||||
assert(b.length < 8, "Numberu64 too large");
|
||||
|
||||
const zeroPad = Buffer.alloc(8);
|
||||
b.copy(zeroPad);
|
||||
return zeroPad;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Numberu64 from Buffer representation
|
||||
*/
|
||||
static fromBuffer(buffer): any {
|
||||
assert(buffer.length === 8, `Invalid buffer length: ${buffer.length}`);
|
||||
return new BN(
|
||||
[...buffer]
|
||||
.reverse()
|
||||
.map((i) => `00${i.toString(16)}`.slice(-2))
|
||||
.join(""),
|
||||
16
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const signAndSendTransactionInstructions = async (
|
||||
// sign and send transaction
|
||||
connection: Connection,
|
||||
signers: Array<Account>,
|
||||
feePayer: Account,
|
||||
txInstructions: Array<TransactionInstruction>
|
||||
): Promise<string> => {
|
||||
const tx = new Transaction();
|
||||
tx.feePayer = feePayer.publicKey;
|
||||
signers.push(feePayer);
|
||||
tx.add(...txInstructions);
|
||||
return await connection.sendTransaction(tx, signers, {
|
||||
preflightCommitment: "single",
|
||||
});
|
||||
};
|
||||
|
||||
export async function getHashedName(
|
||||
name: string
|
||||
): Promise<Buffer> {
|
||||
let input = HASH_PREFIX + name;
|
||||
let buffer = createHash('sha256').update(input, 'utf8').digest();
|
||||
return buffer
|
||||
}
|
||||
|
||||
export async function getNameAccountKey(
|
||||
hashed_name: Buffer,
|
||||
nameClass?: PublicKey,
|
||||
nameParent?: PublicKey
|
||||
): Promise<PublicKey> {
|
||||
let seeds = [hashed_name];
|
||||
if (!!nameClass) {
|
||||
seeds.push(nameClass.toBuffer());
|
||||
} else {
|
||||
seeds.push(Buffer.alloc(32));
|
||||
}
|
||||
if (!!nameParent) {
|
||||
seeds.push(nameParent.toBuffer());
|
||||
} else {
|
||||
seeds.push(Buffer.alloc(32));
|
||||
}
|
||||
let [nameAccountKey, _] = await PublicKey.findProgramAddress(
|
||||
seeds,
|
||||
NAME_PROGRAM_ID
|
||||
);
|
||||
return nameAccountKey
|
||||
}
|
||||
|
||||
export async function getNameOwner(connection: Connection, nameAccountKey: PublicKey): Promise<NameRegistryState> {
|
||||
let nameAccount = await connection.getAccountInfo(nameAccountKey);
|
||||
if (!nameAccount) {
|
||||
throw "Unable to find the given account."
|
||||
}
|
||||
return NameRegistryState.retrieve(connection, nameAccountKey); //TODO use borsh-js
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"extends": "@tsconfig/recommended/tsconfig.json",
|
||||
"ts-node": {
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"*" : ["types/*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"target": "es2019",
|
||||
"outDir": "dist",
|
||||
"rootDir": "./src",
|
||||
"declaration": true,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"sourceMap": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["node_modules/*", "src/types/*"]
|
||||
},
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/*"],
|
||||
"exclude": ["src/**/*.test.ts", "**/node_modules", "dist"]
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "spl-name-service"
|
||||
description = "Solana Program Library Name Service"
|
||||
version = "0.1.0"
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
authors = ["lcchy <lucas@bonfida.com>"]
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
solana-program = "1.6.7"
|
||||
num-traits = "0.2"
|
||||
borsh = "0.8.1"
|
||||
num-derive = "0.3.3"
|
||||
thiserror = "1.0.24"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1 @@
|
|||
namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX
|
|
@ -0,0 +1,36 @@
|
|||
use {
|
||||
crate::error::NameServiceError,
|
||||
crate::processor::Processor,
|
||||
num_traits::FromPrimitive,
|
||||
solana_program::{
|
||||
account_info::AccountInfo, decode_error::DecodeError, entrypoint,
|
||||
entrypoint::ProgramResult, msg, program_error::PrintProgramError, pubkey::Pubkey,
|
||||
},
|
||||
};
|
||||
|
||||
entrypoint!(process_instruction);
|
||||
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
msg!("Entrypoint");
|
||||
if let Err(error) = Processor::process_instruction(program_id, accounts, instruction_data) {
|
||||
// catch the error so we can print it
|
||||
error.print::<NameServiceError>();
|
||||
return Err(error);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl PrintProgramError for NameServiceError {
|
||||
fn print<E>(&self)
|
||||
where
|
||||
E: 'static + std::error::Error + DecodeError<E> + PrintProgramError + FromPrimitive,
|
||||
{
|
||||
match self {
|
||||
NameServiceError::OutOfSpace => msg!("Error: Registry is out of space!"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
use {
|
||||
num_derive::FromPrimitive,
|
||||
solana_program::{decode_error::DecodeError, program_error::ProgramError},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
|
||||
pub enum NameServiceError {
|
||||
#[error("Out of space")]
|
||||
OutOfSpace,
|
||||
}
|
||||
|
||||
pub type NameServiceResult = Result<(), NameServiceError>;
|
||||
|
||||
impl From<NameServiceError> for ProgramError {
|
||||
fn from(e: NameServiceError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DecodeError<T> for NameServiceError {
|
||||
fn type_of() -> &'static str {
|
||||
"NameServiceError"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
use {
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
solana_program::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
system_program,
|
||||
},
|
||||
};
|
||||
|
||||
/// Instructions supported by the generic Name Registry program
|
||||
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
|
||||
pub enum NameRegistryInstruction {
|
||||
/// Create an empty name record
|
||||
///
|
||||
/// The address of the name record (account #1) is a program-derived address with the following
|
||||
/// seeds to ensure uniqueness:
|
||||
/// * SHA256(HASH_PREFIX, `Create::name`)
|
||||
/// * Account class (account #3)
|
||||
/// * Parent name record address (account #4)
|
||||
///
|
||||
/// If this is a child record, the parent record's owner must approve by signing (account #5)
|
||||
///
|
||||
/// Accounts expected by this instruction:
|
||||
/// 0. `[]` System program
|
||||
/// 1. `[writeable, signer]` Funding account (must be a system account)
|
||||
/// 2. `[writeable]` Name record to be created (program-derived address)
|
||||
/// 3. `[]` Account owner (written into `NameRecordHeader::owner`)
|
||||
/// 4. `[signer]` Account class (written into `NameRecordHeader::class`).
|
||||
/// If `Pubkey::default()` then the `signer` bit is not required
|
||||
/// 5. `[]` Parent name record (written into `NameRecordHeader::parent_name). `Pubkey::default()` is equivalent to no existing parent.
|
||||
/// 6. `[signer]` Owner of the parent name record. Optional but needed if parent name different than default.
|
||||
///
|
||||
Create {
|
||||
/// SHA256 of the (HASH_PREFIX + Name) of the record to create, hashing is done off-chain
|
||||
hashed_name: Vec<u8>,
|
||||
|
||||
/// Number of lamports to fund the name record with
|
||||
lamports: u64,
|
||||
|
||||
/// Number of bytes of memory to allocate in addition to the `NameRecordHeader`
|
||||
space: u32,
|
||||
},
|
||||
|
||||
/// Update the data in a name record
|
||||
///
|
||||
/// Accounts expected by this instruction:
|
||||
/// * If account class is `Pubkey::default()`:
|
||||
/// 0. `[writeable]` Name record to be updated
|
||||
/// 1. `[signer]` Account owner
|
||||
///
|
||||
/// * If account class is not `Pubkey::default()`:
|
||||
/// 0. `[writeable]` Name record to be updated
|
||||
/// 1. `[signer]` Account class
|
||||
///
|
||||
Update { offset: u32, data: Vec<u8> },
|
||||
|
||||
/// Transfer ownership of a name record
|
||||
///
|
||||
/// Accounts expected by this instruction:
|
||||
///
|
||||
/// * If account class is `Pubkey::default()`:
|
||||
/// 0. `[writeable]` Name record to be transferred
|
||||
/// 1. `[signer]` Account owner
|
||||
///
|
||||
/// * If account class is not `Pubkey::default()`:
|
||||
/// 0. `[writeable]` Name record to be transferred
|
||||
/// 1. `[signer]` Account owner
|
||||
/// 1. `[signer]` Account class
|
||||
///
|
||||
Transfer { new_owner: Pubkey },
|
||||
|
||||
/// Delete a name record.
|
||||
///
|
||||
/// Any lamports remaining in the name record will be transferred to the refund account (#2)
|
||||
///
|
||||
/// Accounts expected by this instruction:
|
||||
/// 0. `[writeable]` Name record to be deleted
|
||||
/// 1. `[signer]` Account owner
|
||||
/// 2. `[writeable]` Refund account
|
||||
///
|
||||
Delete,
|
||||
}
|
||||
|
||||
#[allow(clippy::clippy::too_many_arguments)]
|
||||
pub fn create(
|
||||
name_service_program_id: Pubkey,
|
||||
instruction_data: NameRegistryInstruction,
|
||||
name_account_key: Pubkey,
|
||||
payer_key: Pubkey,
|
||||
name_owner: Pubkey,
|
||||
name_class_opt: Option<Pubkey>,
|
||||
name_parent_opt: Option<Pubkey>,
|
||||
name_parent_owner_opt: Option<Pubkey>,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let data = instruction_data.try_to_vec().unwrap();
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new(payer_key, true),
|
||||
AccountMeta::new(name_account_key, false),
|
||||
AccountMeta::new_readonly(name_owner, false),
|
||||
];
|
||||
if let Some(name_class) = name_class_opt {
|
||||
accounts.push(AccountMeta::new_readonly(name_class, true));
|
||||
} else {
|
||||
accounts.push(AccountMeta::new_readonly(Pubkey::default(), false));
|
||||
}
|
||||
if let Some(name_parent) = name_parent_opt {
|
||||
accounts.push(AccountMeta::new_readonly(name_parent, false));
|
||||
} else {
|
||||
accounts.push(AccountMeta::new_readonly(Pubkey::default(), false));
|
||||
}
|
||||
if let Some(key) = name_parent_owner_opt {
|
||||
accounts.push(AccountMeta::new_readonly(key, true));
|
||||
}
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: name_service_program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
name_service_program_id: Pubkey,
|
||||
offset: u32,
|
||||
data: Vec<u8>,
|
||||
name_account_key: Pubkey,
|
||||
name_update_signer: Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let instruction_data = NameRegistryInstruction::Update { offset, data };
|
||||
let data = instruction_data.try_to_vec().unwrap();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(name_account_key, false),
|
||||
AccountMeta::new_readonly(name_update_signer, true),
|
||||
];
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: name_service_program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn transfer(
|
||||
name_service_program_id: Pubkey,
|
||||
new_owner: Pubkey,
|
||||
name_account_key: Pubkey,
|
||||
name_owner_key: Pubkey,
|
||||
name_class_opt: Option<Pubkey>,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let instruction_data = NameRegistryInstruction::Transfer { new_owner };
|
||||
let data = instruction_data.try_to_vec().unwrap();
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new(name_account_key, false),
|
||||
AccountMeta::new_readonly(name_owner_key, true),
|
||||
];
|
||||
|
||||
if let Some(key) = name_class_opt {
|
||||
accounts.push(AccountMeta::new_readonly(key, true));
|
||||
}
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: name_service_program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn delete(
|
||||
name_service_program_id: Pubkey,
|
||||
name_account_key: Pubkey,
|
||||
name_owner_key: Pubkey,
|
||||
refund_target: Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let instruction_data = NameRegistryInstruction::Delete;
|
||||
let data = instruction_data.try_to_vec().unwrap();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(name_account_key, false),
|
||||
AccountMeta::new_readonly(name_owner_key, true),
|
||||
AccountMeta::new(refund_target, false),
|
||||
];
|
||||
|
||||
Ok(Instruction {
|
||||
program_id: name_service_program_id,
|
||||
accounts,
|
||||
data,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
#[cfg(not(feature = "no-entrypoint"))]
|
||||
pub mod entrypoint;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
pub mod processor;
|
||||
pub mod state;
|
||||
|
||||
// Export current sdk types for downstream users building with a different sdk version
|
||||
pub use solana_program;
|
||||
|
||||
solana_program::declare_id!("namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX");
|
|
@ -0,0 +1,242 @@
|
|||
use {
|
||||
crate::{
|
||||
instruction::NameRegistryInstruction,
|
||||
state::get_seeds_and_key,
|
||||
state::{write_data, NameRecordHeader},
|
||||
},
|
||||
borsh::BorshDeserialize,
|
||||
solana_program::{
|
||||
account_info::{next_account_info, AccountInfo},
|
||||
entrypoint::ProgramResult,
|
||||
msg,
|
||||
program::{invoke, invoke_signed},
|
||||
program_error::ProgramError,
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
system_instruction,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Processor {}
|
||||
|
||||
impl Processor {
|
||||
pub fn process_create(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
hashed_name: Vec<u8>,
|
||||
lamports: u64,
|
||||
space: u32,
|
||||
) -> ProgramResult {
|
||||
let accounts_iter = &mut accounts.iter();
|
||||
|
||||
let system_program = next_account_info(accounts_iter)?;
|
||||
let payer_account = next_account_info(accounts_iter)?;
|
||||
let name_account = next_account_info(accounts_iter)?;
|
||||
let name_owner = next_account_info(accounts_iter)?;
|
||||
let name_class = next_account_info(accounts_iter)?;
|
||||
let parent_name_account = next_account_info(accounts_iter)?;
|
||||
let parent_name_owner = next_account_info(accounts_iter).ok();
|
||||
|
||||
let (name_account_key, seeds) = get_seeds_and_key(
|
||||
&program_id,
|
||||
hashed_name,
|
||||
Some(name_class.key),
|
||||
Some(parent_name_account.key),
|
||||
);
|
||||
|
||||
// Verifications
|
||||
if name_account_key != *name_account.key {
|
||||
msg!("The given name account is incorrect.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
if name_account.data.borrow().len() > 0 {
|
||||
let name_record_header =
|
||||
NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
|
||||
if name_record_header.owner != Pubkey::default() {
|
||||
msg!("The given name account already exists.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
}
|
||||
if *name_class.key != Pubkey::default() && !name_class.is_signer {
|
||||
msg!("The given name class is not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
if *parent_name_account.key != Pubkey::default() {
|
||||
if !parent_name_owner.unwrap().is_signer {
|
||||
msg!("The given parent name account owner is not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
} else {
|
||||
let parent_name_record_header =
|
||||
NameRecordHeader::unpack_from_slice(&parent_name_account.data.borrow())?;
|
||||
if &parent_name_record_header.owner != parent_name_owner.unwrap().key {
|
||||
msg!("The given parent name account owner is not correct.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
}
|
||||
}
|
||||
if name_owner.key == &Pubkey::default() {
|
||||
msg!("The owner cannot be `Pubkey::default()`.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
|
||||
if name_account.data.borrow().len() == 0 {
|
||||
// Issue the name registry account
|
||||
// The creation is done in three steps: transfer, allocate and assign, because
|
||||
// one cannot `system_instruction::create` an account to which lamports have been transfered before.
|
||||
invoke(
|
||||
&system_instruction::transfer(&payer_account.key, &name_account_key, lamports),
|
||||
&[
|
||||
payer_account.clone(),
|
||||
name_account.clone(),
|
||||
system_program.clone(),
|
||||
],
|
||||
)?;
|
||||
|
||||
invoke_signed(
|
||||
&system_instruction::allocate(&name_account_key, space as u64),
|
||||
&[name_account.clone(), system_program.clone()],
|
||||
&[&seeds.chunks(32).collect::<Vec<&[u8]>>()],
|
||||
)?;
|
||||
|
||||
invoke_signed(
|
||||
&system_instruction::assign(name_account.key, &program_id),
|
||||
&[name_account.clone(), system_program.clone()],
|
||||
&[&seeds.chunks(32).collect::<Vec<&[u8]>>()],
|
||||
)?;
|
||||
}
|
||||
|
||||
let name_state = NameRecordHeader {
|
||||
parent_name: *parent_name_account.key,
|
||||
owner: *name_owner.key,
|
||||
class: *name_class.key,
|
||||
};
|
||||
|
||||
name_state.pack_into_slice(&mut name_account.data.borrow_mut());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_update(accounts: &[AccountInfo], offset: u32, data: Vec<u8>) -> ProgramResult {
|
||||
let accounts_iter = &mut accounts.iter();
|
||||
|
||||
let name_account = next_account_info(accounts_iter)?;
|
||||
let name_update_signer = next_account_info(accounts_iter)?;
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
|
||||
|
||||
// Verifications
|
||||
if !name_update_signer.is_signer {
|
||||
msg!("The given name class or owner is not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
if name_record_header.class != Pubkey::default()
|
||||
&& *name_update_signer.key != name_record_header.class
|
||||
{
|
||||
msg!("The given name class account is incorrect.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
if name_record_header.class == Pubkey::default()
|
||||
&& *name_update_signer.key != name_record_header.owner
|
||||
{
|
||||
msg!("The given name owner account is incorrect.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
|
||||
write_data(name_account, &data, NameRecordHeader::LEN + offset as usize);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_transfer(accounts: &[AccountInfo], new_owner: Pubkey) -> ProgramResult {
|
||||
let accounts_iter = &mut accounts.iter();
|
||||
|
||||
let name_account = next_account_info(accounts_iter)?;
|
||||
let name_owner = next_account_info(accounts_iter)?;
|
||||
let name_class_opt = next_account_info(accounts_iter).ok();
|
||||
|
||||
let mut name_record_header =
|
||||
NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
|
||||
|
||||
// Verifications
|
||||
if !name_owner.is_signer || name_record_header.owner != *name_owner.key {
|
||||
msg!("The given name owner is incorrect or not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
if name_record_header.class != Pubkey::default()
|
||||
&& (name_class_opt.is_none()
|
||||
|| name_record_header.class != *name_class_opt.unwrap().key
|
||||
|| !name_class_opt.unwrap().is_signer)
|
||||
{
|
||||
msg!("The given name class account is incorrect or not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
|
||||
name_record_header.owner = new_owner;
|
||||
name_record_header
|
||||
.pack_into_slice(&mut name_account.data.borrow_mut()[..NameRecordHeader::LEN]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_delete(accounts: &[AccountInfo]) -> ProgramResult {
|
||||
let accounts_iter = &mut accounts.iter();
|
||||
|
||||
let name_account = next_account_info(accounts_iter)?;
|
||||
let name_owner = next_account_info(accounts_iter)?;
|
||||
let refund_target = next_account_info(accounts_iter)?;
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(&name_account.data.borrow())?;
|
||||
|
||||
// Verifications
|
||||
if !name_owner.is_signer || name_record_header.owner != *name_owner.key {
|
||||
msg!("The given name owner is incorrect or not a signer.");
|
||||
return Err(ProgramError::InvalidArgument);
|
||||
}
|
||||
|
||||
// Overwrite the data with zeroes
|
||||
write_data(name_account, &vec![0; name_account.data_len()], 0);
|
||||
|
||||
// Close the account by transferring the rent sol
|
||||
let source_amount: &mut u64 = &mut name_account.lamports.borrow_mut();
|
||||
let dest_amount: &mut u64 = &mut refund_target.lamports.borrow_mut();
|
||||
*dest_amount += *source_amount;
|
||||
*source_amount = 0;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
program_id: &Pubkey,
|
||||
accounts: &[AccountInfo],
|
||||
instruction_data: &[u8],
|
||||
) -> ProgramResult {
|
||||
msg!("Beginning processing");
|
||||
let instruction = NameRegistryInstruction::try_from_slice(instruction_data)
|
||||
.map_err(|_| ProgramError::InvalidInstructionData)?;
|
||||
msg!("Instruction unpack_from_sliceed");
|
||||
|
||||
match instruction {
|
||||
NameRegistryInstruction::Create {
|
||||
hashed_name,
|
||||
lamports,
|
||||
space,
|
||||
} => {
|
||||
msg!("Instruction: Create");
|
||||
Processor::process_create(program_id, accounts, hashed_name, lamports, space)?;
|
||||
}
|
||||
NameRegistryInstruction::Update { offset, data } => {
|
||||
msg!("Instruction: Update Data");
|
||||
Processor::process_update(accounts, offset, data)?;
|
||||
}
|
||||
NameRegistryInstruction::Transfer { new_owner } => {
|
||||
msg!("Instruction: Transfer Ownership");
|
||||
Processor::process_transfer(accounts, new_owner)?;
|
||||
}
|
||||
NameRegistryInstruction::Delete => {
|
||||
msg!("Instruction: Delete Name");
|
||||
Processor::process_delete(accounts)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
use {
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
solana_program::{
|
||||
account_info::AccountInfo,
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
program_pack::{IsInitialized, Pack, Sealed},
|
||||
pubkey::Pubkey,
|
||||
},
|
||||
};
|
||||
|
||||
/// The data for a Name Registry account is always prefixed a `NameRecordHeader` structure.
|
||||
///
|
||||
/// The layout of the remaining bytes in the account data are determined by the record `class`
|
||||
#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)]
|
||||
pub struct NameRecordHeader {
|
||||
// Names are hierarchical. `parent_name` contains the account address of the parent
|
||||
// name, or `Pubkey::default()` if no parent exists.
|
||||
pub parent_name: Pubkey,
|
||||
|
||||
// The owner of this name
|
||||
pub owner: Pubkey,
|
||||
|
||||
// The class of data this account represents (DNS record, twitter handle, SPL Token name/symbol, etc)
|
||||
//
|
||||
// If `Pubkey::default()` the data is unspecified.
|
||||
pub class: Pubkey,
|
||||
}
|
||||
|
||||
impl Sealed for NameRecordHeader {}
|
||||
|
||||
impl Pack for NameRecordHeader {
|
||||
const LEN: usize = 96;
|
||||
|
||||
fn pack_into_slice(&self, dst: &mut [u8]) {
|
||||
let mut slice = dst;
|
||||
self.serialize(&mut slice).unwrap()
|
||||
}
|
||||
|
||||
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
|
||||
let mut p = src;
|
||||
NameRecordHeader::deserialize(&mut p).map_err(|_| {
|
||||
msg!("Failed to deserialize name record");
|
||||
ProgramError::InvalidAccountData
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IsInitialized for NameRecordHeader {
|
||||
fn is_initialized(&self) -> bool {
|
||||
self.owner == Pubkey::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_data(account: &AccountInfo, input: &[u8], offset: usize) {
|
||||
let mut account_data = account.data.borrow_mut();
|
||||
account_data[offset..offset + input.len()].copy_from_slice(input);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
pub const HASH_PREFIX: &str = "SPL Name Service";
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
|
||||
pub fn get_seeds_and_key(
|
||||
program_id: &Pubkey,
|
||||
hashed_name: Vec<u8>, // Hashing is done off-chain
|
||||
name_class_opt: Option<&Pubkey>,
|
||||
parent_name_address_opt: Option<&Pubkey>,
|
||||
) -> (Pubkey, Vec<u8>) {
|
||||
// let hashed_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()]).0.to_vec();
|
||||
let mut seeds_vec: Vec<u8> = hashed_name;
|
||||
|
||||
let name_class = name_class_opt.cloned().unwrap_or_default();
|
||||
|
||||
for b in name_class.to_bytes().to_vec() {
|
||||
seeds_vec.push(b);
|
||||
}
|
||||
|
||||
let parent_name_address = parent_name_address_opt.cloned().unwrap_or_default();
|
||||
|
||||
for b in parent_name_address.to_bytes().to_vec() {
|
||||
seeds_vec.push(b);
|
||||
}
|
||||
|
||||
let (name_account_key, bump) =
|
||||
Pubkey::find_program_address(&seeds_vec.chunks(32).collect::<Vec<&[u8]>>(), program_id);
|
||||
seeds_vec.push(bump);
|
||||
|
||||
(name_account_key, seeds_vec)
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
use std::str::FromStr;
|
||||
|
||||
use solana_program::{instruction::Instruction, program_pack::Pack, pubkey::Pubkey};
|
||||
use solana_program_test::{processor, tokio, ProgramTest, ProgramTestContext};
|
||||
|
||||
use solana_program::hash::hashv;
|
||||
use solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transaction::Transaction,
|
||||
transport::TransportError,
|
||||
};
|
||||
use spl_name_service::{
|
||||
entrypoint::process_instruction,
|
||||
instruction::{create, delete, transfer, update, NameRegistryInstruction},
|
||||
state::{get_seeds_and_key, NameRecordHeader, HASH_PREFIX},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_name_service() {
|
||||
// Create program and test environment
|
||||
let program_id = Pubkey::from_str("XCWuBvfNamesXCWuBvfkegQfZyiNwAJb9Ss623VQ5DA").unwrap();
|
||||
|
||||
let program_test = ProgramTest::new(
|
||||
"spl_name_service",
|
||||
program_id,
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
let mut ctx = program_test.start_with_context().await;
|
||||
|
||||
let root_name = ".sol";
|
||||
let tld_class = Keypair::new();
|
||||
let owner = Keypair::new();
|
||||
|
||||
let hashed_root_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + root_name).as_bytes()])
|
||||
.0
|
||||
.to_vec();
|
||||
let (root_name_account_key, _) = get_seeds_and_key(
|
||||
&program_id,
|
||||
hashed_root_name.clone(),
|
||||
Some(&tld_class.pubkey()),
|
||||
None,
|
||||
);
|
||||
|
||||
let create_name_instruction = create(
|
||||
program_id,
|
||||
NameRegistryInstruction::Create {
|
||||
hashed_name: hashed_root_name,
|
||||
lamports: 1_000_000,
|
||||
space: 1_000,
|
||||
},
|
||||
root_name_account_key,
|
||||
ctx.payer.pubkey(),
|
||||
owner.pubkey(),
|
||||
Some(tld_class.pubkey()),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
sign_send_instruction(&mut ctx, create_name_instruction, vec![&tld_class])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(
|
||||
&mut &ctx
|
||||
.banks_client
|
||||
.get_account(root_name_account_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.data,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Name Record Header: {:?}", name_record_header);
|
||||
|
||||
let name = "bonfida";
|
||||
let sol_subdomains_class = Keypair::new();
|
||||
|
||||
let hashed_name: Vec<u8> = hashv(&[(HASH_PREFIX.to_owned() + name).as_bytes()])
|
||||
.0
|
||||
.to_vec();
|
||||
let (name_account_key, _) = get_seeds_and_key(
|
||||
&program_id,
|
||||
hashed_name.clone(),
|
||||
Some(&sol_subdomains_class.pubkey()),
|
||||
Some(&root_name_account_key),
|
||||
);
|
||||
|
||||
let create_name_instruction = create(
|
||||
program_id,
|
||||
NameRegistryInstruction::Create {
|
||||
hashed_name,
|
||||
lamports: 1_000_000,
|
||||
space: 1_000,
|
||||
},
|
||||
name_account_key,
|
||||
ctx.payer.pubkey(),
|
||||
owner.pubkey(),
|
||||
Some(sol_subdomains_class.pubkey()),
|
||||
Some(root_name_account_key),
|
||||
Some(owner.pubkey()),
|
||||
)
|
||||
.unwrap();
|
||||
sign_send_instruction(
|
||||
&mut ctx,
|
||||
create_name_instruction,
|
||||
vec![&sol_subdomains_class, &owner],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(
|
||||
&mut &ctx
|
||||
.banks_client
|
||||
.get_account(name_account_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.data,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Name Record Header: {:?}", name_record_header);
|
||||
println!("SOl class {:?}", sol_subdomains_class.pubkey());
|
||||
|
||||
let data = "@Dudl".as_bytes().to_vec();
|
||||
let update_instruction = update(
|
||||
program_id,
|
||||
0,
|
||||
data,
|
||||
name_account_key,
|
||||
sol_subdomains_class.pubkey(),
|
||||
)
|
||||
.unwrap();
|
||||
sign_send_instruction(&mut ctx, update_instruction, vec![&sol_subdomains_class])
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(
|
||||
&mut &ctx
|
||||
.banks_client
|
||||
.get_account(name_account_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.data,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Name Record Header: {:?}", name_record_header);
|
||||
|
||||
let transfer_instruction = transfer(
|
||||
program_id,
|
||||
ctx.payer.pubkey(),
|
||||
name_account_key,
|
||||
owner.pubkey(),
|
||||
Some(sol_subdomains_class.pubkey()),
|
||||
)
|
||||
.unwrap();
|
||||
sign_send_instruction(
|
||||
&mut ctx,
|
||||
transfer_instruction,
|
||||
vec![&owner, &sol_subdomains_class],
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name_record_header = NameRecordHeader::unpack_from_slice(
|
||||
&mut &ctx
|
||||
.banks_client
|
||||
.get_account(name_account_key)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.data,
|
||||
)
|
||||
.unwrap();
|
||||
println!("Name Record Header: {:?}", name_record_header);
|
||||
|
||||
let delete_instruction = delete(
|
||||
program_id,
|
||||
name_account_key,
|
||||
ctx.payer.pubkey(),
|
||||
ctx.payer.pubkey(),
|
||||
)
|
||||
.unwrap();
|
||||
sign_send_instruction(&mut ctx, delete_instruction, vec![])
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Utils
|
||||
pub async fn sign_send_instruction(
|
||||
ctx: &mut ProgramTestContext,
|
||||
instruction: Instruction,
|
||||
signers: Vec<&Keypair>,
|
||||
) -> Result<(), TransportError> {
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&ctx.payer.pubkey()));
|
||||
let mut payer_signers = vec![&ctx.payer];
|
||||
for s in signers {
|
||||
payer_signers.push(s);
|
||||
}
|
||||
transaction.partial_sign(&payer_signers, ctx.last_blockhash);
|
||||
ctx.banks_client.process_transaction(transaction).await
|
||||
}
|
|
@ -16,12 +16,12 @@ borsh = "0.8.1"
|
|||
borsh-derive = "0.8.1"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -7,14 +7,16 @@ repository = "https://github.com/solana-labs/solana-program-library"
|
|||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
test-bpf = []
|
||||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "=1.6.7"
|
||||
|
||||
[dev-dependencies]
|
||||
solana-bpf-loader-program = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana_rbpf = "0.2"
|
||||
solana-program-test = "=1.6.7"
|
||||
solana-sdk = "=1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -1,180 +1,185 @@
|
|||
use solana_bpf_loader_program::serialization::serialize_parameters;
|
||||
use solana_program::{
|
||||
bpf_loader, entrypoint::SUCCESS, program_error::ProgramError, pubkey::Pubkey,
|
||||
// Program test does not support calling a raw program entrypoint, only `process_instruction`
|
||||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
account::Account,
|
||||
instruction::InstructionError,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
signature::Signer,
|
||||
transaction::{Transaction, TransactionError},
|
||||
};
|
||||
use solana_sdk::{account::AccountSharedData, keyed_account::KeyedAccount};
|
||||
use spl_shared_memory::entrypoint;
|
||||
|
||||
// TODO: Rework `assert_instruction_count` test to use solana-program-test, avoiding the need to
|
||||
// link directly with the BPF VM
|
||||
/*
|
||||
fn load_program(name: &str) -> Vec<u8> {
|
||||
let mut file =
|
||||
File::open(&name).unwrap_or_else(|err| panic!("Unable to open {}: {}", name, err));
|
||||
|
||||
let mut program = Vec::new();
|
||||
file.read_to_end(&mut program).unwrap();
|
||||
program
|
||||
}
|
||||
|
||||
fn run_program(
|
||||
program_id: &Pubkey,
|
||||
parameter_accounts: &[KeyedAccount],
|
||||
instruction_data: &[u8],
|
||||
) -> u64 {
|
||||
let program_account = Account {
|
||||
data: load_program("../../target/deploy/spl_shared_memory.so"),
|
||||
..Account::default()
|
||||
};
|
||||
let loader_id = bpf_loader::id();
|
||||
let mut invoke_context = MockInvokeContext::default();
|
||||
let executable = EbpfVm::<solana_bpf_loader_program::BPFError>::create_executable_from_elf(
|
||||
&&program_account.data,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let (mut vm, heap_region) = create_vm(
|
||||
&loader_id,
|
||||
executable.as_ref(),
|
||||
parameter_accounts,
|
||||
&mut invoke_context,
|
||||
)
|
||||
.unwrap();
|
||||
let mut parameter_bytes = serialize_parameters(
|
||||
&loader_id,
|
||||
program_id,
|
||||
parameter_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
SUCCESS,
|
||||
vm.execute_program(parameter_bytes.as_mut_slice(), &[], &[heap_region])
|
||||
.unwrap()
|
||||
);
|
||||
deserialize_parameters(&loader_id, parameter_accounts, ¶meter_bytes).unwrap();
|
||||
vm.get_total_instruction_count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assert_instruction_count() {
|
||||
#[tokio::test]
|
||||
async fn assert_instruction_count() {
|
||||
const OFFSET: usize = 51;
|
||||
const NUM_TO_SHARE: usize = 500;
|
||||
let program_id = Pubkey::new_unique();
|
||||
let shared_key = Pubkey::new_unique();
|
||||
let shared_account = Account::new_ref(u64::MAX, OFFSET + NUM_TO_SHARE * 2, &program_id);
|
||||
|
||||
// Send some data to share
|
||||
let parameter_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let share_count = run_program(&program_id, ¶meter_accounts[..], &instruction_data);
|
||||
const BASELINE_COUNT: u64 = 1474; // 113 if NUM_TO_SHARE is 8
|
||||
println!(
|
||||
"BPF instructions executed {:?} (expected {:?})",
|
||||
share_count, BASELINE_COUNT
|
||||
let mut program_test = ProgramTest::new(
|
||||
"spl_shared_memory", // Run the BPF version with `cargo test-bpf`
|
||||
program_id,
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
&shared_account.borrow().data[OFFSET..OFFSET + NUM_TO_SHARE],
|
||||
content
|
||||
program_test.add_account(
|
||||
shared_key,
|
||||
Account {
|
||||
lamports: 5000000000000,
|
||||
data: vec![0_u8; NUM_TO_SHARE * 2],
|
||||
owner: program_id,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
assert!(share_count <= BASELINE_COUNT);
|
||||
}
|
||||
*/
|
||||
|
||||
#[test]
|
||||
fn test_share_data() {
|
||||
const OFFSET: usize = 51;
|
||||
const NUM_TO_SHARE: usize = 500;
|
||||
let program_id = Pubkey::new(&[0; 32]);
|
||||
let shared_key = Pubkey::new_unique();
|
||||
let shared_account = AccountSharedData::new_ref(u64::MAX, NUM_TO_SHARE * 2, &program_id);
|
||||
program_test.set_bpf_compute_max_units(480);
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
|
||||
|
||||
// success
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
|
||||
let mut input = serialize_parameters(
|
||||
&bpf_loader::id(),
|
||||
&program_id,
|
||||
&keyed_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(unsafe { entrypoint(input.as_mut_ptr()) }, SUCCESS);
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![AccountMeta::new(shared_key, false)],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_helloworld() {
|
||||
const OFFSET: usize = 51;
|
||||
const NUM_TO_SHARE: usize = 500;
|
||||
let program_id = Pubkey::new_unique();
|
||||
let shared_key = Pubkey::new_unique();
|
||||
|
||||
let mut program_test = ProgramTest::new(
|
||||
"spl_shared_memory", // Run the BPF version with `cargo test-bpf`
|
||||
program_id,
|
||||
None,
|
||||
);
|
||||
program_test.add_account(
|
||||
shared_key,
|
||||
Account {
|
||||
lamports: 5000000000000,
|
||||
data: vec![0_u8; NUM_TO_SHARE * 2],
|
||||
owner: program_id,
|
||||
..Account::default()
|
||||
},
|
||||
);
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
|
||||
|
||||
// success
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![AccountMeta::new(shared_key, false)],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// success zero offset
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = 0_usize.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
|
||||
let mut input = serialize_parameters(
|
||||
&bpf_loader::id(),
|
||||
&program_id,
|
||||
&keyed_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(unsafe { entrypoint(input.as_mut_ptr()) }, SUCCESS);
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![AccountMeta::new(shared_key, false)],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// too few accounts
|
||||
let mut input =
|
||||
serialize_parameters(&bpf_loader::id(), &program_id, &[], &instruction_data).unwrap();
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let result = banks_client.process_transaction(transaction).await;
|
||||
assert_eq!(
|
||||
unsafe { entrypoint(input.as_mut_ptr()) },
|
||||
u64::from(ProgramError::NotEnoughAccountKeys)
|
||||
result.unwrap_err().unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::NotEnoughAccountKeys)
|
||||
);
|
||||
|
||||
// too many accounts
|
||||
let keyed_accounts = vec![
|
||||
KeyedAccount::new(&shared_key, true, &shared_account),
|
||||
KeyedAccount::new(&shared_key, true, &shared_account),
|
||||
];
|
||||
let mut input = serialize_parameters(
|
||||
&bpf_loader::id(),
|
||||
&program_id,
|
||||
&keyed_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![
|
||||
AccountMeta::new(shared_key, false),
|
||||
AccountMeta::new(shared_key, false),
|
||||
],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let result = banks_client.process_transaction(transaction).await;
|
||||
assert_eq!(
|
||||
unsafe { entrypoint(input.as_mut_ptr()) },
|
||||
u64::from(ProgramError::InvalidArgument)
|
||||
result.unwrap_err().unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::InvalidArgument)
|
||||
);
|
||||
|
||||
// account data too small
|
||||
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
|
||||
let content = vec![42; NUM_TO_SHARE * 10];
|
||||
let mut instruction_data = OFFSET.to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let mut input = serialize_parameters(
|
||||
&bpf_loader::id(),
|
||||
&program_id,
|
||||
&keyed_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![AccountMeta::new(shared_key, false)],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let result = banks_client.process_transaction(transaction).await;
|
||||
assert_eq!(
|
||||
unsafe { entrypoint(input.as_mut_ptr()) },
|
||||
u64::from(ProgramError::AccountDataTooSmall)
|
||||
result.unwrap_err().unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall)
|
||||
);
|
||||
|
||||
// offset too large
|
||||
let keyed_accounts = vec![KeyedAccount::new(&shared_key, true, &shared_account)];
|
||||
let content = vec![42; NUM_TO_SHARE];
|
||||
let mut instruction_data = (OFFSET * 10).to_le_bytes().to_vec();
|
||||
instruction_data.extend_from_slice(&content);
|
||||
let mut input = serialize_parameters(
|
||||
&bpf_loader::id(),
|
||||
&program_id,
|
||||
&keyed_accounts,
|
||||
&instruction_data,
|
||||
)
|
||||
.unwrap();
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[Instruction::new_with_bytes(
|
||||
program_id,
|
||||
&instruction_data,
|
||||
vec![AccountMeta::new(shared_key, false)],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let result = banks_client.process_transaction(transaction).await;
|
||||
assert_eq!(
|
||||
unsafe { entrypoint(input.as_mut_ptr()) },
|
||||
u64::from(ProgramError::AccountDataTooSmall)
|
||||
result.unwrap_err().unwrap(),
|
||||
TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,21 +6,22 @@ homepage = "https://spl.solana.com/stake-pool"
|
|||
license = "Apache-2.0"
|
||||
name = "spl-stake-pool-cli"
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
version = "2.0.1"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
borsh = "0.8"
|
||||
clap = "2.33.3"
|
||||
serde_json = "1.0.62"
|
||||
solana-account-decoder = "1.6.2"
|
||||
solana-clap-utils = "1.6.2"
|
||||
solana-cli-config = "1.6.2"
|
||||
solana-client = "1.6.2"
|
||||
solana-logger = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program = "1.6.2"
|
||||
spl-stake-pool = { path="../program", features = [ "no-entrypoint" ] }
|
||||
spl-token = { path="../../token/program", features = [ "no-entrypoint" ] }
|
||||
solana-account-decoder = "1.6.7"
|
||||
solana-clap-utils = "1.6.7"
|
||||
solana-cli-config = "1.6.7"
|
||||
solana-client = "1.6.7"
|
||||
solana-logger = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-program = "1.6.7"
|
||||
spl-associated-token-account = { version = "1.0", path="../../associated-token-account/program", features = [ "no-entrypoint" ] }
|
||||
spl-stake-pool = { version = "0.1", path="../program", features = [ "no-entrypoint" ] }
|
||||
spl-token = { version = "3.1", path="../../token/program", features = [ "no-entrypoint" ] }
|
||||
bs58 = "0.4.0"
|
||||
bincode = "1.3.1"
|
||||
lazy_static = "1.4.0"
|
||||
|
|
|
@ -18,17 +18,17 @@ use {
|
|||
|
||||
type Error = Box<dyn std::error::Error>;
|
||||
|
||||
pub(crate) fn get_stake_pool(
|
||||
pub fn get_stake_pool(
|
||||
rpc_client: &RpcClient,
|
||||
pool_address: &Pubkey,
|
||||
stake_pool_address: &Pubkey,
|
||||
) -> Result<StakePool, Error> {
|
||||
let account_data = rpc_client.get_account_data(pool_address)?;
|
||||
let account_data = rpc_client.get_account_data(stake_pool_address)?;
|
||||
let stake_pool = StakePool::try_from_slice(account_data.as_slice())
|
||||
.map_err(|err| format!("Invalid stake pool {}: {}", pool_address, err))?;
|
||||
.map_err(|err| format!("Invalid stake pool {}: {}", stake_pool_address, err))?;
|
||||
Ok(stake_pool)
|
||||
}
|
||||
|
||||
pub(crate) fn get_validator_list(
|
||||
pub fn get_validator_list(
|
||||
rpc_client: &RpcClient,
|
||||
validator_list_address: &Pubkey,
|
||||
) -> Result<ValidatorList, Error> {
|
||||
|
@ -38,7 +38,7 @@ pub(crate) fn get_validator_list(
|
|||
Ok(validator_list)
|
||||
}
|
||||
|
||||
pub(crate) fn get_token_account(
|
||||
pub fn get_token_account(
|
||||
rpc_client: &RpcClient,
|
||||
token_account_address: &Pubkey,
|
||||
expected_token_mint: &Pubkey,
|
||||
|
@ -58,7 +58,7 @@ pub(crate) fn get_token_account(
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_token_mint(
|
||||
pub fn get_token_mint(
|
||||
rpc_client: &RpcClient,
|
||||
token_mint_address: &Pubkey,
|
||||
) -> Result<spl_token::state::Mint, Error> {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -19,17 +19,17 @@ num-traits = "0.2"
|
|||
num_enum = "0.5.1"
|
||||
serde = "1.0.121"
|
||||
serde_derive = "1.0.103"
|
||||
solana-program = "1.6.2"
|
||||
spl-math = { path = "../../libraries/math", features = [ "no-entrypoint" ] }
|
||||
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
solana-program = "1.6.7"
|
||||
spl-math = { version = "0.1", path = "../../libraries/math", features = [ "no-entrypoint" ] }
|
||||
spl-token = { version = "3.1", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
bincode = "1.3.1"
|
||||
|
||||
[dev-dependencies]
|
||||
proptest = "0.10"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-vote-program = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
solana-vote-program = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -28,16 +28,16 @@ pub enum StakePoolError {
|
|||
/// Token account is associated with the wrong mint.
|
||||
#[error("WrongAccountMint")]
|
||||
WrongAccountMint,
|
||||
/// Wrong pool owner account.
|
||||
#[error("WrongOwner")]
|
||||
WrongOwner,
|
||||
/// Wrong pool manager account.
|
||||
#[error("WrongManager")]
|
||||
WrongManager,
|
||||
/// Required signature is missing.
|
||||
#[error("SignatureMissing")]
|
||||
SignatureMissing,
|
||||
/// Invalid validator stake list account.
|
||||
#[error("InvalidValidatorStakeList")]
|
||||
InvalidValidatorStakeList,
|
||||
/// Invalid owner fee account.
|
||||
/// Invalid manager fee account.
|
||||
#[error("InvalidFeeAccount")]
|
||||
InvalidFeeAccount,
|
||||
|
||||
|
@ -79,6 +79,15 @@ pub enum StakePoolError {
|
|||
/// The size of the given validator stake list does match the expected amount
|
||||
#[error("UnexpectedValidatorListAccountSize")]
|
||||
UnexpectedValidatorListAccountSize,
|
||||
/// Wrong pool staker account.
|
||||
#[error("WrongStaker")]
|
||||
WrongStaker,
|
||||
/// Pool token supply is not zero on initialization
|
||||
#[error("NonZeroPoolTokenSupply")]
|
||||
NonZeroPoolTokenSupply,
|
||||
/// The lamports in the validator stake account is not equal to the minimum
|
||||
#[error("StakeLamportsNotEqualToMinimum")]
|
||||
StakeLamportsNotEqualToMinimum,
|
||||
}
|
||||
impl From<StakePoolError> for ProgramError {
|
||||
fn from(e: StakePoolError) -> Self {
|
||||
|
|
|
@ -3,7 +3,10 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use {
|
||||
crate::stake_program,
|
||||
crate::{
|
||||
find_deposit_authority_program_address, find_stake_program_address,
|
||||
find_transient_stake_program_address, stake_program, state::Fee,
|
||||
},
|
||||
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
|
||||
solana_program::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
|
@ -13,16 +16,6 @@ use {
|
|||
},
|
||||
};
|
||||
|
||||
/// Fee rate as a ratio, minted on deposit
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
|
||||
pub struct Fee {
|
||||
/// denominator of the fee ratio
|
||||
pub denominator: u64,
|
||||
/// numerator of the fee ratio
|
||||
pub numerator: u64,
|
||||
}
|
||||
|
||||
/// Instructions supported by the StakePool program.
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
|
||||
|
@ -30,15 +23,21 @@ pub enum StakePoolInstruction {
|
|||
/// Initializes a new StakePool.
|
||||
///
|
||||
/// 0. `[w]` New StakePool to create.
|
||||
/// 1. `[s]` Owner
|
||||
/// 2. `[w]` Uninitialized validator stake list storage account
|
||||
/// 3. `[]` pool token Mint. Must be non zero, owned by withdraw authority.
|
||||
/// 4. `[]` Pool Account to deposit the generated fee for owner.
|
||||
/// 5. `[]` Clock sysvar
|
||||
/// 6. `[]` Rent sysvar
|
||||
/// 7. `[]` Token program id
|
||||
/// 1. `[s]` Manager
|
||||
/// 2. `[]` Staker
|
||||
/// 3. `[w]` Uninitialized validator stake list storage account
|
||||
/// 4. `[]` Reserve stake account must be initialized, have zero balance,
|
||||
/// and staker / withdrawer authority set to pool withdraw authority.
|
||||
/// 5. `[]` Pool token mint. Must have zero supply, owned by withdraw authority.
|
||||
/// 6. `[]` Pool account to deposit the generated fee for manager.
|
||||
/// 7. `[]` Clock sysvar
|
||||
/// 8. `[]` Rent sysvar
|
||||
/// 9. `[]` Token program id
|
||||
/// 10. `[]` (Optional) Deposit authority that must sign all deposits.
|
||||
/// Defaults to the program address generated using
|
||||
/// `find_deposit_authority_program_address`, making deposits permissionless.
|
||||
Initialize {
|
||||
/// Deposit fee assessed
|
||||
/// Fee assessed as percentage of perceived rewards
|
||||
#[allow(dead_code)] // but it's not
|
||||
fee: Fee,
|
||||
/// Maximum expected number of validators
|
||||
|
@ -46,62 +45,151 @@ pub enum StakePoolInstruction {
|
|||
max_validators: u32,
|
||||
},
|
||||
|
||||
/// Creates new program account for accumulating stakes for a particular validator
|
||||
/// (Staker only) Creates new program account for accumulating stakes for
|
||||
/// a particular validator
|
||||
///
|
||||
/// 0. `[]` Stake pool account this stake will belong to
|
||||
/// 1. `[s]` Owner
|
||||
/// 1. `[s]` Staker
|
||||
/// 2. `[ws]` Funding account (must be a system account)
|
||||
/// 3. `[w]` Stake account to be created
|
||||
/// 4. `[]` Validator this stake account will vote for
|
||||
/// 5. `[]` Rent sysvar
|
||||
/// 6. `[]` System program
|
||||
/// 7. `[]` Stake program
|
||||
/// 6. `[]` Stake History sysvar
|
||||
/// 7. `[]` Stake Config sysvar
|
||||
/// 8. `[]` System program
|
||||
/// 9. `[]` Stake program
|
||||
CreateValidatorStakeAccount,
|
||||
|
||||
/// Adds stake account delegated to validator to the pool's list of
|
||||
/// managed validators
|
||||
/// (Staker only) Adds stake account delegated to validator to the pool's
|
||||
/// list of managed validators.
|
||||
///
|
||||
/// The stake account must have the rent-exempt amount plus at least 1 SOL,
|
||||
/// and at most 1.001 SOL.
|
||||
///
|
||||
/// Once we delegate even 1 SOL, it will accrue rewards one epoch later,
|
||||
/// so we'll have more than 1 active SOL at this point.
|
||||
/// At 10% annualized rewards, 1 epoch of 2 days will accrue
|
||||
/// 0.000547945 SOL, so we check that it is at least 1 SOL, and at most
|
||||
/// 1.001 SOL.
|
||||
///
|
||||
/// 0. `[w]` Stake pool
|
||||
/// 1. `[s]` Owner
|
||||
/// 2. `[]` Stake pool deposit authority
|
||||
/// 3. `[]` Stake pool withdraw authority
|
||||
/// 4. `[w]` Validator stake list storage account
|
||||
/// 5. `[w]` Stake account to add to the pool, its withdraw authority should be set to stake pool deposit
|
||||
/// 6. `[w]` User account to receive pool tokens
|
||||
/// 7. `[w]` Pool token mint account
|
||||
/// 8. `[]` Clock sysvar (required)
|
||||
/// 9. '[]' Sysvar stake history account
|
||||
/// 10. `[]` Pool token program id,
|
||||
/// 11. `[]` Stake program id,
|
||||
/// 1. `[s]` Staker
|
||||
/// 2. `[]` Stake pool withdraw authority
|
||||
/// 3. `[w]` Validator stake list storage account
|
||||
/// 4. `[w]` Stake account to add to the pool, its withdraw authority must
|
||||
/// be set to the staker
|
||||
/// 5. `[]` Clock sysvar
|
||||
/// 6. '[]' Sysvar stake history account
|
||||
/// 7. `[]` Stake program
|
||||
AddValidatorToPool,
|
||||
|
||||
/// Removes validator stake account from the pool
|
||||
/// (Staker only) Removes validator from the pool
|
||||
///
|
||||
/// Only succeeds if the validator stake account has the minimum of 1 SOL
|
||||
/// plus the rent-exempt amount.
|
||||
///
|
||||
/// 0. `[w]` Stake pool
|
||||
/// 1. `[s]` Owner
|
||||
/// 1. `[s]` Staker
|
||||
/// 2. `[]` Stake pool withdraw authority
|
||||
/// 3. `[]` New withdraw/staker authority to set in the stake account
|
||||
/// 4. `[w]` Validator stake list storage account
|
||||
/// 5. `[w]` Stake account to remove from the pool
|
||||
/// 6. `[w]` User account with pool tokens to burn from
|
||||
/// 7. `[w]` Pool token mint account
|
||||
/// 8. '[]' Sysvar clock account (required)
|
||||
/// 9. `[]` Pool token program id
|
||||
/// 10. `[]` Stake program id,
|
||||
/// 6. `[]` Transient stake account, to check that that we're not trying to activate
|
||||
/// 7. '[]' Sysvar clock
|
||||
/// 8. `[]` Stake program id,
|
||||
RemoveValidatorFromPool,
|
||||
|
||||
/// Updates balances of validator stake accounts in the pool
|
||||
/// (Staker only) Decrease active stake on a validator, eventually moving it to the reserve
|
||||
///
|
||||
/// 0. `[w]` Validator stake list storage account
|
||||
/// 1. `[]` Sysvar clock account
|
||||
/// 2. ..2+N ` [] N validator stake accounts to update balances
|
||||
UpdateValidatorListBalance,
|
||||
/// Internally, this instruction splits a validator stake account into its
|
||||
/// corresponding transient stake account and deactivates it.
|
||||
///
|
||||
/// In order to rebalance the pool without taking custody, the staker needs
|
||||
/// a way of reducing the stake on a stake account. This instruction splits
|
||||
/// some amount of stake, up to the total activated stake, from the canonical
|
||||
/// validator stake account, into its "transient" stake account.
|
||||
///
|
||||
/// The instruction only succeeds if the transient stake account does not
|
||||
/// exist. The amount of lamports to move must be at least rent-exemption
|
||||
/// plus 1 lamport.
|
||||
///
|
||||
/// 0. `[]` Stake pool
|
||||
/// 1. `[s]` Stake pool staker
|
||||
/// 2. `[]` Stake pool withdraw authority
|
||||
/// 3. `[]` Validator list
|
||||
/// 5. `[w]` Canonical stake account to split from
|
||||
/// 5. `[w]` Transient stake account to receive split
|
||||
/// 6. `[]` Clock sysvar
|
||||
/// 7. `[]` Rent sysvar
|
||||
/// 8. `[]` System program
|
||||
/// 9. `[]` Stake program
|
||||
/// userdata: amount of lamports to split into the transient stake account
|
||||
DecreaseValidatorStake(u64),
|
||||
|
||||
/// Updates total pool balance based on balances in validator stake account list storage
|
||||
/// (Staker only) Increase stake on a validator from the reserve account
|
||||
///
|
||||
/// Internally, this instruction splits reserve stake into a transient stake
|
||||
/// account and delegate to the appropriate validator. `UpdateValidatorListBalance`
|
||||
/// will do the work of merging once it's ready.
|
||||
///
|
||||
/// This instruction only succeeds if the transient stake account does not exist.
|
||||
/// The minimum amount to move is rent-exemption plus 1 SOL in order to avoid
|
||||
/// issues on credits observed when merging active stakes later.
|
||||
///
|
||||
/// 0. `[]` Stake pool
|
||||
/// 1. `[s]` Stake pool staker
|
||||
/// 2. `[]` Stake pool withdraw authority
|
||||
/// 3. `[w]` Validator list
|
||||
/// 4. `[w]` Stake pool reserve stake
|
||||
/// 5. `[w]` Transient stake account
|
||||
/// 6. `[]` Validator vote account to delegate to
|
||||
/// 7. '[]' Clock sysvar
|
||||
/// 8. '[]' Rent sysvar
|
||||
/// 9. `[]` Stake History sysvar
|
||||
/// 10. `[]` Stake Config sysvar
|
||||
/// 11. `[]` System program
|
||||
/// 12. `[]` Stake program
|
||||
/// userdata: amount of lamports to split into the transient stake account
|
||||
IncreaseValidatorStake(u64),
|
||||
|
||||
/// Updates balances of validator and transient stake accounts in the pool
|
||||
///
|
||||
/// While going through the pairs of validator and transient stake accounts,
|
||||
/// if the transient stake is inactive, it is merged into the reserve stake
|
||||
/// account. If the transient stake is active and has matching credits
|
||||
/// observed, it is merged into the canonical validator stake account. In
|
||||
/// all other states, nothing is done, and the balance is simply added to
|
||||
/// the canonical stake account balance.
|
||||
///
|
||||
/// 0. `[]` Stake pool
|
||||
/// 1. `[]` Stake pool withdraw authority
|
||||
/// 2. `[w]` Validator stake list storage account
|
||||
/// 3. `[w]` Reserve stake account
|
||||
/// 4. `[]` Sysvar clock
|
||||
/// 5. `[]` Sysvar stake history
|
||||
/// 6. `[]` Stake program
|
||||
/// 7. ..7+N ` [] N pairs of validator and transient stake accounts
|
||||
UpdateValidatorListBalance {
|
||||
/// Index to start updating on the validator list
|
||||
#[allow(dead_code)] // but it's not
|
||||
start_index: u32,
|
||||
/// If true, don't try merging transient stake accounts into the reserve or
|
||||
/// validator stake account. Useful for testing or if a particular stake
|
||||
/// account is in a bad state, but we still want to update
|
||||
#[allow(dead_code)] // but it's not
|
||||
no_merge: bool,
|
||||
},
|
||||
|
||||
/// Updates total pool balance based on balances in the reserve and validator list
|
||||
///
|
||||
/// 0. `[w]` Stake pool
|
||||
/// 1. `[]` Validator stake list storage account
|
||||
/// 2. `[]` Sysvar clock account
|
||||
/// 1. `[]` Stake pool withdraw authority
|
||||
/// 2. `[]` Validator stake list storage account
|
||||
/// 3. `[]` Reserve stake account
|
||||
/// 4. `[w]` Account to receive pool fee tokens
|
||||
/// 5. `[w]` Pool mint account
|
||||
/// 6. `[]` Sysvar clock account
|
||||
/// 7. `[]` Pool token program
|
||||
UpdateStakePoolBalance,
|
||||
|
||||
/// Deposit some stake into the pool. The output is a "pool" token representing ownership
|
||||
|
@ -111,10 +199,9 @@ pub enum StakePoolInstruction {
|
|||
/// 1. `[w]` Validator stake list storage account
|
||||
/// 2. `[]` Stake pool deposit authority
|
||||
/// 3. `[]` Stake pool withdraw authority
|
||||
/// 4. `[w]` Stake account to join the pool (withdraw should be set to stake pool deposit)
|
||||
/// 4. `[w]` Stake account to join the pool (withdraw authority for the stake account should be first set to the stake pool deposit authority)
|
||||
/// 5. `[w]` Validator stake account for the stake account to be merged with
|
||||
/// 6. `[w]` User account to receive pool tokens
|
||||
/// 7. `[w]` Account to receive pool fee tokens
|
||||
/// 8. `[w]` Pool token mint account
|
||||
/// 9. '[]' Sysvar clock account (required)
|
||||
/// 10. '[]' Sysvar stake history account
|
||||
|
@ -123,40 +210,69 @@ pub enum StakePoolInstruction {
|
|||
Deposit,
|
||||
|
||||
/// Withdraw the token from the pool at the current ratio.
|
||||
/// The amount withdrawn is the MIN(u64, stake size)
|
||||
///
|
||||
/// Succeeds if the stake account has enough SOL to cover the desired amount
|
||||
/// of pool tokens, and if the withdrawal keeps the total staked amount
|
||||
/// above the minimum of rent-exempt amount + 1 SOL.
|
||||
///
|
||||
/// A validator stake account can be withdrawn from freely, and the reserve
|
||||
/// can only be drawn from if there is no active stake left, where all
|
||||
/// validator accounts are left with 1 lamport.
|
||||
///
|
||||
/// 0. `[w]` Stake pool
|
||||
/// 1. `[w]` Validator stake list storage account
|
||||
/// 2. `[]` Stake pool withdraw authority
|
||||
/// 3. `[w]` Validator stake account to split
|
||||
/// 3. `[w]` Validator or reserve stake account to split
|
||||
/// 4. `[w]` Unitialized stake account to receive withdrawal
|
||||
/// 5. `[]` User account to set as a new withdraw authority
|
||||
/// 6. `[w]` User account with pool tokens to burn from
|
||||
/// 7. `[w]` Pool token mint account
|
||||
/// 8. '[]' Sysvar clock account (required)
|
||||
/// 9. `[]` Pool token program id
|
||||
/// 10. `[]` Stake program id,
|
||||
/// userdata: amount to withdraw
|
||||
/// 6. `[s]` User transfer authority, for pool token account
|
||||
/// 7. `[w]` User account with pool tokens to burn from
|
||||
/// 8. `[w]` Pool token mint account
|
||||
/// 9. `[]` Sysvar clock account (required)
|
||||
/// 10. `[]` Pool token program id
|
||||
/// 11. `[]` Stake program id,
|
||||
/// userdata: amount of pool tokens to withdraw
|
||||
Withdraw(u64),
|
||||
|
||||
/// Update owner
|
||||
/// (Manager only) Update manager
|
||||
///
|
||||
/// 0. `[w]` StakePool
|
||||
/// 1. `[s]` Owner
|
||||
/// 2. '[]` New owner pubkey
|
||||
/// 3. '[]` New owner fee account
|
||||
SetOwner,
|
||||
/// 0. `[w]` StakePool
|
||||
/// 1. `[s]` Manager
|
||||
/// 2. '[]` New manager pubkey
|
||||
/// 3. '[]` New manager fee account
|
||||
SetManager,
|
||||
|
||||
/// (Manager only) Update fee
|
||||
///
|
||||
/// 0. `[w]` StakePool
|
||||
/// 1. `[s]` Manager
|
||||
/// 2. `[]` Sysvar clock
|
||||
SetFee {
|
||||
/// Fee assessed as percentage of perceived rewards
|
||||
#[allow(dead_code)] // but it's not
|
||||
fee: Fee,
|
||||
},
|
||||
|
||||
/// (Manager or staker only) Update staker
|
||||
///
|
||||
/// 0. `[w]` StakePool
|
||||
/// 1. `[s]` Manager or current staker
|
||||
/// 2. '[]` New staker pubkey
|
||||
SetStaker,
|
||||
}
|
||||
|
||||
/// Creates an 'initialize' instruction.
|
||||
pub fn initialize(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
manager: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
reserve_stake: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
owner_pool_account: &Pubkey,
|
||||
manager_pool_account: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
deposit_authority: Option<Pubkey>,
|
||||
fee: Fee,
|
||||
max_validators: u32,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
|
@ -165,16 +281,21 @@ pub fn initialize(
|
|||
max_validators,
|
||||
};
|
||||
let data = init_data.try_to_vec()?;
|
||||
let accounts = vec![
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new(*stake_pool, true),
|
||||
AccountMeta::new_readonly(*owner, true),
|
||||
AccountMeta::new_readonly(*manager, true),
|
||||
AccountMeta::new_readonly(*staker, false),
|
||||
AccountMeta::new(*validator_list, false),
|
||||
AccountMeta::new_readonly(*reserve_stake, false),
|
||||
AccountMeta::new_readonly(*pool_mint, false),
|
||||
AccountMeta::new_readonly(*owner_pool_account, false),
|
||||
AccountMeta::new_readonly(*manager_pool_account, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
];
|
||||
if let Some(deposit_authority) = deposit_authority {
|
||||
accounts.push(AccountMeta::new_readonly(deposit_authority, true));
|
||||
}
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
|
@ -186,14 +307,14 @@ pub fn initialize(
|
|||
pub fn create_validator_stake_account(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
funder: &Pubkey,
|
||||
stake_account: &Pubkey,
|
||||
validator: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*owner, true),
|
||||
AccountMeta::new_readonly(*staker, true),
|
||||
AccountMeta::new(*funder, true),
|
||||
AccountMeta::new(*stake_account, false),
|
||||
AccountMeta::new_readonly(*validator, false),
|
||||
|
@ -215,27 +336,19 @@ pub fn create_validator_stake_account(
|
|||
pub fn add_validator_to_pool(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
stake_pool_deposit: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
stake_pool_withdraw: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
stake_account: &Pubkey,
|
||||
pool_token_receiver: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*owner, true),
|
||||
AccountMeta::new_readonly(*stake_pool_deposit, false),
|
||||
AccountMeta::new_readonly(*staker, true),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw, false),
|
||||
AccountMeta::new(*validator_list, false),
|
||||
AccountMeta::new(*stake_account, false),
|
||||
AccountMeta::new(*pool_token_receiver, false),
|
||||
AccountMeta::new(*pool_mint, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
|
@ -249,26 +362,22 @@ pub fn add_validator_to_pool(
|
|||
pub fn remove_validator_from_pool(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
owner: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
stake_pool_withdraw: &Pubkey,
|
||||
new_stake_authority: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
stake_account: &Pubkey,
|
||||
burn_from: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
transient_stake_account: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*owner, true),
|
||||
AccountMeta::new_readonly(*staker, true),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw, false),
|
||||
AccountMeta::new_readonly(*new_stake_authority, false),
|
||||
AccountMeta::new(*validator_list, false),
|
||||
AccountMeta::new(*stake_account, false),
|
||||
AccountMeta::new(*burn_from, false),
|
||||
AccountMeta::new(*pool_mint, false),
|
||||
AccountMeta::new_readonly(*transient_stake_account, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
|
@ -278,77 +387,255 @@ pub fn remove_validator_from_pool(
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates `DecreaseValidatorStake` instruction (rebalance from validator account to
|
||||
/// transient account)
|
||||
pub fn decrease_validator_stake(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
stake_pool_withdraw_authority: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
validator_stake: &Pubkey,
|
||||
transient_stake: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Instruction {
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*staker, true),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
|
||||
AccountMeta::new_readonly(*validator_list, false),
|
||||
AccountMeta::new(*validator_stake, false),
|
||||
AccountMeta::new(*transient_stake, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::DecreaseValidatorStake(lamports)
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to
|
||||
/// transient account)
|
||||
pub fn increase_validator_stake(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
staker: &Pubkey,
|
||||
stake_pool_withdraw_authority: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
reserve_stake: &Pubkey,
|
||||
transient_stake: &Pubkey,
|
||||
validator: &Pubkey,
|
||||
lamports: u64,
|
||||
) -> Instruction {
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*staker, true),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
|
||||
AccountMeta::new(*validator_list, false),
|
||||
AccountMeta::new(*reserve_stake, false),
|
||||
AccountMeta::new(*transient_stake, false),
|
||||
AccountMeta::new_readonly(*validator, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::config_id(), false),
|
||||
AccountMeta::new_readonly(system_program::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::IncreaseValidatorStake(lamports)
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates `UpdateValidatorListBalance` instruction (update validator stake account balances)
|
||||
pub fn update_validator_list_balance(
|
||||
program_id: &Pubkey,
|
||||
validator_list_storage: &Pubkey,
|
||||
validator_list: &[Pubkey],
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let mut accounts: Vec<AccountMeta> = validator_list
|
||||
.iter()
|
||||
.map(|pubkey| AccountMeta::new_readonly(*pubkey, false))
|
||||
.collect();
|
||||
accounts.insert(0, AccountMeta::new(*validator_list_storage, false));
|
||||
accounts.insert(1, AccountMeta::new_readonly(sysvar::clock::id(), false));
|
||||
Ok(Instruction {
|
||||
stake_pool: &Pubkey,
|
||||
stake_pool_withdraw_authority: &Pubkey,
|
||||
validator_list: &Pubkey,
|
||||
reserve_stake: &Pubkey,
|
||||
validator_vote_accounts: &[Pubkey],
|
||||
start_index: u32,
|
||||
no_merge: bool,
|
||||
) -> Instruction {
|
||||
let mut accounts = vec![
|
||||
AccountMeta::new_readonly(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
|
||||
AccountMeta::new(*validator_list, false),
|
||||
AccountMeta::new(*reserve_stake, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
accounts.append(
|
||||
&mut validator_vote_accounts
|
||||
.iter()
|
||||
.flat_map(|vote_account_address| {
|
||||
let (validator_stake_account, _) =
|
||||
find_stake_program_address(program_id, vote_account_address, stake_pool);
|
||||
let (transient_stake_account, _) = find_transient_stake_program_address(
|
||||
program_id,
|
||||
vote_account_address,
|
||||
stake_pool,
|
||||
);
|
||||
vec![
|
||||
AccountMeta::new(validator_stake_account, false),
|
||||
AccountMeta::new(transient_stake_account, false),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<AccountMeta>>(),
|
||||
);
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::UpdateValidatorListBalance.try_to_vec()?,
|
||||
})
|
||||
data: StakePoolInstruction::UpdateValidatorListBalance {
|
||||
start_index,
|
||||
no_merge,
|
||||
}
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates `UpdateStakePoolBalance` instruction (pool balance from the stake account list balances)
|
||||
pub fn update_stake_pool_balance(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
withdraw_authority: &Pubkey,
|
||||
validator_list_storage: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
reserve_stake: &Pubkey,
|
||||
manager_fee_account: &Pubkey,
|
||||
stake_pool_mint: &Pubkey,
|
||||
) -> Instruction {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*withdraw_authority, false),
|
||||
AccountMeta::new(*validator_list_storage, false),
|
||||
AccountMeta::new_readonly(*reserve_stake, false),
|
||||
AccountMeta::new(*manager_fee_account, false),
|
||||
AccountMeta::new(*stake_pool_mint, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::UpdateStakePoolBalance.try_to_vec()?,
|
||||
})
|
||||
data: StakePoolInstruction::UpdateStakePoolBalance
|
||||
.try_to_vec()
|
||||
.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a 'Deposit' instruction.
|
||||
/// Creates instructions required to deposit into a stake pool, given a stake
|
||||
/// account owned by the user.
|
||||
pub fn deposit(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
validator_list_storage: &Pubkey,
|
||||
stake_pool_deposit: &Pubkey,
|
||||
stake_pool_withdraw: &Pubkey,
|
||||
stake_to_join: &Pubkey,
|
||||
stake_pool_withdraw_authority: &Pubkey,
|
||||
deposit_stake_address: &Pubkey,
|
||||
deposit_stake_withdraw_authority: &Pubkey,
|
||||
validator_stake_accont: &Pubkey,
|
||||
pool_tokens_to: &Pubkey,
|
||||
pool_fee_to: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
) -> Vec<Instruction> {
|
||||
let stake_pool_deposit_authority =
|
||||
find_deposit_authority_program_address(program_id, stake_pool).0;
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new(*validator_list_storage, false),
|
||||
AccountMeta::new_readonly(*stake_pool_deposit, false),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw, false),
|
||||
AccountMeta::new(*stake_to_join, false),
|
||||
AccountMeta::new_readonly(stake_pool_deposit_authority, false),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
|
||||
AccountMeta::new(*deposit_stake_address, false),
|
||||
AccountMeta::new(*validator_stake_accont, false),
|
||||
AccountMeta::new(*pool_tokens_to, false),
|
||||
AccountMeta::new(*pool_fee_to, false),
|
||||
AccountMeta::new(*pool_mint, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::Deposit.try_to_vec()?,
|
||||
})
|
||||
vec![
|
||||
stake_program::authorize(
|
||||
deposit_stake_address,
|
||||
deposit_stake_withdraw_authority,
|
||||
&stake_pool_deposit_authority,
|
||||
stake_program::StakeAuthorize::Staker,
|
||||
),
|
||||
stake_program::authorize(
|
||||
deposit_stake_address,
|
||||
deposit_stake_withdraw_authority,
|
||||
&stake_pool_deposit_authority,
|
||||
stake_program::StakeAuthorize::Withdrawer,
|
||||
),
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::Deposit.try_to_vec().unwrap(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Creates instructions required to deposit into a stake pool, given a stake
|
||||
/// account owned by the user. The difference with `deposit()` is that a deposit
|
||||
/// authority must sign this instruction, which is required for private pools.
|
||||
pub fn deposit_with_authority(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
validator_list_storage: &Pubkey,
|
||||
stake_pool_deposit_authority: &Pubkey,
|
||||
stake_pool_withdraw_authority: &Pubkey,
|
||||
deposit_stake_address: &Pubkey,
|
||||
deposit_stake_withdraw_authority: &Pubkey,
|
||||
validator_stake_accont: &Pubkey,
|
||||
pool_tokens_to: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
) -> Vec<Instruction> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new(*validator_list_storage, false),
|
||||
AccountMeta::new_readonly(*stake_pool_deposit_authority, true),
|
||||
AccountMeta::new_readonly(*stake_pool_withdraw_authority, false),
|
||||
AccountMeta::new(*deposit_stake_address, false),
|
||||
AccountMeta::new(*validator_stake_accont, false),
|
||||
AccountMeta::new(*pool_tokens_to, false),
|
||||
AccountMeta::new(*pool_mint, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
vec![
|
||||
stake_program::authorize(
|
||||
deposit_stake_address,
|
||||
deposit_stake_withdraw_authority,
|
||||
stake_pool_deposit_authority,
|
||||
stake_program::StakeAuthorize::Staker,
|
||||
),
|
||||
stake_program::authorize(
|
||||
deposit_stake_address,
|
||||
deposit_stake_withdraw_authority,
|
||||
stake_pool_deposit_authority,
|
||||
stake_program::StakeAuthorize::Withdrawer,
|
||||
),
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::Deposit.try_to_vec().unwrap(),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/// Creates a 'withdraw' instruction.
|
||||
|
@ -359,8 +646,9 @@ pub fn withdraw(
|
|||
stake_pool_withdraw: &Pubkey,
|
||||
stake_to_split: &Pubkey,
|
||||
stake_to_receive: &Pubkey,
|
||||
user_withdrawer: &Pubkey,
|
||||
burn_from: &Pubkey,
|
||||
user_stake_authority: &Pubkey,
|
||||
user_transfer_authority: &Pubkey,
|
||||
user_pool_token_account: &Pubkey,
|
||||
pool_mint: &Pubkey,
|
||||
token_program_id: &Pubkey,
|
||||
amount: u64,
|
||||
|
@ -371,8 +659,9 @@ pub fn withdraw(
|
|||
AccountMeta::new_readonly(*stake_pool_withdraw, false),
|
||||
AccountMeta::new(*stake_to_split, false),
|
||||
AccountMeta::new(*stake_to_receive, false),
|
||||
AccountMeta::new_readonly(*user_withdrawer, false),
|
||||
AccountMeta::new(*burn_from, false),
|
||||
AccountMeta::new_readonly(*user_stake_authority, false),
|
||||
AccountMeta::new_readonly(*user_transfer_authority, true),
|
||||
AccountMeta::new(*user_pool_token_account, false),
|
||||
AccountMeta::new(*pool_mint, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(*token_program_id, false),
|
||||
|
@ -385,23 +674,61 @@ pub fn withdraw(
|
|||
})
|
||||
}
|
||||
|
||||
/// Creates a 'set owner' instruction.
|
||||
pub fn set_owner(
|
||||
/// Creates a 'set manager' instruction.
|
||||
pub fn set_manager(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
stake_pool_owner: &Pubkey,
|
||||
stake_pool_new_owner: &Pubkey,
|
||||
stake_pool_new_fee_receiver: &Pubkey,
|
||||
manager: &Pubkey,
|
||||
new_manager: &Pubkey,
|
||||
new_fee_receiver: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*stake_pool_owner, true),
|
||||
AccountMeta::new_readonly(*stake_pool_new_owner, false),
|
||||
AccountMeta::new_readonly(*stake_pool_new_fee_receiver, false),
|
||||
AccountMeta::new_readonly(*manager, true),
|
||||
AccountMeta::new_readonly(*new_manager, false),
|
||||
AccountMeta::new_readonly(*new_fee_receiver, false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::SetOwner.try_to_vec()?,
|
||||
data: StakePoolInstruction::SetManager.try_to_vec()?,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a 'set fee' instruction.
|
||||
pub fn set_fee(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
manager: &Pubkey,
|
||||
fee: Fee,
|
||||
) -> Instruction {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*manager, true),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
];
|
||||
Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::SetFee { fee }.try_to_vec().unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a 'set staker' instruction.
|
||||
pub fn set_staker(
|
||||
program_id: &Pubkey,
|
||||
stake_pool: &Pubkey,
|
||||
set_staker_authority: &Pubkey,
|
||||
new_staker: &Pubkey,
|
||||
) -> Result<Instruction, ProgramError> {
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*stake_pool, false),
|
||||
AccountMeta::new_readonly(*set_staker_authority, true),
|
||||
AccountMeta::new_readonly(*new_staker, false),
|
||||
];
|
||||
Ok(Instruction {
|
||||
program_id: *program_id,
|
||||
accounts,
|
||||
data: StakePoolInstruction::SetStaker.try_to_vec()?,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -14,7 +14,10 @@ pub mod entrypoint;
|
|||
|
||||
// Export current sdk types for downstream users building with a different sdk version
|
||||
pub use solana_program;
|
||||
use solana_program::pubkey::Pubkey;
|
||||
use {
|
||||
crate::stake_program::Meta,
|
||||
solana_program::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey},
|
||||
};
|
||||
|
||||
/// Seed for deposit authority seed
|
||||
const AUTHORITY_DEPOSIT: &[u8] = b"deposit";
|
||||
|
@ -22,6 +25,30 @@ const AUTHORITY_DEPOSIT: &[u8] = b"deposit";
|
|||
/// Seed for withdraw authority seed
|
||||
const AUTHORITY_WITHDRAW: &[u8] = b"withdraw";
|
||||
|
||||
/// Seed for transient stake account
|
||||
const TRANSIENT_STAKE_SEED: &[u8] = b"transient";
|
||||
|
||||
/// Minimum amount of staked SOL required in a validator stake account to allow
|
||||
/// for merges without a mismatch on credits observed
|
||||
pub const MINIMUM_ACTIVE_STAKE: u64 = LAMPORTS_PER_SOL;
|
||||
|
||||
/// Maximum amount of validator stake accounts to update per
|
||||
/// `UpdateValidatorListBalance` instruction, based on compute limits
|
||||
pub const MAX_VALIDATORS_TO_UPDATE: usize = 10;
|
||||
|
||||
/// Get the stake amount under consideration when calculating pool token
|
||||
/// conversions
|
||||
pub fn minimum_stake_lamports(meta: &Meta) -> u64 {
|
||||
meta.rent_exempt_reserve
|
||||
.saturating_add(MINIMUM_ACTIVE_STAKE)
|
||||
}
|
||||
|
||||
/// Get the stake amount under consideration when calculating pool token
|
||||
/// conversions
|
||||
pub fn minimum_reserve_lamports(meta: &Meta) -> u64 {
|
||||
meta.rent_exempt_reserve.saturating_add(1)
|
||||
}
|
||||
|
||||
/// Generates the deposit authority program address for the stake pool
|
||||
pub fn find_deposit_authority_program_address(
|
||||
program_id: &Pubkey,
|
||||
|
@ -59,4 +86,20 @@ pub fn find_stake_program_address(
|
|||
)
|
||||
}
|
||||
|
||||
/// Generates the stake program address for a validator's vote account
|
||||
pub fn find_transient_stake_program_address(
|
||||
program_id: &Pubkey,
|
||||
vote_account_address: &Pubkey,
|
||||
stake_pool_address: &Pubkey,
|
||||
) -> (Pubkey, u8) {
|
||||
Pubkey::find_program_address(
|
||||
&[
|
||||
TRANSIENT_STAKE_SEED,
|
||||
&vote_account_address.to_bytes()[..32],
|
||||
&stake_pool_address.to_bytes()[..32],
|
||||
],
|
||||
program_id,
|
||||
)
|
||||
}
|
||||
|
||||
solana_program::declare_id!("poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj");
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,14 +1,22 @@
|
|||
//! FIXME copied from the solana stake program
|
||||
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_program::{
|
||||
clock::{Epoch, UnixTimestamp},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
stake_history::StakeHistory,
|
||||
system_instruction, sysvar,
|
||||
use {
|
||||
borsh::{
|
||||
maybestd::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult},
|
||||
BorshDeserialize, BorshSchema, BorshSerialize,
|
||||
},
|
||||
serde_derive::{Deserialize, Serialize},
|
||||
solana_program::{
|
||||
clock::{Epoch, UnixTimestamp},
|
||||
instruction::{AccountMeta, Instruction},
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
pubkey::Pubkey,
|
||||
stake_history::StakeHistory,
|
||||
system_instruction, sysvar,
|
||||
},
|
||||
std::str::FromStr,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
solana_program::declare_id!("Stake11111111111111111111111111111111111111");
|
||||
|
||||
|
@ -125,8 +133,39 @@ pub enum StakeState {
|
|||
RewardsPool,
|
||||
}
|
||||
|
||||
impl BorshDeserialize for StakeState {
|
||||
fn deserialize(buf: &mut &[u8]) -> IoResult<Self> {
|
||||
let u: u32 = BorshDeserialize::deserialize(buf)?;
|
||||
match u {
|
||||
0 => Ok(StakeState::Uninitialized),
|
||||
1 => {
|
||||
let meta: Meta = BorshDeserialize::deserialize(buf)?;
|
||||
Ok(StakeState::Initialized(meta))
|
||||
}
|
||||
2 => {
|
||||
let meta: Meta = BorshDeserialize::deserialize(buf)?;
|
||||
let stake: Stake = BorshDeserialize::deserialize(buf)?;
|
||||
Ok(StakeState::Stake(meta, stake))
|
||||
}
|
||||
3 => Ok(StakeState::RewardsPool),
|
||||
_ => Err(IoError::new(IoErrorKind::InvalidData, "Invalid enum value")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Default,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct Meta {
|
||||
/// FIXME copied from the stake program
|
||||
pub rent_exempt_reserve: u64,
|
||||
|
@ -137,7 +176,18 @@ pub struct Meta {
|
|||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct Stake {
|
||||
/// FIXME copied from the stake program
|
||||
pub delegation: Delegation,
|
||||
|
@ -146,7 +196,18 @@ pub struct Stake {
|
|||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Debug,
|
||||
Default,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct Delegation {
|
||||
/// to whom the stake is delegated
|
||||
pub voter_pubkey: Pubkey,
|
||||
|
@ -161,7 +222,17 @@ pub struct Delegation {
|
|||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub enum StakeAuthorize {
|
||||
/// FIXME copied from the stake program
|
||||
Staker,
|
||||
|
@ -169,7 +240,18 @@ pub enum StakeAuthorize {
|
|||
Withdrawer,
|
||||
}
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Default,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct Authorized {
|
||||
/// FIXME copied from the stake program
|
||||
pub staker: Pubkey,
|
||||
|
@ -178,7 +260,18 @@ pub struct Authorized {
|
|||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
#[derive(
|
||||
BorshSerialize,
|
||||
BorshDeserialize,
|
||||
BorshSchema,
|
||||
Default,
|
||||
Debug,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialEq,
|
||||
Clone,
|
||||
Copy,
|
||||
)]
|
||||
pub struct Lockup {
|
||||
/// UnixTimestamp at which this stake will allow withdrawal, unless the
|
||||
/// transaction is signed by the custodian
|
||||
|
@ -200,6 +293,14 @@ impl StakeState {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
/// Get meta
|
||||
pub fn meta(&self) -> Option<&Meta> {
|
||||
match self {
|
||||
StakeState::Initialized(meta) => Some(meta),
|
||||
StakeState::Stake(meta, _) => Some(meta),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
|
@ -397,6 +498,41 @@ impl Delegation {
|
|||
}
|
||||
}
|
||||
|
||||
/// FIXME copied from stake program
|
||||
/// Checks if two active delegations are mergeable, required since we cannot recover
|
||||
/// from a CPI error.
|
||||
pub fn active_delegations_can_merge(
|
||||
stake: &Delegation,
|
||||
source: &Delegation,
|
||||
) -> Result<(), ProgramError> {
|
||||
if stake.voter_pubkey != source.voter_pubkey {
|
||||
msg!("Unable to merge due to voter mismatch");
|
||||
Err(ProgramError::InvalidAccountData)
|
||||
} else if (stake.warmup_cooldown_rate - source.warmup_cooldown_rate).abs() < f64::EPSILON
|
||||
&& stake.deactivation_epoch == Epoch::MAX
|
||||
&& source.deactivation_epoch == Epoch::MAX
|
||||
{
|
||||
Ok(())
|
||||
} else {
|
||||
msg!("Unable to merge due to stake deactivation");
|
||||
Err(ProgramError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME copied from stake program
|
||||
/// Checks if two active stakes are mergeable, required since we cannot recover
|
||||
/// from a CPI error.
|
||||
pub fn active_stakes_can_merge(stake: &Stake, source: &Stake) -> Result<(), ProgramError> {
|
||||
active_delegations_can_merge(&stake.delegation, &source.delegation)?;
|
||||
|
||||
if stake.credits_observed == source.credits_observed {
|
||||
Ok(())
|
||||
} else {
|
||||
msg!("Unable to merge due to credits observed mismatch");
|
||||
Err(ProgramError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
/// FIXME copied from the stake program
|
||||
pub fn split_only(
|
||||
stake_pubkey: &Pubkey,
|
||||
|
@ -508,3 +644,64 @@ pub fn deactivate_stake(stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> In
|
|||
];
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::Deactivate, account_metas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {super::*, crate::borsh::try_from_slice_unchecked, bincode::serialize};
|
||||
|
||||
fn check_borsh_deserialization(stake: StakeState) {
|
||||
let serialized = serialize(&stake).unwrap();
|
||||
let deserialized = StakeState::try_from_slice(&serialized).unwrap();
|
||||
assert_eq!(stake, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bincode_vs_borsh() {
|
||||
check_borsh_deserialization(StakeState::Uninitialized);
|
||||
check_borsh_deserialization(StakeState::RewardsPool);
|
||||
check_borsh_deserialization(StakeState::Initialized(Meta {
|
||||
rent_exempt_reserve: u64::MAX,
|
||||
authorized: Authorized {
|
||||
staker: Pubkey::new_unique(),
|
||||
withdrawer: Pubkey::new_unique(),
|
||||
},
|
||||
lockup: Lockup::default(),
|
||||
}));
|
||||
check_borsh_deserialization(StakeState::Stake(
|
||||
Meta {
|
||||
rent_exempt_reserve: 1,
|
||||
authorized: Authorized {
|
||||
staker: Pubkey::new_unique(),
|
||||
withdrawer: Pubkey::new_unique(),
|
||||
},
|
||||
lockup: Lockup::default(),
|
||||
},
|
||||
Stake {
|
||||
delegation: Delegation {
|
||||
voter_pubkey: Pubkey::new_unique(),
|
||||
stake: u64::MAX,
|
||||
activation_epoch: Epoch::MAX,
|
||||
deactivation_epoch: Epoch::MAX,
|
||||
warmup_cooldown_rate: f64::MAX,
|
||||
},
|
||||
credits_observed: 1,
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn borsh_deserialization_live_data() {
|
||||
let data = [
|
||||
1, 0, 0, 0, 128, 213, 34, 0, 0, 0, 0, 0, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35,
|
||||
119, 124, 168, 12, 120, 216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149,
|
||||
224, 109, 52, 100, 133, 0, 79, 231, 141, 29, 73, 61, 232, 35, 119, 124, 168, 12, 120,
|
||||
216, 195, 29, 12, 166, 139, 28, 36, 182, 186, 154, 246, 149, 224, 109, 52, 100, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0,
|
||||
];
|
||||
let _deserialized = try_from_slice_unchecked::<StakeState>(&data).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! State transition types
|
||||
|
||||
use {
|
||||
crate::{error::StakePoolError, instruction::Fee},
|
||||
crate::error::StakePoolError,
|
||||
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
|
||||
solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey},
|
||||
solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey},
|
||||
spl_math::checked_ceil_div::CheckedCeilDiv,
|
||||
std::convert::TryFrom,
|
||||
};
|
||||
|
@ -31,66 +31,94 @@ impl Default for AccountType {
|
|||
pub struct StakePool {
|
||||
/// Account type, must be StakePool currently
|
||||
pub account_type: AccountType,
|
||||
/// Owner authority
|
||||
/// allows for updating the staking authority
|
||||
pub owner: Pubkey,
|
||||
/// Deposit authority bump seed
|
||||
/// for `create_program_address(&[state::StakePool account, "deposit"])`
|
||||
pub deposit_bump_seed: u8,
|
||||
|
||||
/// Manager authority, allows for updating the staker, manager, and fee account
|
||||
pub manager: Pubkey,
|
||||
|
||||
/// Staker authority, allows for adding and removing validators, and managing stake
|
||||
/// distribution
|
||||
pub staker: Pubkey,
|
||||
|
||||
/// Deposit authority
|
||||
///
|
||||
/// If a depositor pubkey is specified on initialization, then deposits must be
|
||||
/// signed by this authority. If no deposit authority is specified,
|
||||
/// then the stake pool will default to the result of:
|
||||
/// `Pubkey::find_program_address(
|
||||
/// &[&stake_pool_address.to_bytes()[..32], b"deposit"],
|
||||
/// program_id,
|
||||
/// )`
|
||||
pub deposit_authority: Pubkey,
|
||||
|
||||
/// Withdrawal authority bump seed
|
||||
/// for `create_program_address(&[state::StakePool account, "withdrawal"])`
|
||||
pub withdraw_bump_seed: u8,
|
||||
|
||||
/// Validator stake list storage account
|
||||
pub validator_list: Pubkey,
|
||||
|
||||
/// Reserve stake account, holds deactivated stake
|
||||
pub reserve_stake: Pubkey,
|
||||
|
||||
/// Pool Mint
|
||||
pub pool_mint: Pubkey,
|
||||
/// Owner fee account
|
||||
pub owner_fee_account: Pubkey,
|
||||
|
||||
/// Manager fee account
|
||||
pub manager_fee_account: Pubkey,
|
||||
|
||||
/// Pool token program id
|
||||
pub token_program_id: Pubkey,
|
||||
/// total stake under management
|
||||
pub stake_total: u64,
|
||||
/// total pool
|
||||
pub pool_total: u64,
|
||||
/// Last epoch stake_total field was updated
|
||||
|
||||
/// Total stake under management.
|
||||
/// Note that if `last_update_epoch` does not match the current epoch then
|
||||
/// this field may not be accurate
|
||||
pub total_stake_lamports: u64,
|
||||
|
||||
/// Total supply of pool tokens (should always match the supply in the Pool Mint)
|
||||
pub pool_token_supply: u64,
|
||||
|
||||
/// Last epoch the `total_stake_lamports` field was updated
|
||||
pub last_update_epoch: u64,
|
||||
|
||||
/// Fee applied to deposits
|
||||
pub fee: Fee,
|
||||
}
|
||||
impl StakePool {
|
||||
/// calculate the pool tokens that should be minted
|
||||
pub fn calc_pool_deposit_amount(&self, stake_lamports: u64) -> Option<u64> {
|
||||
if self.stake_total == 0 {
|
||||
/// calculate the pool tokens that should be minted for a deposit of `stake_lamports`
|
||||
pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
|
||||
if self.total_stake_lamports == 0 || self.pool_token_supply == 0 {
|
||||
return Some(stake_lamports);
|
||||
}
|
||||
u64::try_from(
|
||||
(stake_lamports as u128)
|
||||
.checked_mul(self.pool_total as u128)?
|
||||
.checked_div(self.stake_total as u128)?,
|
||||
.checked_mul(self.pool_token_supply as u128)?
|
||||
.checked_div(self.total_stake_lamports as u128)?,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
/// calculate the pool tokens that should be withdrawn
|
||||
pub fn calc_pool_withdraw_amount(&self, stake_lamports: u64) -> Option<u64> {
|
||||
/// calculate the pool tokens that should be burned for a withdrawal of `stake_lamports`
|
||||
pub fn calc_pool_tokens_for_withdraw(&self, stake_lamports: u64) -> Option<u64> {
|
||||
let (quotient, _) = (stake_lamports as u128)
|
||||
.checked_mul(self.pool_total as u128)?
|
||||
.checked_ceil_div(self.stake_total as u128)?;
|
||||
.checked_mul(self.pool_token_supply as u128)?
|
||||
.checked_ceil_div(self.total_stake_lamports as u128)?;
|
||||
u64::try_from(quotient).ok()
|
||||
}
|
||||
|
||||
/// calculate lamports amount on withdrawal
|
||||
pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option<u64> {
|
||||
u64::try_from(
|
||||
(pool_tokens as u128)
|
||||
.checked_mul(self.stake_total as u128)?
|
||||
.checked_div(self.pool_total as u128)?,
|
||||
.checked_mul(self.total_stake_lamports as u128)?
|
||||
.checked_div(self.pool_token_supply as u128)?,
|
||||
)
|
||||
.ok()
|
||||
}
|
||||
/// calculate the fee in pool tokens that goes to the owner
|
||||
pub fn calc_fee_amount(&self, pool_amount: u64) -> Option<u64> {
|
||||
/// calculate the fee in pool tokens that goes to the manager
|
||||
pub fn calc_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
|
||||
if self.fee.denominator == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
let pool_amount = self.calc_pool_tokens_for_deposit(reward_lamports)?;
|
||||
u64::try_from(
|
||||
(pool_amount as u128)
|
||||
.checked_mul(self.fee.numerator as u128)?
|
||||
|
@ -107,18 +135,23 @@ impl StakePool {
|
|||
authority_seed: &[u8],
|
||||
bump_seed: u8,
|
||||
) -> Result<(), ProgramError> {
|
||||
if *authority_address
|
||||
== Pubkey::create_program_address(
|
||||
&[
|
||||
&stake_pool_address.to_bytes()[..32],
|
||||
authority_seed,
|
||||
&[bump_seed],
|
||||
],
|
||||
program_id,
|
||||
)?
|
||||
{
|
||||
let expected_address = Pubkey::create_program_address(
|
||||
&[
|
||||
&stake_pool_address.to_bytes()[..32],
|
||||
authority_seed,
|
||||
&[bump_seed],
|
||||
],
|
||||
program_id,
|
||||
)?;
|
||||
|
||||
if *authority_address == expected_address {
|
||||
Ok(())
|
||||
} else {
|
||||
msg!(
|
||||
"Incorrect authority provided, expected {}, received {}",
|
||||
expected_address,
|
||||
authority_address
|
||||
);
|
||||
Err(StakePoolError::InvalidProgramAddress.into())
|
||||
}
|
||||
}
|
||||
|
@ -139,32 +172,94 @@ impl StakePool {
|
|||
)
|
||||
}
|
||||
/// Checks that the deposit authority is valid
|
||||
pub(crate) fn check_authority_deposit(
|
||||
pub(crate) fn check_deposit_authority(
|
||||
&self,
|
||||
deposit_authority: &Pubkey,
|
||||
program_id: &Pubkey,
|
||||
stake_pool_address: &Pubkey,
|
||||
) -> Result<(), ProgramError> {
|
||||
Self::check_authority(
|
||||
deposit_authority,
|
||||
program_id,
|
||||
stake_pool_address,
|
||||
crate::AUTHORITY_DEPOSIT,
|
||||
self.deposit_bump_seed,
|
||||
)
|
||||
if self.deposit_authority == *deposit_authority {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(StakePoolError::InvalidProgramAddress.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check owner validity and signature
|
||||
pub(crate) fn check_owner(&self, owner_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
if *owner_info.key != self.owner {
|
||||
return Err(StakePoolError::WrongOwner.into());
|
||||
/// Check staker validity and signature
|
||||
pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
if *mint_info.key != self.pool_mint {
|
||||
Err(StakePoolError::WrongPoolMint.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
if !owner_info.is_signer {
|
||||
}
|
||||
|
||||
/// Check manager validity and signature
|
||||
pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
if *manager_info.key != self.manager {
|
||||
msg!(
|
||||
"Incorrect manager provided, expected {}, received {}",
|
||||
self.manager,
|
||||
manager_info.key
|
||||
);
|
||||
return Err(StakePoolError::WrongManager.into());
|
||||
}
|
||||
if !manager_info.is_signer {
|
||||
msg!("Manager signature missing");
|
||||
return Err(StakePoolError::SignatureMissing.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check staker validity and signature
|
||||
pub(crate) fn check_staker(&self, staker_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||
if *staker_info.key != self.staker {
|
||||
msg!(
|
||||
"Incorrect staker provided, expected {}, received {}",
|
||||
self.staker,
|
||||
staker_info.key
|
||||
);
|
||||
return Err(StakePoolError::WrongStaker.into());
|
||||
}
|
||||
if !staker_info.is_signer {
|
||||
msg!("Staker signature missing");
|
||||
return Err(StakePoolError::SignatureMissing.into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check the validator list is valid
|
||||
pub fn check_validator_list(
|
||||
&self,
|
||||
validator_list_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if *validator_list_info.key != self.validator_list {
|
||||
msg!(
|
||||
"Invalid validator list provided, expected {}, received {}",
|
||||
self.validator_list,
|
||||
validator_list_info.key
|
||||
);
|
||||
Err(StakePoolError::InvalidValidatorStakeList.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check the validator list is valid
|
||||
pub fn check_reserve_stake(
|
||||
&self,
|
||||
reserve_stake_info: &AccountInfo,
|
||||
) -> Result<(), ProgramError> {
|
||||
if *reserve_stake_info.key != self.reserve_stake {
|
||||
msg!(
|
||||
"Invalid reserve stake provided, expected {}, received {}",
|
||||
self.reserve_stake,
|
||||
reserve_stake_info.key
|
||||
);
|
||||
Err(StakePoolError::InvalidProgramAddress.into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if StakePool is actually initialized as a stake pool
|
||||
pub fn is_valid(&self) -> bool {
|
||||
self.account_type == AccountType::StakePool
|
||||
|
@ -186,21 +281,45 @@ pub struct ValidatorList {
|
|||
/// Maximum allowable number of validators
|
||||
pub max_validators: u32,
|
||||
|
||||
/// List of all validator stake accounts and their info
|
||||
/// List of stake info for each validator in the pool
|
||||
pub validators: Vec<ValidatorStakeInfo>,
|
||||
}
|
||||
|
||||
/// Status of the stake account in the validator list, for accounting
|
||||
#[derive(Copy, Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub enum StakeStatus {
|
||||
/// Stake account is active, there may be a transient stake as well
|
||||
Active,
|
||||
/// Only transient stake account exists, when a transient stake is
|
||||
/// deactivating during validator removal
|
||||
DeactivatingTransient,
|
||||
/// No more validator stake accounts exist, entry ready for removal during
|
||||
/// `UpdateStakePoolBalance`
|
||||
ReadyForRemoval,
|
||||
}
|
||||
|
||||
impl Default for StakeStatus {
|
||||
fn default() -> Self {
|
||||
Self::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about the singe validator stake account
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||
pub struct ValidatorStakeInfo {
|
||||
/// Status of the validator stake account
|
||||
pub status: StakeStatus,
|
||||
|
||||
/// Validator vote account address
|
||||
pub vote_account: Pubkey,
|
||||
pub vote_account_address: Pubkey,
|
||||
|
||||
/// Balance of the validator's stake account
|
||||
pub balance: u64,
|
||||
/// Amount of stake delegated to this validator
|
||||
/// Note that if `last_update_epoch` does not match the current epoch then this field may not
|
||||
/// be accurate
|
||||
pub stake_lamports: u64,
|
||||
|
||||
/// Last epoch balance field was updated
|
||||
/// Last epoch the `stake_lamports` field was updated
|
||||
pub last_update_epoch: u64,
|
||||
}
|
||||
|
||||
|
@ -217,27 +336,27 @@ impl ValidatorList {
|
|||
/// Calculate the number of validator entries that fit in the provided length
|
||||
pub fn calculate_max_validators(buffer_length: usize) -> usize {
|
||||
let header_size = 1 + 4 + 4;
|
||||
buffer_length.saturating_sub(header_size) / 48
|
||||
buffer_length.saturating_sub(header_size) / 49
|
||||
}
|
||||
|
||||
/// Check if contains validator with particular pubkey
|
||||
pub fn contains(&self, vote_account: &Pubkey) -> bool {
|
||||
pub fn contains(&self, vote_account_address: &Pubkey) -> bool {
|
||||
self.validators
|
||||
.iter()
|
||||
.any(|x| x.vote_account == *vote_account)
|
||||
.any(|x| x.vote_account_address == *vote_account_address)
|
||||
}
|
||||
|
||||
/// Check if contains validator with particular pubkey
|
||||
pub fn find_mut(&mut self, vote_account: &Pubkey) -> Option<&mut ValidatorStakeInfo> {
|
||||
pub fn find_mut(&mut self, vote_account_address: &Pubkey) -> Option<&mut ValidatorStakeInfo> {
|
||||
self.validators
|
||||
.iter_mut()
|
||||
.find(|x| x.vote_account == *vote_account)
|
||||
.find(|x| x.vote_account_address == *vote_account_address)
|
||||
}
|
||||
/// Check if contains validator with particular pubkey
|
||||
pub fn find(&self, vote_account: &Pubkey) -> Option<&ValidatorStakeInfo> {
|
||||
pub fn find(&self, vote_account_address: &Pubkey) -> Option<&ValidatorStakeInfo> {
|
||||
self.validators
|
||||
.iter()
|
||||
.find(|x| x.vote_account == *vote_account)
|
||||
.find(|x| x.vote_account_address == *vote_account_address)
|
||||
}
|
||||
|
||||
/// Check if validator stake list is actually initialized as a validator stake list
|
||||
|
@ -251,6 +370,17 @@ impl ValidatorList {
|
|||
}
|
||||
}
|
||||
|
||||
/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
|
||||
/// the rewards
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
|
||||
pub struct Fee {
|
||||
/// denominator of the fee ratio
|
||||
pub denominator: u64,
|
||||
/// numerator of the fee ratio
|
||||
pub numerator: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use {
|
||||
|
@ -294,18 +424,21 @@ mod test {
|
|||
max_validators,
|
||||
validators: vec![
|
||||
ValidatorStakeInfo {
|
||||
vote_account: Pubkey::new_from_array([1; 32]),
|
||||
balance: 123456789,
|
||||
status: StakeStatus::Active,
|
||||
vote_account_address: Pubkey::new_from_array([1; 32]),
|
||||
stake_lamports: 123456789,
|
||||
last_update_epoch: 987654321,
|
||||
},
|
||||
ValidatorStakeInfo {
|
||||
vote_account: Pubkey::new_from_array([2; 32]),
|
||||
balance: 998877665544,
|
||||
status: StakeStatus::DeactivatingTransient,
|
||||
vote_account_address: Pubkey::new_from_array([2; 32]),
|
||||
stake_lamports: 998877665544,
|
||||
last_update_epoch: 11223445566,
|
||||
},
|
||||
ValidatorStakeInfo {
|
||||
vote_account: Pubkey::new_from_array([3; 32]),
|
||||
balance: 0,
|
||||
status: StakeStatus::ReadyForRemoval,
|
||||
vote_account_address: Pubkey::new_from_array([3; 32]),
|
||||
stake_lamports: 0,
|
||||
last_update_epoch: 999999999999999,
|
||||
},
|
||||
],
|
||||
|
|
|
@ -0,0 +1,386 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use {
|
||||
bincode::deserialize,
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey,
|
||||
system_instruction::SystemError,
|
||||
},
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transaction::{Transaction, TransactionError},
|
||||
},
|
||||
spl_stake_pool::{error::StakePoolError, id, instruction, stake_program},
|
||||
};
|
||||
|
||||
async fn setup() -> (
|
||||
BanksClient,
|
||||
Keypair,
|
||||
Hash,
|
||||
StakePoolAccounts,
|
||||
ValidatorStakeAccount,
|
||||
DepositStakeAccount,
|
||||
u64,
|
||||
) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
100_000_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let lamports = deposit_info.stake_lamports / 2;
|
||||
|
||||
(
|
||||
banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
lamports,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
_deposit_info,
|
||||
decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
// Save validator stake
|
||||
let pre_validator_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake.stake_account).await;
|
||||
|
||||
// Check no transient stake
|
||||
let transient_account = banks_client
|
||||
.get_account(validator_stake.transient_stake_account)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(transient_account.is_none());
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
decrease_lamports,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check validator stake account balance
|
||||
let validator_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake.stake_account).await;
|
||||
let validator_stake_state =
|
||||
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
|
||||
assert_eq!(
|
||||
pre_validator_stake_account.lamports - decrease_lamports,
|
||||
validator_stake_account.lamports
|
||||
);
|
||||
assert_eq!(
|
||||
validator_stake_state
|
||||
.delegation()
|
||||
.unwrap()
|
||||
.deactivation_epoch,
|
||||
Epoch::MAX
|
||||
);
|
||||
|
||||
// Check transient stake account state and balance
|
||||
let transient_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake.transient_stake_account).await;
|
||||
let transient_stake_state =
|
||||
deserialize::<stake_program::StakeState>(&transient_stake_account.data).unwrap();
|
||||
assert_eq!(transient_stake_account.lamports, decrease_lamports);
|
||||
assert_ne!(
|
||||
transient_stake_state
|
||||
.delegation()
|
||||
.unwrap()
|
||||
.deactivation_epoch,
|
||||
Epoch::MAX
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_withdraw_authority() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
_deposit_info,
|
||||
decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_authority = Pubkey::new_unique();
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::decrease_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&wrong_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
decrease_lamports,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::InvalidProgramAddress as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while decreasing with wrong withdraw authority"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_validator_list() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
mut stake_pool_accounts,
|
||||
validator_stake,
|
||||
_deposit_info,
|
||||
decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
stake_pool_accounts.validator_list = Keypair::new();
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::decrease_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
decrease_lamports,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while decreasing with wrong validator stake list account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_unknown_validator() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
_validator_stake,
|
||||
_deposit_info,
|
||||
decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let unknown_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
unknown_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::decrease_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&unknown_stake.stake_account,
|
||||
&unknown_stake.transient_stake_account,
|
||||
decrease_lamports,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::ValidatorNotFound as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while decreasing stake from unknown validator"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_decrease_twice() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
_deposit_info,
|
||||
decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
decrease_lamports / 3,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
decrease_lamports / 2,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = SystemError::AccountAlreadyInUse as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_small_lamport_amount() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
_deposit_info,
|
||||
_decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::InvalidError) => {}
|
||||
_ => panic!("Wrong error occurs while try to decrease small stake"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_overdraw_validator() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
deposit_info,
|
||||
_decrease_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
deposit_info.stake_lamports * 1_000_000,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {}
|
||||
_ => panic!("Wrong error occurs while overdrawing stake account on decrease"),
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
mod helpers;
|
||||
|
||||
use {
|
||||
bincode::deserialize,
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
|
@ -19,7 +20,8 @@ use {
|
|||
transport::TransportError,
|
||||
},
|
||||
spl_stake_pool::{
|
||||
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state,
|
||||
borsh::try_from_slice_unchecked, error, id, instruction, minimum_stake_lamports,
|
||||
stake_program, state,
|
||||
},
|
||||
spl_token::error as token_error,
|
||||
};
|
||||
|
@ -34,7 +36,7 @@ async fn setup() -> (
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -78,6 +80,7 @@ async fn test_stake_pool_deposit() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -85,6 +88,7 @@ async fn test_stake_pool_deposit() {
|
|||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake_account.validator,
|
||||
&validator_stake_account.vote,
|
||||
)
|
||||
.await;
|
||||
|
@ -98,28 +102,6 @@ async fn test_stake_pool_deposit() {
|
|||
)
|
||||
.await;
|
||||
|
||||
// Change authority to the stake pool's deposit
|
||||
authorize_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&stake_authority,
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
stake_program::StakeAuthorize::Withdrawer,
|
||||
)
|
||||
.await;
|
||||
authorize_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&stake_authority,
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
stake_program::StakeAuthorize::Staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
|
@ -159,6 +141,7 @@ async fn test_stake_pool_deposit() {
|
|||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&stake_authority,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -171,30 +154,23 @@ async fn test_stake_pool_deposit() {
|
|||
.is_none());
|
||||
|
||||
let tokens_issued = stake_lamports; // For now tokens are 1:1 to stake
|
||||
let fee = stake_pool_accounts.calculate_fee(tokens_issued);
|
||||
|
||||
// Stake pool should add its balance to the pool balance
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
stake_pool.stake_total,
|
||||
stake_pool_before.stake_total + stake_lamports
|
||||
stake_pool.total_stake_lamports,
|
||||
stake_pool_before.total_stake_lamports + stake_lamports
|
||||
);
|
||||
assert_eq!(
|
||||
stake_pool.pool_total,
|
||||
stake_pool_before.pool_total + tokens_issued
|
||||
stake_pool.pool_token_supply,
|
||||
stake_pool_before.pool_token_supply + tokens_issued
|
||||
);
|
||||
|
||||
// Check minted tokens
|
||||
let user_token_balance =
|
||||
get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
|
||||
assert_eq!(user_token_balance, tokens_issued - fee);
|
||||
let pool_fee_token_balance = get_token_balance(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pool_fee_token_balance, fee);
|
||||
assert_eq!(user_token_balance, tokens_issued);
|
||||
|
||||
// Check balances in validator stake account list storage
|
||||
let validator_list = get_account(
|
||||
|
@ -208,16 +184,19 @@ async fn test_stake_pool_deposit() {
|
|||
.find(&validator_stake_account.vote.pubkey())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
validator_stake_item.balance,
|
||||
validator_stake_item_before.balance + stake_lamports
|
||||
validator_stake_item.stake_lamports,
|
||||
validator_stake_item_before.stake_lamports + stake_lamports
|
||||
);
|
||||
|
||||
// Check validator stake account actual SOL balance
|
||||
let validator_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake_account.stake_account).await;
|
||||
let stake_state =
|
||||
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
|
||||
let meta = stake_state.meta().unwrap();
|
||||
assert_eq!(
|
||||
validator_stake_account.lamports,
|
||||
validator_stake_item.balance
|
||||
validator_stake_account.lamports - minimum_stake_lamports(&meta),
|
||||
validator_stake_item.stake_lamports
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -253,7 +232,6 @@ async fn test_stake_pool_deposit_with_wrong_stake_program_id() {
|
|||
AccountMeta::new(user_stake.pubkey(), false),
|
||||
AccountMeta::new(validator_stake_account.stake_account, false),
|
||||
AccountMeta::new(user_pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
|
@ -284,75 +262,6 @@ async fn test_stake_pool_deposit_with_wrong_stake_program_id() {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_pool_fee_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
mut stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
) = setup().await;
|
||||
|
||||
let user = Keypair::new();
|
||||
// make stake account
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let wrong_pool_fee_acc = Keypair::new();
|
||||
stake_pool_accounts.pool_fee_account = wrong_pool_fee_acc;
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.deposit_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidFeeAccount as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to make a deposit with wrong pool fee account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_token_program_id() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
|
||||
|
@ -363,8 +272,8 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
|
|||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
|
@ -373,6 +282,7 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -392,23 +302,21 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() {
|
|||
let wrong_token_program = Keypair::new();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::deposit(
|
||||
&instruction::deposit(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&user_stake.pubkey(),
|
||||
&user.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&wrong_token_program.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
),
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
transaction.sign(&[&payer, &user], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -438,8 +346,8 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
|
|||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
|
@ -448,6 +356,7 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -475,6 +384,7 @@ async fn test_stake_pool_deposit_with_wrong_validator_list_account() {
|
|||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
|
@ -497,20 +407,18 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let validator_stake_account =
|
||||
ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
validator_stake_account
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -531,8 +439,8 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
|
|||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
|
@ -541,6 +449,7 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -552,6 +461,7 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
|
|||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
|
@ -571,74 +481,6 @@ async fn test_stake_pool_deposit_to_unknown_validator() {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_deposit_authority() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
mut stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
) = setup().await;
|
||||
|
||||
let user = Keypair::new();
|
||||
// make stake account
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
stake_pool_accounts.deposit_authority = Keypair::new().pubkey();
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.deposit_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidProgramAddress as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to make a deposit with wrong deposit authority"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
|
||||
let (
|
||||
|
@ -654,8 +496,8 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
|
|||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
|
@ -664,6 +506,7 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -690,6 +533,7 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
|
|||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
|
@ -707,75 +551,18 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() {
|
|||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_set_deposit_authority() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
|
||||
setup().await;
|
||||
|
||||
let user = Keypair::new();
|
||||
// make stake account
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: Keypair::new().pubkey(),
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
)
|
||||
.await;
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.deposit_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
|
||||
assert_eq!(error, InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
_ => {
|
||||
panic!("Wrong error occurs while try to make deposit with wrong set deposit authority")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake_account) =
|
||||
setup().await;
|
||||
|
||||
// make stake account
|
||||
let user = Keypair::new();
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: stake_pool_accounts.deposit_authority,
|
||||
withdrawer: stake_pool_accounts.deposit_authority,
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
|
@ -784,12 +571,13 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
let outside_mint = Keypair::new();
|
||||
let outside_withdraw_auth = Keypair::new();
|
||||
let outside_owner = Keypair::new();
|
||||
let outside_manager = Keypair::new();
|
||||
let outside_pool_fee_acc = Keypair::new();
|
||||
|
||||
create_mint(
|
||||
|
@ -808,7 +596,7 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
|
|||
&recent_blockhash,
|
||||
&outside_pool_fee_acc,
|
||||
&outside_mint.pubkey(),
|
||||
&outside_owner.pubkey(),
|
||||
&outside_manager.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -821,6 +609,7 @@ async fn test_stake_pool_deposit_with_wrong_mint_for_receiver_acc() {
|
|||
&user_stake.pubkey(),
|
||||
&outside_pool_fee_acc.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
|
@ -843,3 +632,180 @@ async fn test_deposit_with_uninitialized_validator_list() {} // TODO
|
|||
|
||||
#[tokio::test]
|
||||
async fn test_deposit_with_out_of_dated_pool_balances() {} // TODO
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_with_deposit_authority() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let deposit_authority = Keypair::new();
|
||||
let stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let user = Keypair::new();
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
let _stake_lamports = create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
create_vote(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake_account.validator,
|
||||
&validator_stake_account.vote,
|
||||
)
|
||||
.await;
|
||||
delegate_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user,
|
||||
&validator_stake_account.vote.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
stake_pool_accounts
|
||||
.deposit_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_without_deposit_authority_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let deposit_authority = Keypair::new();
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let user = Keypair::new();
|
||||
let user_stake = Keypair::new();
|
||||
let lockup = stake_program::Lockup::default();
|
||||
let authorized = stake_program::Authorized {
|
||||
staker: user.pubkey(),
|
||||
withdrawer: user.pubkey(),
|
||||
};
|
||||
let _stake_lamports = create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
|
||||
create_vote(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake_account.validator,
|
||||
&validator_stake_account.vote,
|
||||
)
|
||||
.await;
|
||||
delegate_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user,
|
||||
&validator_stake_account.vote.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let wrong_depositor = Keypair::new();
|
||||
stake_pool_accounts.deposit_authority = wrong_depositor.pubkey();
|
||||
stake_pool_accounts.deposit_authority_keypair = Some(wrong_depositor);
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.deposit_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.pubkey(),
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&user,
|
||||
)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
assert_eq!(
|
||||
error_index,
|
||||
error::StakePoolError::InvalidProgramAddress as u32
|
||||
);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"),
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,384 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use {
|
||||
bincode::deserialize,
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey,
|
||||
system_instruction::SystemError,
|
||||
},
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
signature::{Keypair, Signer},
|
||||
transaction::{Transaction, TransactionError},
|
||||
},
|
||||
spl_stake_pool::{error::StakePoolError, id, instruction, stake_program},
|
||||
};
|
||||
|
||||
async fn setup() -> (
|
||||
BanksClient,
|
||||
Keypair,
|
||||
Hash,
|
||||
StakePoolAccounts,
|
||||
ValidatorStakeAccount,
|
||||
u64,
|
||||
) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
let reserve_lamports = 100_000_000_000;
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
reserve_lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let _deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
5_000_000,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(
|
||||
banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
reserve_lamports,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
// Save reserve stake
|
||||
let pre_reserve_stake_account = get_account(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Check no transient stake
|
||||
let transient_account = banks_client
|
||||
.get_account(validator_stake.transient_stake_account)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(transient_account.is_none());
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
let reserve_lamports = reserve_lamports - lamports;
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check reserve stake account balance
|
||||
let reserve_stake_account = get_account(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let reserve_stake_state =
|
||||
deserialize::<stake_program::StakeState>(&reserve_stake_account.data).unwrap();
|
||||
assert_eq!(
|
||||
pre_reserve_stake_account.lamports - reserve_lamports,
|
||||
reserve_stake_account.lamports
|
||||
);
|
||||
assert!(reserve_stake_state.delegation().is_none());
|
||||
|
||||
// Check transient stake account state and balance
|
||||
let transient_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake.transient_stake_account).await;
|
||||
let transient_stake_state =
|
||||
deserialize::<stake_program::StakeState>(&transient_stake_account.data).unwrap();
|
||||
assert_eq!(transient_stake_account.lamports, reserve_lamports);
|
||||
assert_ne!(
|
||||
transient_stake_state.delegation().unwrap().activation_epoch,
|
||||
Epoch::MAX
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_withdraw_authority() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_authority = Pubkey::new_unique();
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::increase_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&wrong_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports / 2,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::InvalidProgramAddress as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_validator_list() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_validator_list = Pubkey::new_unique();
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::increase_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&wrong_validator_list,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports / 2,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_unknown_validator() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
_validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let unknown_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
unknown_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::increase_validator_stake(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&unknown_stake.transient_stake_account,
|
||||
&unknown_stake.vote.pubkey(),
|
||||
reserve_lamports / 2,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::ValidatorNotFound as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_increase_twice() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports / 3,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports / 4,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = SystemError::AccountAlreadyInUse as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_small_lamport_amount() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
_reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::InvalidError) => {}
|
||||
_ => panic!("Wrong error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_overdraw_reserve() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
validator_stake,
|
||||
reserve_lamports,
|
||||
) = setup().await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
reserve_lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::InsufficientFunds) => {}
|
||||
_ => panic!("Wrong error occurs while overdrawing reserve stake"),
|
||||
}
|
||||
}
|
|
@ -3,13 +3,14 @@
|
|||
mod helpers;
|
||||
|
||||
use {
|
||||
borsh::BorshSerialize,
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
borsh::get_packed_len,
|
||||
hash::Hash,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
program_pack::Pack,
|
||||
pubkey::Pubkey,
|
||||
system_instruction, sysvar,
|
||||
},
|
||||
solana_program_test::*,
|
||||
|
@ -19,11 +20,11 @@ use {
|
|||
},
|
||||
spl_stake_pool::{
|
||||
borsh::{get_instance_packed_len, try_from_slice_unchecked},
|
||||
error, id, instruction, state,
|
||||
error, id, instruction, stake_program, state,
|
||||
},
|
||||
};
|
||||
|
||||
async fn create_mint_and_token_account(
|
||||
async fn create_required_accounts(
|
||||
banks_client: &mut BanksClient,
|
||||
payer: &Keypair,
|
||||
recent_blockhash: &Hash,
|
||||
|
@ -45,18 +46,32 @@ async fn create_mint_and_token_account(
|
|||
recent_blockhash,
|
||||
&stake_pool_accounts.pool_fee_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
create_independent_stake_account(
|
||||
banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.reserve_stake,
|
||||
&stake_program::Authorized {
|
||||
staker: stake_pool_accounts.withdraw_authority,
|
||||
withdrawer: stake_pool_accounts.withdraw_authority,
|
||||
},
|
||||
&stake_program::Lockup::default(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_initialize() {
|
||||
async fn success() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -77,11 +92,11 @@ async fn test_stake_pool_initialize() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_already_initialized_stake_pool() {
|
||||
async fn fail_double_initialize() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -91,7 +106,7 @@ async fn test_initialize_already_initialized_stake_pool() {
|
|||
second_stake_pool_accounts.stake_pool = stake_pool_accounts.stake_pool;
|
||||
|
||||
let transaction_error = second_stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash, 1)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
@ -108,11 +123,11 @@ async fn test_initialize_already_initialized_stake_pool() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage() {
|
||||
async fn fail_with_already_initialized_validator_list() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -122,7 +137,7 @@ async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage(
|
|||
second_stake_pool_accounts.validator_list = stake_pool_accounts.validator_list;
|
||||
|
||||
let transaction_error = second_stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &latest_blockhash, 1)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
@ -139,16 +154,16 @@ async fn test_initialize_stake_pool_with_already_initialized_stake_list_storage(
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_high_fee() {
|
||||
async fn fail_with_high_fee() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts.fee = instruction::Fee {
|
||||
stake_pool_accounts.fee = state::Fee {
|
||||
numerator: 100001,
|
||||
denominator: 100000,
|
||||
};
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
@ -165,11 +180,11 @@ async fn test_initialize_stake_pool_with_high_fee() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_wrong_max_validators() {
|
||||
async fn fail_with_wrong_max_validators() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
create_mint_and_token_account(
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -204,12 +219,15 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
|
|||
instruction::initialize(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&spl_token::id(),
|
||||
stake_pool_accounts.fee.clone(),
|
||||
None,
|
||||
stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -221,7 +239,7 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
|
|||
&payer,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
@ -244,12 +262,12 @@ async fn test_initialize_stake_pool_with_wrong_max_validators() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_wrong_mint_authority() {
|
||||
async fn fail_with_wrong_mint_authority() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
let wrong_mint = Keypair::new();
|
||||
|
||||
create_mint_and_token_account(
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -274,9 +292,12 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() {
|
|||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&wrong_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
|
@ -297,7 +318,7 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_wrong_token_program_id() {
|
||||
async fn fail_with_wrong_token_program_id() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
|
@ -358,12 +379,15 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
|
|||
instruction::initialize(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&wrong_token_program.pubkey(),
|
||||
stake_pool_accounts.fee.clone(),
|
||||
None,
|
||||
stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -375,7 +399,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
|
|||
&payer,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
@ -396,7 +420,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() {
|
||||
async fn fail_with_wrong_fee_account() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
|
@ -434,39 +458,35 @@ async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() {
|
|||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidFeeAccount as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!(
|
||||
"Wrong error occurs while try to initialize stake pool with wrong fee account's owner"
|
||||
),
|
||||
}
|
||||
assert_eq!(
|
||||
transaction_error,
|
||||
TransactionError::InstructionError(2, InstructionError::IncorrectProgramId)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_wrong_withdraw_authority() {
|
||||
async fn fail_with_wrong_withdraw_authority() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
stake_pool_accounts.withdraw_authority = Keypair::new().pubkey();
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
@ -486,11 +506,11 @@ async fn test_initialize_stake_pool_with_wrong_withdraw_authority() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
|
||||
async fn fail_with_not_rent_exempt_pool() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
create_mint_and_token_account(
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -524,12 +544,15 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
|
|||
instruction::initialize(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&spl_token::id(),
|
||||
stake_pool_accounts.fee.clone(),
|
||||
None,
|
||||
stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -541,7 +564,7 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
|
|||
&payer,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
@ -558,11 +581,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
|
||||
async fn fail_with_not_rent_exempt_validator_list() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
create_mint_and_token_account(
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -596,12 +619,15 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
|
|||
instruction::initialize(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&spl_token::id(),
|
||||
stake_pool_accounts.fee.clone(),
|
||||
None,
|
||||
stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.unwrap(),
|
||||
|
@ -613,7 +639,7 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
|
|||
&payer,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.manager,
|
||||
],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
@ -632,11 +658,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_list() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_initialize_stake_pool_without_owner_signature() {
|
||||
async fn fail_without_manager_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
|
||||
create_mint_and_token_account(
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -653,14 +679,16 @@ async fn test_initialize_stake_pool_without_owner_signature() {
|
|||
let rent_validator_list = rent.minimum_balance(validator_list_size);
|
||||
|
||||
let init_data = instruction::StakePoolInstruction::Initialize {
|
||||
fee: stake_pool_accounts.fee.clone(),
|
||||
fee: stake_pool_accounts.fee,
|
||||
max_validators: stake_pool_accounts.max_validators,
|
||||
};
|
||||
let data = init_data.try_to_vec().unwrap();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.reserve_stake.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.pool_fee_account.pubkey(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
|
@ -716,7 +744,298 @@ async fn test_initialize_stake_pool_without_owner_signature() {
|
|||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!(
|
||||
"Wrong error occurs while try to initialize stake pool without owner's signature"
|
||||
"Wrong error occurs while try to initialize stake pool without manager's signature"
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_pre_minted_pool_tokens() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
let mint_authority = Keypair::new();
|
||||
|
||||
create_mint(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.pool_mint,
|
||||
&mint_authority.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.pool_fee_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
mint_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&mint_authority,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let transaction_error = create_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::NonZeroPoolTokenSupply as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to initialize stake pool with wrong mint authority of pool fee account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_bad_reserve() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
let wrong_authority = Pubkey::new_unique();
|
||||
|
||||
create_required_accounts(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
{
|
||||
let bad_stake = Keypair::new();
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&bad_stake,
|
||||
&stake_program::Authorized {
|
||||
staker: wrong_authority,
|
||||
withdrawer: stake_pool_accounts.withdraw_authority,
|
||||
},
|
||||
&stake_program::Lockup::default(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = create_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&bad_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
2,
|
||||
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let bad_stake = Keypair::new();
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&bad_stake,
|
||||
&stake_program::Authorized {
|
||||
staker: stake_pool_accounts.withdraw_authority,
|
||||
withdrawer: wrong_authority,
|
||||
},
|
||||
&stake_program::Lockup::default(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = create_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&bad_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
2,
|
||||
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let bad_stake = Keypair::new();
|
||||
create_independent_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&bad_stake,
|
||||
&stake_program::Authorized {
|
||||
staker: stake_pool_accounts.withdraw_authority,
|
||||
withdrawer: stake_pool_accounts.withdraw_authority,
|
||||
},
|
||||
&stake_program::Lockup {
|
||||
custodian: wrong_authority,
|
||||
..stake_program::Lockup::default()
|
||||
},
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = create_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&bad_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
2,
|
||||
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
let bad_stake = Keypair::new();
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let lamports = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[system_instruction::create_account(
|
||||
&payer.pubkey(),
|
||||
&bad_stake.pubkey(),
|
||||
lamports,
|
||||
std::mem::size_of::<stake_program::StakeState>() as u64,
|
||||
&stake_program::id(),
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &bad_stake],
|
||||
recent_blockhash,
|
||||
);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let error = create_stake_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.stake_pool,
|
||||
&stake_pool_accounts.validator_list,
|
||||
&bad_stake.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
&stake_pool_accounts.manager,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&None,
|
||||
&stake_pool_accounts.fee,
|
||||
stake_pool_accounts.max_validators,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
2,
|
||||
InstructionError::Custom(error::StakePoolError::WrongStakeState as u32),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_with_required_deposit_authority() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let deposit_authority = Keypair::new();
|
||||
let stake_pool_accounts = StakePoolAccounts::new_with_deposit_authority(deposit_authority);
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Stake pool now exists
|
||||
let stake_pool_account =
|
||||
get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(stake_pool_account.data.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
stake_pool.deposit_authority,
|
||||
stake_pool_accounts.deposit_authority
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use {
|
||||
borsh::BorshDeserialize,
|
||||
helpers::*,
|
||||
solana_program::hash::Hash,
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
instruction::InstructionError, signature::Keypair, signature::Signer,
|
||||
transaction::Transaction, transaction::TransactionError,
|
||||
},
|
||||
spl_stake_pool::{
|
||||
error, id, instruction,
|
||||
state::{Fee, StakePool},
|
||||
},
|
||||
};
|
||||
|
||||
async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Fee) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_fee = Fee {
|
||||
numerator: 10,
|
||||
denominator: 10,
|
||||
};
|
||||
|
||||
(
|
||||
banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
new_fee,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_fee) = setup().await;
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::set_fee(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
new_fee,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.manager],
|
||||
recent_blockhash,
|
||||
);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(stake_pool.fee, new_fee);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_wrong_manager() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_fee) = setup().await;
|
||||
|
||||
let wrong_manager = Keypair::new();
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::set_fee(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&wrong_manager.pubkey(),
|
||||
new_fee,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &wrong_manager],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = error::StakePoolError::WrongManager as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to set manager"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_bad_fee() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, _new_fee) = setup().await;
|
||||
|
||||
let new_fee = Fee {
|
||||
numerator: 11,
|
||||
denominator: 10,
|
||||
};
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::set_fee(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
new_fee,
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.manager],
|
||||
recent_blockhash,
|
||||
);
|
||||
let error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = error::StakePoolError::FeeTooHigh as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to set manager"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_not_updated() {
|
||||
let mut context = program_test().start_with_context().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let new_fee = Fee {
|
||||
numerator: 10,
|
||||
denominator: 100,
|
||||
};
|
||||
|
||||
// move forward so an update is required
|
||||
context.warp_to_slot(50_000).unwrap();
|
||||
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::set_fee(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
new_fee,
|
||||
)],
|
||||
Some(&context.payer.pubkey()),
|
||||
&[&context.payer, &stake_pool_accounts.manager],
|
||||
context.last_blockhash,
|
||||
);
|
||||
let error = context
|
||||
.banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = error::StakePoolError::StakeListAndPoolOutOfDate as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to set manager"),
|
||||
}
|
||||
}
|
|
@ -28,19 +28,19 @@ async fn setup() -> (
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_pool_fee = Keypair::new();
|
||||
let new_owner = Keypair::new();
|
||||
let new_manager = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&new_pool_fee,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -51,52 +51,52 @@ async fn setup() -> (
|
|||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
new_pool_fee,
|
||||
new_owner,
|
||||
new_manager,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_owner() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) =
|
||||
async fn test_set_manager() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
|
||||
setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_owner(
|
||||
&[instruction::set_manager(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
&new_pool_fee.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(stake_pool.owner, new_owner.pubkey());
|
||||
assert_eq!(stake_pool.manager, new_manager.pubkey());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_owner_by_malicious() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) =
|
||||
async fn test_set_manager_by_malicious() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
|
||||
setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_owner(
|
||||
&[instruction::set_manager(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
&new_pool_fee.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &new_owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &new_manager], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -108,25 +108,25 @@ async fn test_set_owner_by_malicious() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::WrongOwner as u32;
|
||||
let program_error = error::StakePoolError::WrongManager as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to set owner"),
|
||||
_ => panic!("Wrong error occurs while malicious try to set manager"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_owner_without_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) =
|
||||
async fn test_set_manager_without_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_manager) =
|
||||
setup().await;
|
||||
|
||||
let data = instruction::StakePoolInstruction::SetOwner
|
||||
let data = instruction::StakePoolInstruction::SetManager
|
||||
.try_to_vec()
|
||||
.unwrap();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),
|
||||
AccountMeta::new_readonly(new_owner.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
|
||||
AccountMeta::new_readonly(new_manager.pubkey(), false),
|
||||
AccountMeta::new_readonly(new_pool_fee.pubkey(), false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
|
@ -151,23 +151,23 @@ async fn test_set_owner_without_signature() {
|
|||
let program_error = error::StakePoolError::SignatureMissing as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to set new owner without signature"),
|
||||
_ => panic!("Wrong error occurs while try to set new manager without signature"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() {
|
||||
async fn test_set_manager_with_wrong_mint_for_pool_fee_acc() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_mint = Keypair::new();
|
||||
let new_withdraw_auth = Keypair::new();
|
||||
let new_pool_fee = Keypair::new();
|
||||
let new_owner = Keypair::new();
|
||||
let new_manager = Keypair::new();
|
||||
|
||||
create_mint(
|
||||
&mut banks_client,
|
||||
|
@ -184,23 +184,23 @@ async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() {
|
|||
&recent_blockhash,
|
||||
&new_pool_fee,
|
||||
&new_mint.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_owner(
|
||||
&[instruction::set_manager(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&new_owner.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&new_manager.pubkey(),
|
||||
&new_pool_fee.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -215,6 +215,6 @@ async fn test_set_owner_with_wrong_mint_for_pool_fee_acc() {
|
|||
let program_error = error::StakePoolError::WrongAccountMint as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to set new owner with wrong mint"),
|
||||
_ => panic!("Wrong error occurs while try to set new manager with wrong mint"),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,176 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use {
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
hash::Hash,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
},
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
instruction::InstructionError, signature::Keypair, signature::Signer,
|
||||
transaction::Transaction, transaction::TransactionError, transport::TransportError,
|
||||
},
|
||||
spl_stake_pool::{error, id, instruction, state},
|
||||
};
|
||||
|
||||
async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, Keypair) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_staker = Keypair::new();
|
||||
|
||||
(
|
||||
banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
new_staker,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_set_staker_as_manager() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
|
||||
setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_staker(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.manager.pubkey(),
|
||||
&new_staker.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.manager], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(stake_pool.staker, new_staker.pubkey());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_set_staker_as_staker() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
|
||||
setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_staker(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&new_staker.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(stake_pool.staker, new_staker.pubkey());
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_staker(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&new_staker.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &new_staker], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
|
||||
assert_eq!(stake_pool.staker, stake_pool_accounts.staker.pubkey());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_wrong_manager() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
|
||||
setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::set_staker(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&new_staker.pubkey(),
|
||||
&new_staker.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &new_staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::SignatureMissing as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to set manager"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_set_staker_without_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_staker) =
|
||||
setup().await;
|
||||
|
||||
let data = instruction::StakePoolInstruction::SetStaker
|
||||
.try_to_vec()
|
||||
.unwrap();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.manager.pubkey(), false),
|
||||
AccountMeta::new_readonly(new_staker.pubkey(), false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
program_id: id(),
|
||||
accounts,
|
||||
data,
|
||||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::SignatureMissing as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to set new manager without signature"),
|
||||
}
|
||||
}
|
|
@ -3,60 +3,219 @@
|
|||
mod helpers;
|
||||
|
||||
use {
|
||||
borsh::BorshDeserialize,
|
||||
helpers::*,
|
||||
solana_program::{instruction::InstructionError, pubkey::Pubkey},
|
||||
solana_program_test::*,
|
||||
solana_sdk::{
|
||||
instruction::InstructionError, signature::Keypair, signature::Signer,
|
||||
transaction::Transaction, transaction::TransactionError, transport::TransportError,
|
||||
signature::{Keypair, Signer},
|
||||
transaction::TransactionError,
|
||||
},
|
||||
spl_stake_pool::*,
|
||||
spl_stake_pool::{error::StakePoolError, state::StakePool},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_stake_pool_balance() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
async fn setup() -> (
|
||||
ProgramTestContext,
|
||||
StakePoolAccounts,
|
||||
Vec<ValidatorStakeAccount>,
|
||||
) {
|
||||
let mut context = program_test().start_with_context().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// TODO: Waiting for the ability to advance clock (or modify account data) to finish the tests
|
||||
// Add several accounts
|
||||
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![];
|
||||
const STAKE_ACCOUNTS: u64 = 3;
|
||||
for _ in 0..STAKE_ACCOUNTS {
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let _deposit_info = simple_deposit(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
stake_accounts.push(validator_stake_account);
|
||||
}
|
||||
|
||||
(context, stake_pool_accounts, stake_accounts)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_stake_pool_balance_with_wrong_validator_list() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.await
|
||||
.unwrap();
|
||||
async fn success() {
|
||||
let (mut context, stake_pool_accounts, stake_accounts) = setup().await;
|
||||
|
||||
let wrong_stake_list_storage = Keypair::new();
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::update_stake_pool_balance(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&wrong_stake_list_storage.pubkey(),
|
||||
let pre_balance = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
assert_eq!(pre_balance, stake_pool.total_stake_lamports);
|
||||
|
||||
let pre_token_supply = get_token_supply(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.update_stake_pool_balance(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
// Add extra funds, simulating rewards
|
||||
const EXTRA_STAKE_AMOUNT: u64 = 1_000_000;
|
||||
for stake_account in &stake_accounts {
|
||||
transfer(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_account.stake_account,
|
||||
EXTRA_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Update epoch
|
||||
context.warp_to_slot(50_000).unwrap();
|
||||
|
||||
// Update list and pool
|
||||
let error = stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check fee
|
||||
let post_balance = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
assert_eq!(post_balance, stake_pool.total_stake_lamports);
|
||||
|
||||
let actual_fee = get_token_balance(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let pool_token_supply = get_token_supply(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
let expected_fee = (post_balance - pre_balance) * pre_token_supply / pre_balance
|
||||
* stake_pool.fee.numerator
|
||||
/ stake_pool.fee.denominator;
|
||||
assert_eq!(actual_fee, expected_fee);
|
||||
assert_eq!(pool_token_supply, stake_pool.pool_token_supply);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_validator_list() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
let wrong_validator_list = Keypair::new();
|
||||
stake_pool_accounts.validator_list = wrong_validator_list;
|
||||
let error = stake_pool_accounts
|
||||
.update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32;
|
||||
) => {
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_pool_fee_account() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let wrong_fee_account = Keypair::new();
|
||||
stake_pool_accounts.pool_fee_account = wrong_fee_account;
|
||||
let error = stake_pool_accounts
|
||||
.update_stake_pool_balance(&mut banks_client, &payer, &recent_blockhash)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match error {
|
||||
TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
) => {
|
||||
let program_error = StakePoolError::InvalidFeeAccount as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to update pool balance with wrong validator stake list account"),
|
||||
|
|
|
@ -3,87 +3,616 @@
|
|||
mod helpers;
|
||||
|
||||
use {
|
||||
crate::helpers::TEST_STAKE_AMOUNT,
|
||||
borsh::BorshDeserialize,
|
||||
helpers::*,
|
||||
solana_program::{native_token, pubkey::Pubkey},
|
||||
solana_program::pubkey::Pubkey,
|
||||
solana_program_test::*,
|
||||
solana_sdk::signature::Signer,
|
||||
spl_stake_pool::{borsh::try_from_slice_unchecked, stake_program, state},
|
||||
solana_sdk::signature::{Keypair, Signer},
|
||||
spl_stake_pool::{
|
||||
borsh::try_from_slice_unchecked,
|
||||
stake_program,
|
||||
state::{StakePool, StakeStatus, ValidatorList},
|
||||
MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE,
|
||||
},
|
||||
};
|
||||
|
||||
async fn get_list_sum(banks_client: &mut BanksClient, validator_list_key: &Pubkey) -> u64 {
|
||||
let validator_list = banks_client
|
||||
.get_account(*validator_list_key)
|
||||
.await
|
||||
.expect("get_account")
|
||||
.expect("validator stake list not none");
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
async fn setup(
|
||||
num_validators: usize,
|
||||
) -> (
|
||||
ProgramTestContext,
|
||||
StakePoolAccounts,
|
||||
Vec<ValidatorStakeAccount>,
|
||||
u64,
|
||||
u64,
|
||||
u64,
|
||||
) {
|
||||
let mut context = program_test().start_with_context().await;
|
||||
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
let mut slot = first_normal_slot;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
validator_list
|
||||
.validators
|
||||
.iter()
|
||||
.map(|info| info.balance)
|
||||
.sum()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_validator_list_balance() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let reserve_stake_amount = TEST_STAKE_AMOUNT * num_validators as u64;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
reserve_stake_amount + 1,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Add several accounts
|
||||
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![];
|
||||
const STAKE_ACCOUNTS: u64 = 3;
|
||||
for _ in 0..STAKE_ACCOUNTS {
|
||||
stake_accounts.push(
|
||||
simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
|
||||
// Add stake extra funds
|
||||
const EXTRA_STAKE: u64 = 1_000_000;
|
||||
|
||||
for stake_account in stake_accounts {
|
||||
transfer(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_account.stake_account,
|
||||
EXTRA_STAKE,
|
||||
// so warmups / cooldowns go faster
|
||||
let validator = Keypair::new();
|
||||
let vote = Keypair::new();
|
||||
create_vote(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&validator,
|
||||
&vote,
|
||||
)
|
||||
.await;
|
||||
let deposit_account =
|
||||
DepositStakeAccount::new_with_vote(vote.pubkey(), validator.pubkey(), 100_000_000_000);
|
||||
deposit_account
|
||||
.create_and_delegate(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Add several accounts with some stake
|
||||
let mut stake_accounts: Vec<ValidatorStakeAccount> = vec![];
|
||||
let mut deposit_accounts: Vec<DepositStakeAccount> = vec![];
|
||||
for _ in 0..num_validators {
|
||||
let stake_account = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
stake_account
|
||||
.create_and_delegate(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_account = DepositStakeAccount::new_with_vote(
|
||||
stake_account.vote.pubkey(),
|
||||
stake_account.stake_account,
|
||||
TEST_STAKE_AMOUNT,
|
||||
);
|
||||
deposit_account
|
||||
.create_and_delegate(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
)
|
||||
.await;
|
||||
|
||||
stake_accounts.push(stake_account);
|
||||
deposit_accounts.push(deposit_account);
|
||||
}
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>())
|
||||
+ native_token::sol_to_lamports(1.0);
|
||||
// Warp forward so the stakes properly activate, and deposit
|
||||
// TODO This is *bad* -- program-test needs to have some more active stake
|
||||
// so we can warm up faster than this. Also, we need to do each of these
|
||||
// warps by hand to get fully active stakes.
|
||||
for i in 2..50 {
|
||||
slot = first_normal_slot + i * slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
}
|
||||
|
||||
// Check current balance in the list
|
||||
assert_eq!(
|
||||
get_list_sum(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey()
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await,
|
||||
STAKE_ACCOUNTS * (stake_rent + TEST_STAKE_AMOUNT)
|
||||
);
|
||||
.await;
|
||||
|
||||
// TODO: Execute update list with updated clock
|
||||
for stake_account in &stake_accounts {
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_account.stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
}
|
||||
|
||||
for deposit_account in &deposit_accounts {
|
||||
deposit_account
|
||||
.deposit(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
slot += slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
(
|
||||
context,
|
||||
stake_pool_accounts,
|
||||
stake_accounts,
|
||||
TEST_STAKE_AMOUNT,
|
||||
reserve_stake_amount,
|
||||
slot,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_validator_list_balance_with_uninitialized_validator_list() {} // TODO
|
||||
#[ignore]
|
||||
async fn success() {
|
||||
let num_validators = 5;
|
||||
let (
|
||||
mut context,
|
||||
stake_pool_accounts,
|
||||
stake_accounts,
|
||||
validator_lamports,
|
||||
reserve_lamports,
|
||||
mut slot,
|
||||
) = setup(num_validators).await;
|
||||
|
||||
// Check current balance in the list
|
||||
let rent = context.banks_client.get_rent().await.unwrap();
|
||||
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
// initially, have all of the deposits plus their rent, and the reserve stake
|
||||
let initial_lamports =
|
||||
(validator_lamports + stake_rent) * num_validators as u64 + reserve_lamports;
|
||||
assert_eq!(
|
||||
get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey()
|
||||
)
|
||||
.await,
|
||||
initial_lamports,
|
||||
);
|
||||
|
||||
// Simulate rewards
|
||||
for stake_account in &stake_accounts {
|
||||
context.increment_vote_account_credits(&stake_account.vote.pubkey(), 100);
|
||||
}
|
||||
|
||||
// Warp one more epoch so the rewards are paid out
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
slot += slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let new_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert!(new_lamports > initial_lamports);
|
||||
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(new_lamports, stake_pool.total_stake_lamports);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_update_validator_list_balance_with_wrong_stake_state() {} // TODO
|
||||
#[ignore]
|
||||
async fn merge_into_reserve() {
|
||||
let (mut context, stake_pool_accounts, stake_accounts, lamports, _, mut slot) =
|
||||
setup(MAX_VALIDATORS_TO_UPDATE).await;
|
||||
|
||||
let pre_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let reserve_stake = context
|
||||
.banks_client
|
||||
.get_account(stake_pool_accounts.reserve_stake.pubkey())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let pre_reserve_lamports = reserve_stake.lamports;
|
||||
|
||||
// Decrease from all validators
|
||||
for stake_account in &stake_accounts {
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_account.stake_account,
|
||||
&stake_account.transient_stake_account,
|
||||
lamports,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
}
|
||||
|
||||
// Update, should not change, no merges yet
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
let expected_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pre_lamports, expected_lamports);
|
||||
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
||||
|
||||
// Warp one more epoch so the stakes deactivate
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
slot += slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let expected_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pre_lamports, expected_lamports);
|
||||
|
||||
let reserve_stake = context
|
||||
.banks_client
|
||||
.get_account(stake_pool_accounts.reserve_stake.pubkey())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let post_reserve_lamports = reserve_stake.lamports;
|
||||
assert!(post_reserve_lamports > pre_reserve_lamports);
|
||||
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn merge_into_validator_stake() {
|
||||
let (mut context, stake_pool_accounts, stake_accounts, lamports, reserve_lamports, mut slot) =
|
||||
setup(MAX_VALIDATORS_TO_UPDATE).await;
|
||||
|
||||
let rent = context.banks_client.get_rent().await.unwrap();
|
||||
let pre_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Increase stake to all validators
|
||||
for stake_account in &stake_accounts {
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_account.transient_stake_account,
|
||||
&stake_account.vote.pubkey(),
|
||||
reserve_lamports / stake_accounts.len() as u64,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
}
|
||||
|
||||
// Warp just a little bit to get a new blockhash and update again
|
||||
context.warp_to_slot(slot + 10).unwrap();
|
||||
|
||||
// Update, should not change, no merges yet
|
||||
let error = stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let expected_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pre_lamports, expected_lamports);
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
||||
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(expected_lamports, stake_pool.total_stake_lamports);
|
||||
|
||||
// Warp one more epoch so the stakes activate, ready to merge
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
slot += slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
let current_lamports = get_validator_list_sum(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool_info = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_pool = StakePool::try_from_slice(&stake_pool_info.data).unwrap();
|
||||
assert_eq!(current_lamports, stake_pool.total_stake_lamports);
|
||||
|
||||
// Check that transient accounts are gone
|
||||
for stake_account in &stake_accounts {
|
||||
assert!(context
|
||||
.banks_client
|
||||
.get_account(stake_account.transient_stake_account)
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// Check validator stake accounts have the expected balance now:
|
||||
// validator stake account minimum + deposited lamports + 2 rents + increased lamports
|
||||
let expected_lamports = MINIMUM_ACTIVE_STAKE
|
||||
+ lamports
|
||||
+ reserve_lamports / stake_accounts.len() as u64
|
||||
+ 2 * rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
for stake_account in &stake_accounts {
|
||||
let validator_stake =
|
||||
get_account(&mut context.banks_client, &stake_account.stake_account).await;
|
||||
assert_eq!(validator_stake.lamports, expected_lamports);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn merge_transient_stake_after_remove() {
|
||||
let (mut context, stake_pool_accounts, stake_accounts, lamports, reserve_lamports, mut slot) =
|
||||
setup(1).await;
|
||||
|
||||
let rent = context.banks_client.get_rent().await.unwrap();
|
||||
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
let deactivated_lamports = lamports + stake_rent;
|
||||
let new_authority = Pubkey::new_unique();
|
||||
// Decrease and remove all validators
|
||||
for stake_account in &stake_accounts {
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_account.stake_account,
|
||||
&stake_account.transient_stake_account,
|
||||
deactivated_lamports,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
let error = stake_pool_accounts
|
||||
.remove_validator_from_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&new_authority,
|
||||
&stake_account.stake_account,
|
||||
&stake_account.transient_stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
}
|
||||
|
||||
// Warp forward to merge time
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
slot += slots_per_epoch;
|
||||
context.warp_to_slot(slot).unwrap();
|
||||
|
||||
// Update without merge, status should be DeactivatingTransient
|
||||
let error = stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
true,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let validator_list = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
assert_eq!(validator_list.validators.len(), 1);
|
||||
assert_eq!(
|
||||
validator_list.validators[0].status,
|
||||
StakeStatus::DeactivatingTransient
|
||||
);
|
||||
assert_eq!(
|
||||
validator_list.validators[0].stake_lamports,
|
||||
deactivated_lamports
|
||||
);
|
||||
|
||||
// Update with merge, status should be ReadyForRemoval and no lamports
|
||||
let error = stake_pool_accounts
|
||||
.update_validator_list_balance(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
stake_accounts
|
||||
.iter()
|
||||
.map(|v| v.vote.pubkey())
|
||||
.collect::<Vec<Pubkey>>()
|
||||
.as_slice(),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let validator_list = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
assert_eq!(validator_list.validators.len(), 1);
|
||||
assert_eq!(
|
||||
validator_list.validators[0].status,
|
||||
StakeStatus::ReadyForRemoval
|
||||
);
|
||||
assert_eq!(validator_list.validators[0].stake_lamports, 0);
|
||||
|
||||
let reserve_stake = context
|
||||
.banks_client
|
||||
.get_account(stake_pool_accounts.reserve_stake.pubkey())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
reserve_stake.lamports,
|
||||
reserve_lamports + deactivated_lamports + stake_rent + 1
|
||||
);
|
||||
|
||||
// Update stake pool balance, should be gone
|
||||
let error = stake_pool_accounts
|
||||
.update_stake_pool_balance(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let validator_list = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
assert_eq!(validator_list.validators.len(), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_uninitialized_validator_list() {} // TODO
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_wrong_stake_state() {} // TODO
|
||||
|
|
|
@ -19,7 +19,8 @@ use {
|
|||
transport::TransportError,
|
||||
},
|
||||
spl_stake_pool::{
|
||||
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state,
|
||||
borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction, stake_program,
|
||||
state,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -29,63 +30,37 @@ async fn setup() -> (
|
|||
Hash,
|
||||
StakePoolAccounts,
|
||||
ValidatorStakeAccount,
|
||||
Keypair,
|
||||
) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = Keypair::new();
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
user_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(
|
||||
banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
async fn success() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
|
@ -93,28 +68,10 @@ async fn test_add_validator_to_pool() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let stake_account_balance = banks_client
|
||||
.get_account(user_stake.stake_account)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.lamports;
|
||||
let deposit_tokens = stake_account_balance; // For now 1:1 math
|
||||
// Check token account balance
|
||||
let token_balance = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
|
||||
assert_eq!(token_balance, deposit_tokens);
|
||||
let pool_fee_token_balance = get_token_balance(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.pool_fee_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(pool_fee_token_balance, 0); // No fee when adding validator stake accounts
|
||||
|
||||
// Check if validator account was added to the list
|
||||
let validator_list = get_account(
|
||||
&mut banks_client,
|
||||
|
@ -129,9 +86,10 @@ async fn test_add_validator_to_pool() {
|
|||
account_type: state::AccountType::ValidatorList,
|
||||
max_validators: stake_pool_accounts.max_validators,
|
||||
validators: vec![state::ValidatorStakeInfo {
|
||||
vote_account: user_stake.vote.pubkey(),
|
||||
status: state::StakeStatus::Active,
|
||||
vote_account_address: user_stake.vote.pubkey(),
|
||||
last_update_epoch: 0,
|
||||
balance: stake_account_balance,
|
||||
stake_lamports: 0,
|
||||
}]
|
||||
}
|
||||
);
|
||||
|
@ -155,105 +113,9 @@ async fn test_add_validator_to_pool() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_with_wrong_token_program_id() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::add_validator_to_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&stake_program::id(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
|
||||
assert_eq!(error, InstructionError::IncorrectProgramId);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to add validator stake address with wrong token program ID"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_with_wrong_pool_mint_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_pool_mint = Keypair::new();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::add_validator_to_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&wrong_pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::WrongPoolMint as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to add validator stake address with wrong pool mint account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
async fn fail_with_wrong_validator_list_account() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
let wrong_validator_list = Keypair::new();
|
||||
|
||||
|
@ -261,19 +123,15 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
|
|||
&[instruction::add_validator_to_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&wrong_validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -285,7 +143,7 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32;
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to add validator stake address with wrong validator stake list account"),
|
||||
|
@ -293,15 +151,122 @@ async fn test_add_validator_to_pool_with_wrong_validator_list_account() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_try_to_add_already_added_validator_stake_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
async fn fail_too_little_stake() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
create_vote(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.validator,
|
||||
&user_stake.vote,
|
||||
)
|
||||
.await;
|
||||
|
||||
create_validator_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_pool,
|
||||
&stake_pool_accounts.staker,
|
||||
&user_stake.stake_account,
|
||||
&user_stake.vote.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create stake account to withdraw to
|
||||
let split = Keypair::new();
|
||||
create_blank_stake_account(&mut banks_client, &payer, &recent_blockhash, &split).await;
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[stake_program::split_only(
|
||||
&user_stake.stake_account,
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
1,
|
||||
&split.pubkey(),
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &stake_pool_accounts.staker],
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
);
|
||||
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_too_much_stake() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
user_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
transfer(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_double_add() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
|
@ -309,7 +274,6 @@ async fn test_try_to_add_already_added_validator_stake_account() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -321,7 +285,6 @@ async fn test_try_to_add_already_added_validator_stake_account() {
|
|||
&payer,
|
||||
&latest_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -331,7 +294,7 @@ async fn test_try_to_add_already_added_validator_stake_account() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::ValidatorAlreadyAdded as u32;
|
||||
let program_error = StakePoolError::ValidatorAlreadyAdded as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to add already added validator stake account"),
|
||||
|
@ -339,15 +302,9 @@ async fn test_try_to_add_already_added_validator_stake_account() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_owner_try_to_add_validator_to_pool() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
async fn fail_wrong_staker() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
let malicious = Keypair::new();
|
||||
|
||||
|
@ -356,13 +313,9 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
|
|||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&malicious.pubkey(),
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
|
@ -379,7 +332,7 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::WrongOwner as u32;
|
||||
let program_error = StakePoolError::WrongStaker as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to add validator stake account"),
|
||||
|
@ -387,28 +340,18 @@ async fn test_not_owner_try_to_add_validator_to_pool() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_owner_try_to_add_validator_to_pool_without_signature() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
async fn fail_without_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.deposit_authority, false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
|
||||
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
|
||||
AccountMeta::new(user_stake.stake_account, false),
|
||||
AccountMeta::new(user_pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
|
@ -432,7 +375,7 @@ async fn test_not_owner_try_to_add_validator_to_pool_without_signature() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::SignatureMissing as u32;
|
||||
let program_error = StakePoolError::SignatureMissing as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to add validator stake account without signing transaction"),
|
||||
|
@ -440,30 +383,20 @@ async fn test_not_owner_try_to_add_validator_to_pool_without_signature() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_with_wrong_stake_program_id() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
) = setup().await;
|
||||
async fn fail_with_wrong_stake_program_id() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, user_stake) =
|
||||
setup().await;
|
||||
|
||||
let wrong_stake_program = Pubkey::new_unique();
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.deposit_authority, false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
|
||||
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
|
||||
AccountMeta::new(user_stake.stake_account, false),
|
||||
AccountMeta::new(user_pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(wrong_stake_program, false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
|
@ -474,7 +407,7 @@ async fn test_add_validator_to_pool_with_wrong_stake_program_id() {
|
|||
.unwrap(),
|
||||
};
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -492,64 +425,42 @@ async fn test_add_validator_to_pool_with_wrong_stake_program_id() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_too_many_validator_stake_accounts() {
|
||||
async fn fail_add_too_many_validator_stake_accounts() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let mut stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts.max_validators = 1;
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = Keypair::new();
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
user_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
user_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
let error = stake_pool_accounts
|
||||
|
@ -558,7 +469,6 @@ async fn test_add_too_many_validator_stake_accounts() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
|
@ -570,7 +480,7 @@ async fn test_add_too_many_validator_stake_accounts() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_to_unupdated_stake_pool() {} // TODO
|
||||
async fn fail_with_unupdated_stake_pool() {} // TODO
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_add_validator_to_pool_with_uninitialized_validator_list_account() {} // TODO
|
||||
async fn fail_with_uninitialized_validator_list_account() {} // TODO
|
||||
|
|
|
@ -26,16 +26,24 @@ async fn success_create_validator_stake_account() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator = Keypair::new();
|
||||
create_vote(&mut banks_client, &payer, &recent_blockhash, &validator).await;
|
||||
let vote = Keypair::new();
|
||||
create_vote(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator,
|
||||
&vote,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (stake_account, _) = find_stake_program_address(
|
||||
&id(),
|
||||
&validator.pubkey(),
|
||||
&vote.pubkey(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
|
||||
|
@ -43,15 +51,15 @@ async fn success_create_validator_stake_account() {
|
|||
&[instruction::create_validator_stake_account(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&stake_account,
|
||||
&validator.pubkey(),
|
||||
&vote.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
banks_client.process_transaction(transaction).await.unwrap();
|
||||
|
||||
// Check authorities
|
||||
|
@ -59,12 +67,15 @@ async fn success_create_validator_stake_account() {
|
|||
let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap();
|
||||
match stake_state {
|
||||
stake_program::StakeState::Stake(meta, stake) => {
|
||||
assert_eq!(&meta.authorized.staker, &stake_pool_accounts.owner.pubkey());
|
||||
assert_eq!(
|
||||
&meta.authorized.staker,
|
||||
&stake_pool_accounts.staker.pubkey()
|
||||
);
|
||||
assert_eq!(
|
||||
&meta.authorized.withdrawer,
|
||||
&stake_pool_accounts.owner.pubkey()
|
||||
&stake_pool_accounts.staker.pubkey()
|
||||
);
|
||||
assert_eq!(stake.delegation.voter_pubkey, validator.pubkey());
|
||||
assert_eq!(stake.delegation.voter_pubkey, vote.pubkey());
|
||||
}
|
||||
_ => panic!(),
|
||||
}
|
||||
|
@ -75,7 +86,7 @@ async fn fail_create_validator_stake_account_on_non_vote_account() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -88,7 +99,7 @@ async fn fail_create_validator_stake_account_on_non_vote_account() {
|
|||
&[instruction::create_validator_stake_account(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&stake_account,
|
||||
&validator,
|
||||
|
@ -96,7 +107,7 @@ async fn fail_create_validator_stake_account_on_non_vote_account() {
|
|||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -115,7 +126,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -126,7 +137,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
|
|||
let wrong_system_program = Pubkey::new_unique();
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new(stake_account, false),
|
||||
AccountMeta::new_readonly(validator, false),
|
||||
|
@ -146,7 +157,7 @@ async fn fail_create_validator_stake_account_with_wrong_system_program() {
|
|||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -165,7 +176,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -176,7 +187,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
|
|||
let wrong_stake_program = Pubkey::new_unique();
|
||||
let accounts = vec![
|
||||
AccountMeta::new_readonly(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
|
||||
AccountMeta::new(payer.pubkey(), true),
|
||||
AccountMeta::new(stake_account, false),
|
||||
AccountMeta::new_readonly(validator, false),
|
||||
|
@ -196,7 +207,7 @@ async fn fail_create_validator_stake_account_with_wrong_stake_program() {
|
|||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -215,7 +226,7 @@ async fn fail_create_validator_stake_account_with_incorrect_address() {
|
|||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -226,7 +237,7 @@ async fn fail_create_validator_stake_account_with_incorrect_address() {
|
|||
&[instruction::create_validator_stake_account(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&stake_account.pubkey(),
|
||||
&validator,
|
||||
|
@ -234,7 +245,7 @@ async fn fail_create_validator_stake_account_with_incorrect_address() {
|
|||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
|
|
@ -19,7 +19,8 @@ use {
|
|||
transport::TransportError,
|
||||
},
|
||||
spl_stake_pool::{
|
||||
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state,
|
||||
borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction, stake_program,
|
||||
state,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -29,51 +30,30 @@ async fn setup() -> (
|
|||
Hash,
|
||||
StakePoolAccounts,
|
||||
ValidatorStakeAccount,
|
||||
Keypair,
|
||||
Keypair,
|
||||
) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 10_000_000_000)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let user = Keypair::new();
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
user_stake
|
||||
let validator_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
validator_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
// make pool token account
|
||||
let user_pool_account = Keypair::new();
|
||||
create_token_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&user.pubkey(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let error = stake_pool_accounts
|
||||
.add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&validator_stake.stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
@ -83,35 +63,14 @@ async fn setup() -> (
|
|||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
user,
|
||||
validator_stake,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
user,
|
||||
) = setup().await;
|
||||
|
||||
let tokens_to_burn = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
|
||||
delegate_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account.pubkey(),
|
||||
&user,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await;
|
||||
async fn success() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let error = stake_pool_accounts
|
||||
|
@ -119,17 +78,13 @@ async fn test_remove_validator_from_pool() {
|
|||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check if all tokens were burned
|
||||
let tokens_left = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
|
||||
assert_eq!(tokens_left, 0);
|
||||
|
||||
// Check if account was removed from the list of stake accounts
|
||||
let validator_list = get_account(
|
||||
&mut banks_client,
|
||||
|
@ -148,7 +103,7 @@ async fn test_remove_validator_from_pool() {
|
|||
);
|
||||
|
||||
// Check of stake account authority has changed
|
||||
let stake = get_account(&mut banks_client, &user_stake.stake_account).await;
|
||||
let stake = get_account(&mut banks_client, &validator_stake.stake_account).await;
|
||||
let stake_state = deserialize::<stake_program::StakeState>(&stake.data).unwrap();
|
||||
match stake_state {
|
||||
stake_program::StakeState::Stake(meta, _) => {
|
||||
|
@ -160,31 +115,22 @@ async fn test_remove_validator_from_pool() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_with_wrong_stake_program_id() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
async fn fail_with_wrong_stake_program_id() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let wrong_stake_program = Pubkey::new_unique();
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), true),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
|
||||
AccountMeta::new_readonly(new_authority, false),
|
||||
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
|
||||
AccountMeta::new(user_stake.stake_account, false),
|
||||
AccountMeta::new(user_pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new(validator_stake.stake_account, false),
|
||||
AccountMeta::new_readonly(validator_stake.transient_stake_account, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(wrong_stake_program, false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
|
@ -196,7 +142,7 @@ async fn test_remove_validator_from_pool_with_wrong_stake_program_id() {
|
|||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -215,112 +161,9 @@ async fn test_remove_validator_from_pool_with_wrong_stake_program_id() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_with_wrong_token_program_id() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_token_program = Keypair::new();
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::remove_validator_from_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&new_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&wrong_token_program.pubkey(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
|
||||
assert_eq!(error, InstructionError::IncorrectProgramId);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong token program ID"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_with_wrong_pool_mint_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
|
||||
let wrong_pool_mint = Keypair::new();
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[instruction::remove_validator_from_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&new_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&wrong_pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::WrongPoolMint as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong pool mint account"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
async fn fail_with_wrong_validator_list_account() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let wrong_validator_list = Keypair::new();
|
||||
|
||||
|
@ -329,19 +172,17 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
|
|||
&[instruction::remove_validator_from_pool(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
&stake_pool_accounts.owner.pubkey(),
|
||||
&stake_pool_accounts.staker.pubkey(),
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&new_authority,
|
||||
&wrong_validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.owner], recent_blockhash);
|
||||
transaction.sign(&[&payer, &stake_pool_accounts.staker], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -353,7 +194,7 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32;
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to remove validator stake address with wrong validator stake list account"),
|
||||
|
@ -361,26 +202,16 @@ async fn test_remove_validator_from_pool_with_wrong_validator_list_account() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_already_removed_validator_stake_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
user,
|
||||
) = setup().await;
|
||||
async fn fail_not_at_minimum() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let tokens_to_burn = get_token_balance(&mut banks_client, &user_pool_account.pubkey()).await;
|
||||
delegate_tokens(
|
||||
transfer(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_pool_account.pubkey(),
|
||||
&user,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
tokens_to_burn,
|
||||
&validator_stake.stake_account,
|
||||
1_000_001,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -390,9 +221,36 @@ async fn test_remove_already_removed_validator_stake_account() {
|
|||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_double_remove() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let error = stake_pool_accounts
|
||||
.remove_validator_from_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
@ -404,9 +262,9 @@ async fn test_remove_already_removed_validator_stake_account() {
|
|||
&mut banks_client,
|
||||
&payer,
|
||||
&latest_blockhash,
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -416,7 +274,7 @@ async fn test_remove_already_removed_validator_stake_account() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::ValidatorNotFound as u32;
|
||||
let program_error = StakePoolError::ValidatorNotFound as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => {
|
||||
|
@ -426,16 +284,9 @@ async fn test_remove_already_removed_validator_stake_account() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_owner_try_to_remove_validator_from_pool() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
async fn fail_wrong_staker() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let malicious = Keypair::new();
|
||||
|
||||
|
@ -448,10 +299,8 @@ async fn test_not_owner_try_to_remove_validator_from_pool() {
|
|||
&stake_pool_accounts.withdraw_authority,
|
||||
&new_authority,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
&user_stake.stake_account,
|
||||
&user_pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&spl_token::id(),
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
|
@ -468,38 +317,31 @@ async fn test_not_owner_try_to_remove_validator_from_pool() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::WrongOwner as u32;
|
||||
let program_error = StakePoolError::WrongStaker as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while not an owner try to remove validator stake address"),
|
||||
_ => {
|
||||
panic!("Wrong error occurs while not an staker try to remove validator stake address")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
user_stake,
|
||||
user_pool_account,
|
||||
_,
|
||||
) = setup().await;
|
||||
async fn fail_no_signature() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
|
||||
let accounts = vec![
|
||||
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.staker.pubkey(), false),
|
||||
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
|
||||
AccountMeta::new_readonly(new_authority, false),
|
||||
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
|
||||
AccountMeta::new(user_stake.stake_account, false),
|
||||
AccountMeta::new(user_pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new(validator_stake.stake_account, false),
|
||||
AccountMeta::new_readonly(validator_stake.transient_stake_account, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
AccountMeta::new_readonly(stake_program::id(), false),
|
||||
];
|
||||
let instruction = Instruction {
|
||||
|
@ -510,8 +352,12 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
|
|||
.unwrap(),
|
||||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer],
|
||||
recent_blockhash,
|
||||
);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -523,7 +369,7 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::SignatureMissing as u32;
|
||||
let program_error = StakePoolError::SignatureMissing as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while malicious try to remove validator stake account without signing transaction"),
|
||||
|
@ -531,7 +377,185 @@ async fn test_not_owner_try_to_remove_validator_from_pool_without_signature() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_from_unupdated_stake_pool() {} // TODO
|
||||
async fn fail_with_activating_transient_stake() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
// increase the validator stake
|
||||
let error = stake_pool_accounts
|
||||
.increase_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.transient_stake_account,
|
||||
&validator_stake.vote.pubkey(),
|
||||
2_000_000_000,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let error = stake_pool_accounts
|
||||
.remove_validator_from_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
match error {
|
||||
TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
) => {
|
||||
let program_error = StakePoolError::WrongStakeState as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while removing validator stake account while transient stake is activating"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_remove_validator_from_pool_with_uninitialized_validator_list_account() {} // TODO
|
||||
async fn success_with_deactivating_transient_stake() {
|
||||
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, validator_stake) =
|
||||
setup().await;
|
||||
|
||||
let rent = banks_client.get_rent().await.unwrap();
|
||||
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
let deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// increase the validator stake
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
TEST_STAKE_AMOUNT + stake_rent,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let error = stake_pool_accounts
|
||||
.remove_validator_from_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&new_authority,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// fail deposit
|
||||
let maybe_deposit = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
assert!(maybe_deposit.is_none());
|
||||
|
||||
// fail withdraw
|
||||
let user_stake_recipient = Keypair::new();
|
||||
create_blank_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient,
|
||||
)
|
||||
.await;
|
||||
|
||||
let user_transfer_authority = Keypair::new();
|
||||
let new_authority = Pubkey::new_unique();
|
||||
delegate_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&deposit_info.authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake.stake_account,
|
||||
&new_authority,
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_some());
|
||||
|
||||
// check validator has changed
|
||||
let validator_list = get_account(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
let expected_list = state::ValidatorList {
|
||||
account_type: state::AccountType::ValidatorList,
|
||||
max_validators: stake_pool_accounts.max_validators,
|
||||
validators: vec![state::ValidatorStakeInfo {
|
||||
status: state::StakeStatus::DeactivatingTransient,
|
||||
vote_account_address: validator_stake.vote.pubkey(),
|
||||
last_update_epoch: 0,
|
||||
stake_lamports: TEST_STAKE_AMOUNT + stake_rent,
|
||||
}],
|
||||
};
|
||||
assert_eq!(validator_list, expected_list);
|
||||
|
||||
// Update, should not change, no merges yet
|
||||
let error = stake_pool_accounts
|
||||
.update_all(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&[validator_stake.vote.pubkey()],
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let validator_list = get_account(
|
||||
&mut banks_client,
|
||||
&stake_pool_accounts.validator_list.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let validator_list =
|
||||
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
|
||||
assert_eq!(validator_list, expected_list);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_not_updated_stake_pool() {} // TODO
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_with_uninitialized_validator_list_account() {} // TODO
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
mod helpers;
|
||||
|
||||
use {
|
||||
bincode::deserialize,
|
||||
borsh::{BorshDeserialize, BorshSerialize},
|
||||
helpers::*,
|
||||
solana_program::{
|
||||
|
@ -18,7 +19,8 @@ use {
|
|||
transport::TransportError,
|
||||
},
|
||||
spl_stake_pool::{
|
||||
borsh::try_from_slice_unchecked, error, id, instruction, stake_program, state,
|
||||
borsh::try_from_slice_unchecked, error::StakePoolError, id, instruction,
|
||||
minimum_stake_lamports, stake_program, state,
|
||||
},
|
||||
spl_token::error::TokenError,
|
||||
};
|
||||
|
@ -29,17 +31,18 @@ async fn setup() -> (
|
|||
Hash,
|
||||
StakePoolAccounts,
|
||||
ValidatorStakeAccount,
|
||||
DepositInfo,
|
||||
DepositStakeAccount,
|
||||
Keypair,
|
||||
u64,
|
||||
) {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool(
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -47,25 +50,28 @@ async fn setup() -> (
|
|||
)
|
||||
.await;
|
||||
|
||||
let deposit_info: DepositInfo = simple_deposit(
|
||||
let deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tokens_to_burn = deposit_info.pool_tokens / 4;
|
||||
|
||||
// Delegate tokens for burning
|
||||
let user_transfer_authority = Keypair::new();
|
||||
delegate_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&deposit_info.user_pool_account,
|
||||
&deposit_info.user,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&deposit_info.authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await;
|
||||
|
@ -77,12 +83,13 @@ async fn setup() -> (
|
|||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw() {
|
||||
async fn success() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -90,6 +97,7 @@ async fn test_stake_pool_withdraw() {
|
|||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -123,33 +131,34 @@ async fn test_stake_pool_withdraw() {
|
|||
|
||||
// Save user token balance
|
||||
let user_token_balance_before =
|
||||
get_token_balance(&mut banks_client, &deposit_info.user_pool_account).await;
|
||||
get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
stake_pool_accounts
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check pool stats
|
||||
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
|
||||
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
|
||||
assert_eq!(
|
||||
stake_pool.stake_total,
|
||||
stake_pool_before.stake_total - tokens_to_burn
|
||||
stake_pool.total_stake_lamports,
|
||||
stake_pool_before.total_stake_lamports - tokens_to_burn
|
||||
);
|
||||
assert_eq!(
|
||||
stake_pool.pool_total,
|
||||
stake_pool_before.pool_total - tokens_to_burn
|
||||
stake_pool.pool_token_supply,
|
||||
stake_pool_before.pool_token_supply - tokens_to_burn
|
||||
);
|
||||
|
||||
// Check validator stake list storage
|
||||
|
@ -164,13 +173,13 @@ async fn test_stake_pool_withdraw() {
|
|||
.find(&validator_stake_account.vote.pubkey())
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
validator_stake_item.balance,
|
||||
validator_stake_item_before.balance - tokens_to_burn
|
||||
validator_stake_item.stake_lamports,
|
||||
validator_stake_item_before.stake_lamports - tokens_to_burn
|
||||
);
|
||||
|
||||
// Check tokens burned
|
||||
let user_token_balance =
|
||||
get_token_balance(&mut banks_client, &deposit_info.user_pool_account).await;
|
||||
get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
|
||||
assert_eq!(
|
||||
user_token_balance,
|
||||
user_token_balance_before - tokens_to_burn
|
||||
|
@ -179,9 +188,12 @@ async fn test_stake_pool_withdraw() {
|
|||
// Check validator stake account balance
|
||||
let validator_stake_account =
|
||||
get_account(&mut banks_client, &validator_stake_account.stake_account).await;
|
||||
let stake_state =
|
||||
deserialize::<stake_program::StakeState>(&validator_stake_account.data).unwrap();
|
||||
let meta = stake_state.meta().unwrap();
|
||||
assert_eq!(
|
||||
validator_stake_account.lamports,
|
||||
validator_stake_item.balance
|
||||
validator_stake_account.lamports - minimum_stake_lamports(&meta),
|
||||
validator_stake_item.stake_lamports
|
||||
);
|
||||
|
||||
// Check user recipient stake account balance
|
||||
|
@ -194,7 +206,7 @@ async fn test_stake_pool_withdraw() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_with_wrong_stake_program() {
|
||||
async fn fail_with_wrong_stake_program() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -202,6 +214,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
|
|||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -218,7 +231,8 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
|
|||
AccountMeta::new(validator_stake_account.stake_account, false),
|
||||
AccountMeta::new(user_stake_recipient.pubkey(), false),
|
||||
AccountMeta::new_readonly(new_authority, false),
|
||||
AccountMeta::new(deposit_info.user_pool_account, false),
|
||||
AccountMeta::new_readonly(user_transfer_authority.pubkey(), true),
|
||||
AccountMeta::new(deposit_info.pool_account.pubkey(), false),
|
||||
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(spl_token::id(), false),
|
||||
|
@ -232,8 +246,12 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
|
|||
.unwrap(),
|
||||
};
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &user_transfer_authority],
|
||||
recent_blockhash,
|
||||
);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -249,7 +267,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
|
||||
async fn fail_with_wrong_withdraw_authority() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -257,6 +275,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
|
|||
mut stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -272,13 +291,13 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
|
@ -286,7 +305,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidProgramAddress as u32;
|
||||
let program_error = StakePoolError::InvalidProgramAddress as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to withdraw with wrong withdraw authority"),
|
||||
|
@ -294,7 +313,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
|
||||
async fn fail_with_wrong_token_program_id() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -302,6 +321,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
|
|||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -311,7 +331,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
|
|||
let new_authority = Pubkey::new_unique();
|
||||
let wrong_token_program = Keypair::new();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
let transaction = Transaction::new_signed_with_payer(
|
||||
&[instruction::withdraw(
|
||||
&id(),
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
|
@ -320,15 +340,17 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
|
|||
&validator_stake_account.stake_account,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&new_authority,
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority.pubkey(),
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&stake_pool_accounts.pool_mint.pubkey(),
|
||||
&wrong_token_program.pubkey(),
|
||||
tokens_to_burn,
|
||||
)
|
||||
.unwrap()],
|
||||
Some(&payer.pubkey()),
|
||||
&[&payer, &user_transfer_authority],
|
||||
recent_blockhash,
|
||||
);
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
let transaction_error = banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
|
@ -344,7 +366,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_with_wrong_validator_list() {
|
||||
async fn fail_with_wrong_validator_list() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -352,6 +374,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
|
|||
mut stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -367,13 +390,13 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
|
@ -381,7 +404,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
|
|||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::InvalidValidatorStakeList as u32;
|
||||
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!(
|
||||
|
@ -391,37 +414,36 @@ async fn test_stake_pool_withdraw_with_wrong_validator_list() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_from_unknown_validator() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.await
|
||||
.unwrap();
|
||||
async fn fail_with_unknown_validator() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
_,
|
||||
_,
|
||||
user_transfer_authority,
|
||||
_,
|
||||
) = setup().await;
|
||||
|
||||
let validator_stake_account = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let validator_stake_account =
|
||||
ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
validator_stake_account
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
let user_stake = ValidatorStakeAccount::new_with_target_authority(
|
||||
&stake_pool_accounts.deposit_authority,
|
||||
&stake_pool_accounts.stake_pool.pubkey(),
|
||||
);
|
||||
let user_stake = ValidatorStakeAccount::new(&stake_pool_accounts.stake_pool.pubkey());
|
||||
user_stake
|
||||
.create_and_delegate(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts.owner,
|
||||
&stake_pool_accounts.staker,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -453,6 +475,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
|
|||
&user_stake,
|
||||
&authorized,
|
||||
&lockup,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
// make pool token account
|
||||
|
@ -480,7 +503,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
|
|||
&recent_blockhash,
|
||||
&user_pool_account,
|
||||
&user,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await;
|
||||
|
@ -496,21 +519,19 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&user_transfer_authority,
|
||||
&user_pool_account,
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
TransportError::TransactionError(TransactionError::InstructionError(
|
||||
_,
|
||||
InstructionError::Custom(error_index),
|
||||
)) => {
|
||||
let program_error = error::StakePoolError::ValidatorNotFound as u32;
|
||||
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
|
||||
let program_error = StakePoolError::ValidatorNotFound as u32;
|
||||
assert_eq!(error_index, program_error);
|
||||
}
|
||||
_ => panic!("Wrong error occurs while try to do withdraw from unknown validator"),
|
||||
|
@ -518,7 +539,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_double_withdraw_to_the_same_account() {
|
||||
async fn fail_double_withdraw_to_the_same_account() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
|
@ -526,6 +547,7 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
|
|||
stake_pool_accounts,
|
||||
validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
|
@ -540,35 +562,48 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
|
|||
.await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
stake_pool_accounts
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
let latest_blockhash = banks_client.get_recent_blockhash().await.unwrap();
|
||||
|
||||
// Delegate tokens for burning
|
||||
delegate_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&latest_blockhash,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&deposit_info.authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await;
|
||||
|
||||
let transaction_error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&latest_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
|
@ -580,15 +615,15 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
|
||||
async fn fail_without_token_approval() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool(
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -596,14 +631,16 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
|
|||
)
|
||||
.await;
|
||||
|
||||
let deposit_info: DepositInfo = simple_deposit(
|
||||
let deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tokens_to_burn = deposit_info.pool_tokens / 4;
|
||||
|
||||
|
@ -618,19 +655,20 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
|
|||
.await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let user_transfer_authority = Keypair::new();
|
||||
let transaction_error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
|
@ -648,15 +686,15 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_stake_pool_withdraw_with_low_delegation() {
|
||||
async fn fail_with_low_delegation() {
|
||||
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
|
||||
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake_account: ValidatorStakeAccount = simple_add_validator_to_pool(
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
|
@ -664,25 +702,28 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
|
|||
)
|
||||
.await;
|
||||
|
||||
let deposit_info: DepositInfo = simple_deposit(
|
||||
let deposit_info = simple_deposit(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake_account,
|
||||
TEST_STAKE_AMOUNT,
|
||||
)
|
||||
.await;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let tokens_to_burn = deposit_info.pool_tokens / 4;
|
||||
|
||||
let user_transfer_authority = Keypair::new();
|
||||
// Delegate tokens for burning
|
||||
delegate_tokens(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&deposit_info.user_pool_account,
|
||||
&deposit_info.user,
|
||||
&stake_pool_accounts.withdraw_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&deposit_info.authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
1,
|
||||
)
|
||||
.await;
|
||||
|
@ -704,13 +745,13 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
|
|||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&deposit_info.user_pool_account,
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.unwrap();
|
||||
|
||||
match transaction_error {
|
||||
|
@ -726,3 +767,250 @@ async fn test_stake_pool_withdraw_with_low_delegation() {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn fail_overdraw_validator() {
|
||||
let (
|
||||
mut banks_client,
|
||||
payer,
|
||||
recent_blockhash,
|
||||
stake_pool_accounts,
|
||||
_validator_stake_account,
|
||||
deposit_info,
|
||||
user_transfer_authority,
|
||||
tokens_to_burn,
|
||||
) = setup().await;
|
||||
|
||||
// Create stake account to withdraw to
|
||||
let user_stake_recipient = Keypair::new();
|
||||
let _initial_stake_lamports = create_blank_stake_account(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient,
|
||||
)
|
||||
.await;
|
||||
|
||||
let validator_stake_account = simple_add_validator_to_pool(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let new_authority = Pubkey::new_unique();
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
&recent_blockhash,
|
||||
&user_stake_recipient.pubkey(),
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&validator_stake_account.stake_account,
|
||||
&new_authority,
|
||||
tokens_to_burn,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn success_with_reserve() {
|
||||
let mut context = program_test().start_with_context().await;
|
||||
let stake_pool_accounts = StakePoolAccounts::new();
|
||||
let initial_reserve_lamports = 1;
|
||||
stake_pool_accounts
|
||||
.initialize_stake_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
initial_reserve_lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let validator_stake = simple_add_validator_to_pool(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts,
|
||||
)
|
||||
.await;
|
||||
|
||||
let deposit_lamports = TEST_STAKE_AMOUNT;
|
||||
let rent = context.banks_client.get_rent().await.unwrap();
|
||||
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake_program::StakeState>());
|
||||
|
||||
let deposit_info = simple_deposit(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&stake_pool_accounts,
|
||||
&validator_stake,
|
||||
deposit_lamports,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// decrease some stake
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
deposit_lamports - 1,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// warp forward to deactivation
|
||||
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
|
||||
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
|
||||
context
|
||||
.warp_to_slot(first_normal_slot + slots_per_epoch)
|
||||
.unwrap();
|
||||
|
||||
// update to merge deactivated stake into reserve
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&[validator_stake.vote.pubkey()],
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Delegate tokens for burning during withdraw
|
||||
let user_transfer_authority = Keypair::new();
|
||||
delegate_tokens(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&deposit_info.authority,
|
||||
&user_transfer_authority.pubkey(),
|
||||
deposit_info.pool_tokens,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Withdraw directly from reserve, fail because some stake left
|
||||
let withdraw_destination = Keypair::new();
|
||||
let withdraw_destination_authority = Pubkey::new_unique();
|
||||
let initial_stake_lamports = create_blank_stake_account(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&withdraw_destination,
|
||||
)
|
||||
.await;
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&withdraw_destination.pubkey(),
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&withdraw_destination_authority,
|
||||
deposit_info.pool_tokens,
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
error,
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
|
||||
)
|
||||
);
|
||||
|
||||
// decrease rest of stake
|
||||
let error = stake_pool_accounts
|
||||
.decrease_validator_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&validator_stake.stake_account,
|
||||
&validator_stake.transient_stake_account,
|
||||
stake_rent + 1,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// warp forward to deactivation
|
||||
context
|
||||
.warp_to_slot(first_normal_slot + 2 * slots_per_epoch)
|
||||
.unwrap();
|
||||
|
||||
// update to merge deactivated stake into reserve
|
||||
stake_pool_accounts
|
||||
.update_all(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&[validator_stake.vote.pubkey()],
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
|
||||
// now it works
|
||||
let error = stake_pool_accounts
|
||||
.withdraw_stake(
|
||||
&mut context.banks_client,
|
||||
&context.payer,
|
||||
&context.last_blockhash,
|
||||
&withdraw_destination.pubkey(),
|
||||
&user_transfer_authority,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
&withdraw_destination_authority,
|
||||
deposit_info.pool_tokens,
|
||||
)
|
||||
.await;
|
||||
assert!(error.is_none());
|
||||
|
||||
// Check tokens burned
|
||||
let user_token_balance = get_token_balance(
|
||||
&mut context.banks_client,
|
||||
&deposit_info.pool_account.pubkey(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(user_token_balance, 0);
|
||||
|
||||
// Check reserve stake account balance
|
||||
let reserve_stake_account = get_account(
|
||||
&mut context.banks_client,
|
||||
&stake_pool_accounts.reserve_stake.pubkey(),
|
||||
)
|
||||
.await;
|
||||
let stake_state =
|
||||
deserialize::<stake_program::StakeState>(&reserve_stake_account.data).unwrap();
|
||||
let meta = stake_state.meta().unwrap();
|
||||
assert_eq!(
|
||||
initial_reserve_lamports + meta.rent_exempt_reserve,
|
||||
reserve_stake_account.lamports
|
||||
);
|
||||
|
||||
// Check user recipient stake account balance
|
||||
let user_stake_recipient_account =
|
||||
get_account(&mut context.banks_client, &withdraw_destination.pubkey()).await;
|
||||
assert_eq!(
|
||||
user_stake_recipient_account.lamports,
|
||||
initial_stake_lamports + deposit_info.stake_lamports + stake_rent
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,13 +11,13 @@ exclude = ["js/**"]
|
|||
|
||||
[dependencies]
|
||||
bincode = "1.3"
|
||||
borsh = "0.7.1"
|
||||
borsh = "0.8"
|
||||
curve25519-dalek = {package = "curve25519-dalek-ng", git = "https://github.com/garious/curve25519-dalek", rev = "fcef1fa11b3d3e89a1abf8986386ba9ae375392c", default-features = false, features = ["borsh"]}
|
||||
elgamal_ristretto = { git = "https://github.com/garious/elgamal", rev = "892dbe115104bcb8cc26d79f9676c836ff6c018e", default-features = false }
|
||||
futures = "0.3"
|
||||
solana-banks-client = "1.6.2"
|
||||
solana-cli-config = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-banks-client = "1.6.7"
|
||||
solana-cli-config = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
spl-themis-ristretto = { version = "0.1.0", path = "../program_ristretto", features = ["no-entrypoint"] }
|
||||
tarpc = { version = "0.22.0", features = ["full"] }
|
||||
tokio = "0.3"
|
||||
|
@ -25,11 +25,11 @@ url = "2.1"
|
|||
|
||||
[dev-dependencies]
|
||||
separator = "0.4.1"
|
||||
solana-banks-server = "1.6.2"
|
||||
solana-bpf-loader-program = "1.6.2"
|
||||
solana-core = "1.6.2"
|
||||
solana-banks-server = "1.6.7"
|
||||
solana-bpf-loader-program = "1.6.7"
|
||||
solana-core = "1.6.7"
|
||||
solana_rbpf = "0.1"
|
||||
solana-runtime = "1.6.2"
|
||||
solana-runtime = "1.6.7"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
|
|
|
@ -20,7 +20,7 @@ getrandom = { version = "0.1.15", features = ["dummy"] }
|
|||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
rand = "0.8.0"
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
subtle = "=2.2.3"
|
||||
thiserror = "1.0"
|
||||
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "spl-token-lending-client"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Token Lending Client"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
repository = "https://github.com/solana-labs/solana-program-library"
|
||||
license = "Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
solana-client = "1.6.2"
|
||||
solana-program = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
spl-token-lending = { path = "../program", features = [ "no-entrypoint" ] }
|
||||
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
|
|
@ -1,304 +0,0 @@
|
|||
use solana_client::rpc_client::RpcClient;
|
||||
use solana_program::program_pack::Pack;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{read_keypair_file, Keypair, Signer},
|
||||
system_instruction::create_account,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use spl_token::{
|
||||
instruction::approve,
|
||||
state::{Account as Token, Mint},
|
||||
};
|
||||
use spl_token_lending::{
|
||||
instruction::{init_lending_market, init_reserve},
|
||||
state::{LendingMarket, Reserve, ReserveConfig, ReserveFees},
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
// -------- UPDATE START -------
|
||||
const KEYPAIR_PATH: &str = "/your/path";
|
||||
const SRM_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
|
||||
const USDC_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
|
||||
const WRAPPED_SOL_TOKEN_ACCOUNT: &str = "BASE58_ADDRESS";
|
||||
solana_program::declare_id!("TokenLend1ng1111111111111111111111111111111");
|
||||
// -------- UPDATE END ---------
|
||||
|
||||
pub struct DexMarket {
|
||||
pub name: &'static str,
|
||||
pub pubkey: Pubkey,
|
||||
}
|
||||
|
||||
pub fn main() {
|
||||
let mut client = RpcClient::new("https://api.mainnet-beta.solana.com".to_owned());
|
||||
|
||||
let payer = read_keypair_file(&format!("{}/payer.json", KEYPAIR_PATH)).unwrap();
|
||||
let usdc_mint_pubkey =
|
||||
Pubkey::from_str("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v").unwrap();
|
||||
|
||||
let sol_usdc_dex_market = DexMarket {
|
||||
name: "sol_usdc",
|
||||
pubkey: Pubkey::from_str("9wFFyRfZBsuAha4YcuxcXLKwMxJR43S7fPfQLusDBzvT").unwrap(),
|
||||
};
|
||||
|
||||
let srm_usdc_dex_market = DexMarket {
|
||||
name: "srm_usdc",
|
||||
pubkey: Pubkey::from_str("ByRys5tuUWDgL73G8JBAEfkdFf8JWBzPBDHsBVQ5vbQA").unwrap(),
|
||||
};
|
||||
|
||||
let quote_token_mint = usdc_mint_pubkey;
|
||||
let (lending_market_owner, lending_market_pubkey, _lending_market) =
|
||||
create_lending_market(&mut client, quote_token_mint, &payer);
|
||||
|
||||
let usdc_liquidity_source = Pubkey::from_str(USDC_TOKEN_ACCOUNT).unwrap();
|
||||
let usdc_reserve_config = ReserveConfig {
|
||||
optimal_utilization_rate: 80,
|
||||
loan_to_value_ratio: 75,
|
||||
liquidation_bonus: 5,
|
||||
liquidation_threshold: 80,
|
||||
min_borrow_rate: 0,
|
||||
optimal_borrow_rate: 4,
|
||||
max_borrow_rate: 30,
|
||||
fees: ReserveFees {
|
||||
borrow_fee_wad: 100_000_000_000_000, // 1 bp
|
||||
host_fee_percentage: 20,
|
||||
},
|
||||
};
|
||||
|
||||
let (usdc_reserve_pubkey, _usdc_reserve) = create_reserve(
|
||||
&mut client,
|
||||
usdc_reserve_config,
|
||||
lending_market_pubkey,
|
||||
&lending_market_owner,
|
||||
None,
|
||||
usdc_liquidity_source,
|
||||
&payer,
|
||||
);
|
||||
|
||||
println!("Created usdc reserve with pubkey: {}", usdc_reserve_pubkey);
|
||||
|
||||
let sol_liquidity_source = Pubkey::from_str(WRAPPED_SOL_TOKEN_ACCOUNT).unwrap();
|
||||
let sol_reserve_config = ReserveConfig {
|
||||
optimal_utilization_rate: 0,
|
||||
loan_to_value_ratio: 75,
|
||||
liquidation_bonus: 10,
|
||||
liquidation_threshold: 80,
|
||||
min_borrow_rate: 0,
|
||||
optimal_borrow_rate: 2,
|
||||
max_borrow_rate: 15,
|
||||
fees: ReserveFees {
|
||||
borrow_fee_wad: 1_000_000_000_000, // 0.01 bp
|
||||
host_fee_percentage: 20,
|
||||
},
|
||||
};
|
||||
|
||||
let (sol_reserve_pubkey, _sol_reserve) = create_reserve(
|
||||
&mut client,
|
||||
sol_reserve_config,
|
||||
lending_market_pubkey,
|
||||
&lending_market_owner,
|
||||
Some(sol_usdc_dex_market.pubkey),
|
||||
sol_liquidity_source,
|
||||
&payer,
|
||||
);
|
||||
|
||||
println!("Created sol reserve with pubkey: {}", sol_reserve_pubkey);
|
||||
|
||||
let srm_liquidity_source = Pubkey::from_str(SRM_TOKEN_ACCOUNT).unwrap();
|
||||
let srm_reserve_config = ReserveConfig {
|
||||
optimal_utilization_rate: 0,
|
||||
loan_to_value_ratio: 75,
|
||||
liquidation_bonus: 10,
|
||||
liquidation_threshold: 80,
|
||||
min_borrow_rate: 0,
|
||||
optimal_borrow_rate: 2,
|
||||
max_borrow_rate: 15,
|
||||
fees: ReserveFees {
|
||||
borrow_fee_wad: 10_000_000_000_000, // 0.1 bp
|
||||
host_fee_percentage: 25,
|
||||
},
|
||||
};
|
||||
|
||||
let (srm_reserve_pubkey, _srm_reserve) = create_reserve(
|
||||
&mut client,
|
||||
srm_reserve_config,
|
||||
lending_market_pubkey,
|
||||
&lending_market_owner,
|
||||
Some(srm_usdc_dex_market.pubkey),
|
||||
srm_liquidity_source,
|
||||
&payer,
|
||||
);
|
||||
|
||||
println!("Created srm reserve with pubkey: {}", srm_reserve_pubkey);
|
||||
}
|
||||
|
||||
pub fn create_lending_market(
|
||||
client: &mut RpcClient,
|
||||
quote_token_mint: Pubkey,
|
||||
payer: &Keypair,
|
||||
) -> (Keypair, Pubkey, LendingMarket) {
|
||||
let owner = read_keypair_file(&format!("{}/lending_market_owner.json", KEYPAIR_PATH)).unwrap();
|
||||
let keypair = Keypair::new();
|
||||
let pubkey = keypair.pubkey();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&pubkey,
|
||||
client
|
||||
.get_minimum_balance_for_rent_exemption(LendingMarket::LEN)
|
||||
.unwrap(),
|
||||
LendingMarket::LEN as u64,
|
||||
&id(),
|
||||
),
|
||||
init_lending_market(id(), pubkey, owner.pubkey(), quote_token_mint),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
|
||||
transaction.sign(&[&payer, &keypair], recent_blockhash);
|
||||
client.send_and_confirm_transaction(&transaction).unwrap();
|
||||
|
||||
let account = client.get_account(&pubkey).unwrap();
|
||||
let lending_market = LendingMarket::unpack(&account.data).unwrap();
|
||||
|
||||
(owner, pubkey, lending_market)
|
||||
}
|
||||
|
||||
pub fn create_reserve(
|
||||
client: &mut RpcClient,
|
||||
config: ReserveConfig,
|
||||
lending_market_pubkey: Pubkey,
|
||||
lending_market_owner: &Keypair,
|
||||
dex_market_pubkey: Option<Pubkey>,
|
||||
liquidity_source_pubkey: Pubkey,
|
||||
payer: &Keypair,
|
||||
) -> (Pubkey, Reserve) {
|
||||
let reserve_keypair = Keypair::new();
|
||||
let reserve_pubkey = reserve_keypair.pubkey();
|
||||
let collateral_mint_keypair = Keypair::new();
|
||||
let collateral_supply_keypair = Keypair::new();
|
||||
let collateral_fees_receiver_keypair = Keypair::new();
|
||||
let liquidity_supply_keypair = Keypair::new();
|
||||
let user_collateral_token_keypair = Keypair::new();
|
||||
let user_transfer_authority = Keypair::new();
|
||||
|
||||
let liquidity_source_account = client.get_account(&liquidity_source_pubkey).unwrap();
|
||||
let liquidity_source_token = Token::unpack(&liquidity_source_account.data).unwrap();
|
||||
let liquidity_mint_pubkey = liquidity_source_token.mint;
|
||||
|
||||
let recent_blockhash = client.get_recent_blockhash().unwrap().0;
|
||||
let token_balance = client
|
||||
.get_minimum_balance_for_rent_exemption(Token::LEN)
|
||||
.unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&collateral_mint_keypair.pubkey(),
|
||||
client
|
||||
.get_minimum_balance_for_rent_exemption(Mint::LEN)
|
||||
.unwrap(),
|
||||
Mint::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&collateral_supply_keypair.pubkey(),
|
||||
token_balance,
|
||||
Token::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&collateral_fees_receiver_keypair.pubkey(),
|
||||
token_balance,
|
||||
Token::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&liquidity_supply_keypair.pubkey(),
|
||||
token_balance,
|
||||
Token::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&user_collateral_token_keypair.pubkey(),
|
||||
token_balance,
|
||||
Token::LEN as u64,
|
||||
&spl_token::id(),
|
||||
),
|
||||
create_account(
|
||||
&payer.pubkey(),
|
||||
&reserve_pubkey,
|
||||
client
|
||||
.get_minimum_balance_for_rent_exemption(Reserve::LEN)
|
||||
.unwrap(),
|
||||
Reserve::LEN as u64,
|
||||
&id(),
|
||||
),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&vec![
|
||||
payer,
|
||||
&reserve_keypair,
|
||||
&collateral_mint_keypair,
|
||||
&collateral_supply_keypair,
|
||||
&liquidity_supply_keypair,
|
||||
&user_collateral_token_keypair,
|
||||
],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
client.send_and_confirm_transaction(&transaction).unwrap();
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
approve(
|
||||
&spl_token::id(),
|
||||
&liquidity_source_pubkey,
|
||||
&user_transfer_authority.pubkey(),
|
||||
&payer.pubkey(),
|
||||
&[],
|
||||
liquidity_source_token.amount,
|
||||
)
|
||||
.unwrap(),
|
||||
init_reserve(
|
||||
id(),
|
||||
liquidity_source_token.amount,
|
||||
config,
|
||||
liquidity_source_pubkey,
|
||||
user_collateral_token_keypair.pubkey(),
|
||||
reserve_pubkey,
|
||||
liquidity_mint_pubkey,
|
||||
liquidity_supply_keypair.pubkey(),
|
||||
collateral_mint_keypair.pubkey(),
|
||||
collateral_supply_keypair.pubkey(),
|
||||
collateral_fees_receiver_keypair.pubkey(),
|
||||
lending_market_pubkey,
|
||||
lending_market_owner.pubkey(),
|
||||
user_transfer_authority.pubkey(),
|
||||
dex_market_pubkey,
|
||||
),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(
|
||||
&vec![payer, &lending_market_owner, &user_transfer_authority],
|
||||
recent_blockhash,
|
||||
);
|
||||
|
||||
client.send_and_confirm_transaction(&transaction).unwrap();
|
||||
|
||||
let account = client.get_account(&reserve_pubkey).unwrap();
|
||||
(reserve_pubkey, Reserve::unpack(&account.data).unwrap())
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
# Flash Loan Design
|
||||
|
||||
We added a new instruction with the following signature for flash loan:
|
||||
```rust
|
||||
pub enum LendingInstruction {
|
||||
// ....
|
||||
/// Make a flash loan.
|
||||
///
|
||||
/// Accounts expected by this instruction:
|
||||
///
|
||||
/// 0. `[writable]` Source liquidity token account.
|
||||
/// Minted by reserve liquidity mint.
|
||||
/// Must match the reserve liquidity supply.
|
||||
/// 1. `[writable]` Destination liquidity token account.
|
||||
/// Minted by reserve liquidity mint.
|
||||
/// 2. `[writable]` Reserve account.
|
||||
/// 3. `[]` Lending market account.
|
||||
/// 4. `[]` Derived lending market authority.
|
||||
/// 5. `[]` Flash loan receiver program account.
|
||||
/// Must implement an instruction that has tag of 0 and a signature of `(repay_amount: u64)`
|
||||
/// This instruction must return the amount to the source liquidity account.
|
||||
/// 6. `[]` Token program id.
|
||||
/// 7. `[writable]` Flash loan fee receiver account.
|
||||
/// Must match the reserve liquidity fee receiver.
|
||||
/// 8. `[writable]` Host fee receiver.
|
||||
/// .. `[any]` Additional accounts expected by the receiving program's `ReceiveFlashLoan` instruction.
|
||||
FlashLoan {
|
||||
/// The amount that is to be borrowed
|
||||
amount: u64,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
In the implementation, we do the following in order:
|
||||
|
||||
1. Perform safety checks and calculate fees
|
||||
2. Transfer `amount` from the source liquidity account to the destination liquidity account
|
||||
2. Call the `ReceiveFlashLoan` function (the flash loan receiver program is required to have this function with tag `0`).
|
||||
The additional account required for `ReceiveFlashLoan` is given from the 10th account of the `FlashLoan` instruction, i.e. after host fee receiver.
|
||||
3. Check that the returned amount with the fee is in the reserve account after the completion of `ReceiveFlashLoan` function.
|
||||
|
||||
The flash loan receiver program should have a `ReceiveFlashLoan` instruction which executes the user-defined operation and return the funds to the reserve in the end.
|
||||
|
||||
```rust
|
||||
pub enum FlashLoanReceiverInstruction {
|
||||
|
||||
/// Receive a flash loan and perform user-defined operation and finally return the fund back.
|
||||
///
|
||||
/// Accounts expected:
|
||||
///
|
||||
/// 0. `[writable]` Source liquidity (matching the destination from above).
|
||||
/// 1. `[writable]` Destination liquidity (matching the source from above).
|
||||
/// 2. `[]` Token program id
|
||||
/// .. `[any]` Additional accounts provided to the lending program's `FlashLoan` instruction above.
|
||||
ReceiveFlashLoan {
|
||||
// Amount that is loaned to the receiver program
|
||||
amount: u64
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
You can view a sample implementation [here](https://github.com/solana-labs/solana-program-library/tree/master/token-lending/program/tests/helpers/flash_loan_receiver.rs).
|
|
@ -30,7 +30,7 @@ export const LendingMarketLayout: typeof BufferLayout.Structure = BufferLayout.s
|
|||
Layout.publicKey("owner"),
|
||||
Layout.publicKey("quoteTokenMint"),
|
||||
Layout.publicKey("tokenProgramId"),
|
||||
BufferLayout.blob(62, "padding"),
|
||||
BufferLayout.blob(128, "padding"),
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -38,6 +38,19 @@
|
|||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
|
@ -224,13 +237,18 @@
|
|||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@solana/web3.js": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.2.5.tgz",
|
||||
"integrity": "sha512-dkKIDhmSM9qX5eJs4bNWPLMLzWcSCd36Imj4IIIfJHKwMfhjO6sWfYjHD4dzB502GoqpvZ/jHv4JBxZR5ERSiA==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.9.1.tgz",
|
||||
"integrity": "sha512-fiHSCtV4hSmWJpbS+jsrdWeKtHAQWjEflDcxLG7qcu/BFO/npCETNPZl1c6yUDBdvPyfUwvGiQqKNDip4yRoqA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"bn.js": "^5.0.0",
|
||||
|
@ -271,9 +289,9 @@
|
|||
}
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "7.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.8.tgz",
|
||||
"integrity": "sha512-RTKvBsfz0T8CKOGZMfuluDNyMFHnu5lvNr4hWEsQeHXH6FcmIDIozOyWMh36nLGMwVd5UFNXC2xztA8lln22MQ==",
|
||||
"version": "7.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-7.2.10.tgz",
|
||||
"integrity": "sha512-kUEPnMKrqbtpCq/KTaGFFKAcz6Ethm2EjCoKIDaCmfRBWLbFuTcOJfTlorwbnboXBzahqWLgUp1BQeKHiJzPUQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/estree": "*",
|
||||
|
@ -335,9 +353,9 @@
|
|||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.37",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz",
|
||||
"integrity": "sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw=="
|
||||
"version": "15.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.2.tgz",
|
||||
"integrity": "sha512-p68+a+KoxpoB47015IeYZYRrdqMUcpbK8re/zpFB8Ld46LHC1lPEbp3EXgkEhAYEcPvjJF6ZO+869SQ0aH1dcA=="
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.2.3",
|
||||
|
@ -387,82 +405,71 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.20.0.tgz",
|
||||
"integrity": "sha512-sw+3HO5aehYqn5w177z2D82ZQlqHCwcKSMboueo7oE4KU9QiC0SAgfS/D4z9xXvpTc8Bt41Raa9fBR8T2tIhoQ==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.22.1.tgz",
|
||||
"integrity": "sha512-kVTAghWDDhsvQ602tHBc6WmQkdaYbkcTwZu+7l24jtJiYvm9l+/y/b2BZANEezxPDiX5MK2ZecE+9BFi/YJryw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.20.0",
|
||||
"@typescript-eslint/scope-manager": "4.20.0",
|
||||
"@typescript-eslint/experimental-utils": "4.22.1",
|
||||
"@typescript-eslint/scope-manager": "4.22.1",
|
||||
"debug": "^4.1.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"regexpp": "^3.0.0",
|
||||
"semver": "^7.3.2",
|
||||
"tsutils": "^3.17.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.20.0.tgz",
|
||||
"integrity": "sha512-sQNlf6rjLq2yB5lELl3gOE7OuoA/6IVXJUJ+Vs7emrQMva14CkOwyQwD7CW+TkmOJ4Q/YGmoDLmbfFrpGmbKng==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.22.1.tgz",
|
||||
"integrity": "sha512-svYlHecSMCQGDO2qN1v477ax/IDQwWhc7PRBiwAdAMJE7GXk5stF4Z9R/8wbRkuX/5e9dHqbIWxjeOjckK3wLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/scope-manager": "4.20.0",
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/typescript-estree": "4.20.0",
|
||||
"@typescript-eslint/scope-manager": "4.22.1",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/typescript-estree": "4.22.1",
|
||||
"eslint-scope": "^5.0.0",
|
||||
"eslint-utils": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.20.0.tgz",
|
||||
"integrity": "sha512-m6vDtgL9EABdjMtKVw5rr6DdeMCH3OA1vFb0dAyuZSa3e5yw1YRzlwFnm9knma9Lz6b2GPvoNSa8vOXrqsaglA==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.22.1.tgz",
|
||||
"integrity": "sha512-l+sUJFInWhuMxA6rtirzjooh8cM/AATAe3amvIkqKFeMzkn85V+eLzb1RyuXkHak4dLfYzOmF6DXPyflJvjQnw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.20.0",
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/typescript-estree": "4.20.0",
|
||||
"@typescript-eslint/scope-manager": "4.22.1",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/typescript-estree": "4.22.1",
|
||||
"debug": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz",
|
||||
"integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz",
|
||||
"integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/visitor-keys": "4.20.0"
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/visitor-keys": "4.22.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz",
|
||||
"integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz",
|
||||
"integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz",
|
||||
"integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz",
|
||||
"integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/visitor-keys": "4.20.0",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/visitor-keys": "4.22.1",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
|
@ -471,88 +478,68 @@
|
|||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz",
|
||||
"integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz",
|
||||
"integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.20.0.tgz",
|
||||
"integrity": "sha512-/zm6WR6iclD5HhGpcwl/GOYDTzrTHmvf8LLLkwKqqPKG6+KZt/CfSgPCiybshmck66M2L5fWSF/MKNuCwtKQSQ==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.22.1.tgz",
|
||||
"integrity": "sha512-d5bAiPBiessSmNi8Amq/RuLslvcumxLmyhf1/Xa9IuaoFJ0YtshlJKxhlbY7l2JdEk3wS0EnmnfeJWSvADOe0g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/visitor-keys": "4.20.0"
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/visitor-keys": "4.22.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.20.0.tgz",
|
||||
"integrity": "sha512-cYY+1PIjei1nk49JAPnH1VEnu7OYdWRdJhYI5wiKOUMhLTG1qsx5cQxCUTuwWCmQoyriadz3Ni8HZmGSofeC+w==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.22.1.tgz",
|
||||
"integrity": "sha512-2HTkbkdAeI3OOcWbqA8hWf/7z9c6gkmnWNGz0dKSLYLWywUlkOAQ2XcjhlKLj5xBFDf8FgAOF5aQbnLRvgNbCw==",
|
||||
"dev": true
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.20.0.tgz",
|
||||
"integrity": "sha512-Knpp0reOd4ZsyoEJdW8i/sK3mtZ47Ls7ZHvD8WVABNx5Xnn7KhenMTRGegoyMTx6TiXlOVgMz9r0pDgXTEEIHA==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.22.1.tgz",
|
||||
"integrity": "sha512-p3We0pAPacT+onSGM+sPR+M9CblVqdA9F1JEdIqRVlxK5Qth4ochXQgIyb9daBomyQKAXbygxp1aXQRV0GC79A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/visitor-keys": "4.20.0",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"@typescript-eslint/visitor-keys": "4.22.1",
|
||||
"debug": "^4.1.1",
|
||||
"globby": "^11.0.1",
|
||||
"is-glob": "^4.0.1",
|
||||
"semver": "^7.3.2",
|
||||
"tsutils": "^3.17.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.20.0.tgz",
|
||||
"integrity": "sha512-NXKRM3oOVQL8yNFDNCZuieRIwZ5UtjNLYtmMx2PacEAGmbaEYtGgVHUHVyZvU/0rYZcizdrWjDo+WBtRPSgq+A==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.22.1.tgz",
|
||||
"integrity": "sha512-WPkOrIRm+WCLZxXQHCi+WG8T2MMTUFR70rWjdWYddLT7cEfb2P4a3O/J2U1FBVsSFTocXLCoXWY6MZGejeStvQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.20.0",
|
||||
"@typescript-eslint/types": "4.22.1",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
|
||||
"integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
|
@ -778,16 +765,6 @@
|
|||
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
|
||||
"dev": true
|
||||
},
|
||||
"call-bind": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz",
|
||||
"integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"get-intrinsic": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
|
@ -795,14 +772,54 @@
|
|||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
|
||||
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.1",
|
||||
"escape-string-regexp": "^1.0.5",
|
||||
"supports-color": "^5.3.0"
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"check-more-types": {
|
||||
|
@ -953,9 +970,10 @@
|
|||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.0.tgz",
|
||||
"integrity": "sha512-yy3x9XjojW8ROTBePD25AcMoHqGHsvHmtfw8QWlpEXyMMXXPj6brUA464AptUvHuTPRmNz6Sd3ZLNLeJl6dHJA==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexer": {
|
||||
"version": "0.1.2",
|
||||
|
@ -1028,9 +1046,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint": {
|
||||
"version": "7.23.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.23.0.tgz",
|
||||
"integrity": "sha512-kqvNVbdkjzpFy0XOszNwjkKzZ+6TcwCQ/h+ozlcIWwaimBBuhlQ4nN6kbiM2L+OjDcznkTJxzYfRFH92sx4a0Q==",
|
||||
"version": "7.25.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-7.25.0.tgz",
|
||||
"integrity": "sha512-TVpSovpvCNpLURIScDRB6g5CYu/ZFq9GfX2hLNIV4dSBKxIWojeDODvYl3t0k0VtMxYeR8OXPCFE5+oHMlGfhw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "7.12.11",
|
||||
|
@ -1072,75 +1090,17 @@
|
|||
"v8-compile-cache": "^2.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz",
|
||||
"integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1151,9 +1111,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz",
|
||||
"integrity": "sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.0.tgz",
|
||||
"integrity": "sha512-UDK6rJT6INSfcOo545jiaOwB701uAIt2/dR7WnFQoGCVl1/EMqdANBmwUaqqQ45aXprsTGzSa39LI1PyuRBxxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
|
@ -1446,17 +1406,6 @@
|
|||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
|
||||
"integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"get-prototype-of": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/get-prototype-of/-/get-prototype-of-0.0.0.tgz",
|
||||
|
@ -1495,9 +1444,9 @@
|
|||
}
|
||||
},
|
||||
"globals": {
|
||||
"version": "13.7.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.7.0.tgz",
|
||||
"integrity": "sha512-Aipsz6ZKRxa/xQkZhNg0qIWXT6x6rD46f6x/PCnBomlttdIyAPak4YD9jTmKpZ72uROSMU87qJtcgpgHaVchiA==",
|
||||
"version": "13.8.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-13.8.0.tgz",
|
||||
"integrity": "sha512-rHtdA6+PDBIjeEvA91rpqzEvk/k3/i7EeNQiryiWuJH0Hw9cpyJMAt2jtbAwUaRdhD+573X4vWw6IcjKPasi9Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"type-fest": "^0.20.2"
|
||||
|
@ -1554,12 +1503,6 @@
|
|||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz",
|
||||
"integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==",
|
||||
"dev": true
|
||||
},
|
||||
"hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
|
@ -1627,15 +1570,6 @@
|
|||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
},
|
||||
"is-boolean-object": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz",
|
||||
"integrity": "sha512-a7Uprx8UtD+HWdyYwnD1+ExtTgqQtD2k/1yJgtXP6wnMm8byhkoTZRl+95LLThpzNZJ5aEvi46cdH+ayMFRwmA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"call-bind": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-capitalized": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-capitalized/-/is-capitalized-1.0.0.tgz",
|
||||
|
@ -1688,12 +1622,6 @@
|
|||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"is-number-object": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.4.tgz",
|
||||
"integrity": "sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw==",
|
||||
"dev": true
|
||||
},
|
||||
"is-reference": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
|
||||
|
@ -1709,12 +1637,6 @@
|
|||
"integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
|
||||
"dev": true
|
||||
},
|
||||
"is-string": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz",
|
||||
"integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==",
|
||||
"dev": true
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
@ -2227,9 +2149,9 @@
|
|||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.44.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.44.0.tgz",
|
||||
"integrity": "sha512-rGSF4pLwvuaH/x4nAS+zP6UNn5YUDWf/TeEU5IoXSZKBbKRNTCI3qMnYXKZgrC0D2KzS2baiOZt1OlqhMu5rnQ==",
|
||||
"version": "2.47.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.47.0.tgz",
|
||||
"integrity": "sha512-rqBjgq9hQfW0vRmz+0S062ORRNJXvwRpzxhFXORvar/maZqY6za3rgQ/p1Glg+j1hnc1GtYyQCPiAei95uTElg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fsevents": "~2.3.1"
|
||||
|
@ -2383,6 +2305,15 @@
|
|||
"node-gyp-build": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "7.3.5",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
|
||||
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
@ -2576,26 +2507,24 @@
|
|||
}
|
||||
},
|
||||
"table": {
|
||||
"version": "6.0.9",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.0.9.tgz",
|
||||
"integrity": "sha512-F3cLs9a3hL1Z7N4+EkSscsel3z55XT950AvB05bwayrNg5T1/gykXtigioTAjbltvbMSJvvhFCbnf6mX+ntnJQ==",
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/table/-/table-6.5.1.tgz",
|
||||
"integrity": "sha512-xGDXWTBJxahkzPQCsn1S9ESHEenU7TbMD5Iv4FeopXv/XwJyWatFjfbor+6ipI10/MNPXBYUamYukOrbPZ9L/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ajv": "^8.0.1",
|
||||
"is-boolean-object": "^1.1.0",
|
||||
"is-number-object": "^1.0.4",
|
||||
"is-string": "^1.0.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.flatten": "^4.4.0",
|
||||
"lodash.truncate": "^4.4.2",
|
||||
"slice-ansi": "^4.0.0",
|
||||
"string-width": "^4.2.0"
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ajv": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.0.1.tgz",
|
||||
"integrity": "sha512-46ZA4TalFcLLqX1dEU3dhdY38wAtDydJ4e7QQTVekLUTzXkb1LfqU6VOBXC/a9wiv4T094WURqJH6ZitF92Kqw==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.1.0.tgz",
|
||||
"integrity": "sha512-B/Sk2Ix7A36fs/ZkuGLIR86EdjbgR6fsAcbx9lOP/QBSXujDNbVmIS/U4Itz5k8fPFDeVZl/zQ/gJW4Jrq6XjQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
|
@ -2687,9 +2616,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz",
|
||||
"integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==",
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz",
|
||||
"integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==",
|
||||
"dev": true
|
||||
},
|
||||
"universalify": {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@solana/spl-token": "0.1.3",
|
||||
"@solana/web3.js": "^1.2.5",
|
||||
"@solana/web3.js": "^1.9.1",
|
||||
"bn.js": "^5.2.0",
|
||||
"mkdirp": "^1.0.4"
|
||||
},
|
||||
|
@ -43,28 +43,28 @@
|
|||
"@rollup/plugin-commonjs": "^18.0.0",
|
||||
"@tsconfig/recommended": "^1.0.1",
|
||||
"@types/bn.js": "^5.1.0",
|
||||
"@types/eslint": "^7.2.8",
|
||||
"@types/eslint": "^7.2.10",
|
||||
"@types/eslint-plugin-prettier": "^3.1.0",
|
||||
"@types/mkdirp": "^1.0.1",
|
||||
"@types/mz": "^2.7.3",
|
||||
"@types/node": "^14.14.37",
|
||||
"@types/node": "^15.0.2",
|
||||
"@types/prettier": "^2.2.3",
|
||||
"@types/rollup-plugin-json": "^3.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^4.20.0",
|
||||
"@typescript-eslint/parser": "^4.20.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"eslint": "^7.23.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.1",
|
||||
"@typescript-eslint/parser": "^4.22.1",
|
||||
"dotenv": "^9.0.0",
|
||||
"eslint": "^7.25.0",
|
||||
"eslint-config-prettier": "^7.2.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"prettier": "^2.2.1",
|
||||
"rollup": "^2.44.0",
|
||||
"rollup": "^2.47.0",
|
||||
"rollup-plugin-json": "^4.0.0",
|
||||
"rollup-plugin-node-resolve": "^5.2.0",
|
||||
"rollup-plugin-sourcemaps": "^0.6.3",
|
||||
"rollup-plugin-typescript2": "^0.30.0",
|
||||
"start-server-and-test": "^1.11.6",
|
||||
"ts-node": "^9.1.1",
|
||||
"typescript": "^4.2.3"
|
||||
"typescript": "^4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
|
|
|
@ -14,10 +14,10 @@ test-dump-genesis-accounts = []
|
|||
|
||||
[dependencies]
|
||||
arrayref = "0.3.6"
|
||||
flux-aggregator = { git = "https://github.com/octopus-network/solana-flux-aggregator", rev = "9cfaec5", features = ["no-entrypoint"] }
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
serum_dex = { git = "https://github.com/project-serum/serum-dex", rev = "991a86e", features = ["no-entrypoint"] }
|
||||
solana-program = "1.6.2"
|
||||
solana-program = "1.6.7"
|
||||
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||
thiserror = "1.0"
|
||||
uint = "0.8"
|
||||
|
@ -27,8 +27,8 @@ assert_matches = "1.5.0"
|
|||
base64 = "0.13"
|
||||
log = "0.4.14"
|
||||
proptest = "0.10"
|
||||
solana-program-test = "1.6.2"
|
||||
solana-sdk = "1.6.2"
|
||||
solana-program-test = "1.6.7"
|
||||
solana-sdk = "1.6.7"
|
||||
serde = "1.0"
|
||||
serde_yaml = "0.8"
|
||||
|
||||
|
|
|
@ -1,345 +0,0 @@
|
|||
//! Dex market used for simulating trades
|
||||
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
math::{Decimal, TryAdd, TryDiv, TryMul, TrySub},
|
||||
state::TokenConverter,
|
||||
};
|
||||
use arrayref::{array_refs, mut_array_refs};
|
||||
use serum_dex::critbit::{Slab, SlabView};
|
||||
use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey};
|
||||
use std::{cell::RefMut, convert::TryFrom};
|
||||
|
||||
/// Side of the dex market order book
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Side {
|
||||
Bid,
|
||||
Ask,
|
||||
}
|
||||
|
||||
/// Market currency
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
enum Currency {
|
||||
Base,
|
||||
Quote,
|
||||
}
|
||||
|
||||
impl Currency {
|
||||
fn opposite(&self) -> Self {
|
||||
match self {
|
||||
Currency::Base => Currency::Quote,
|
||||
Currency::Quote => Currency::Base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trade action for trade simulator
|
||||
#[derive(PartialEq)]
|
||||
pub enum TradeAction {
|
||||
/// Sell tokens
|
||||
Sell,
|
||||
/// Buy tokens
|
||||
Buy,
|
||||
}
|
||||
|
||||
/// Dex market order
|
||||
struct Order {
|
||||
price: u64,
|
||||
quantity: u64,
|
||||
}
|
||||
|
||||
/// Trade simulator
|
||||
pub struct TradeSimulator<'a> {
|
||||
dex_market: DexMarket,
|
||||
orders: DexMarketOrders<'a>,
|
||||
orders_side: Side,
|
||||
quote_token_mint: &'a Pubkey,
|
||||
buy_token_mint: &'a Pubkey,
|
||||
sell_token_mint: &'a Pubkey,
|
||||
}
|
||||
|
||||
impl<'a> TokenConverter for TradeSimulator<'a> {
|
||||
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError> {
|
||||
let action = if token_mint == self.buy_token_mint {
|
||||
TradeAction::Buy
|
||||
} else {
|
||||
TradeAction::Sell
|
||||
};
|
||||
|
||||
let currency = if token_mint == self.quote_token_mint {
|
||||
Currency::Quote
|
||||
} else {
|
||||
Currency::Base
|
||||
};
|
||||
|
||||
let order_book_side = match (action, currency) {
|
||||
(TradeAction::Buy, Currency::Base) => Side::Ask,
|
||||
(TradeAction::Sell, Currency::Quote) => Side::Ask,
|
||||
(TradeAction::Buy, Currency::Quote) => Side::Bid,
|
||||
(TradeAction::Sell, Currency::Base) => Side::Bid,
|
||||
};
|
||||
if order_book_side != self.orders_side {
|
||||
return Err(LendingError::DexInvalidOrderBookSide.into());
|
||||
}
|
||||
|
||||
let best_order_price = self
|
||||
.orders
|
||||
.best_order_price()
|
||||
.ok_or(LendingError::TradeSimulationError)?;
|
||||
|
||||
let input_token = Decimal::one().try_div(self.dex_market.get_lots(currency))?;
|
||||
let output_token_price = if currency == Currency::Base {
|
||||
input_token.try_mul(best_order_price)
|
||||
} else {
|
||||
input_token.try_div(best_order_price)
|
||||
}?;
|
||||
output_token_price.try_mul(self.dex_market.get_lots(currency.opposite()))
|
||||
}
|
||||
|
||||
fn convert(
|
||||
self,
|
||||
from_amount: Decimal,
|
||||
from_token_mint: &Pubkey,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let action = if from_token_mint == self.buy_token_mint {
|
||||
TradeAction::Buy
|
||||
} else {
|
||||
TradeAction::Sell
|
||||
};
|
||||
|
||||
self.simulate_trade(action, from_amount)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TradeSimulator<'a> {
|
||||
/// Create a new TradeSimulator
|
||||
pub fn new(
|
||||
dex_market_info: &AccountInfo,
|
||||
dex_market_orders: &AccountInfo,
|
||||
memory: &'a AccountInfo,
|
||||
quote_token_mint: &'a Pubkey,
|
||||
buy_token_mint: &'a Pubkey,
|
||||
sell_token_mint: &'a Pubkey,
|
||||
) -> Result<Self, ProgramError> {
|
||||
let dex_market = DexMarket::new(dex_market_info);
|
||||
let orders = DexMarketOrders::new(&dex_market, dex_market_orders, memory)?;
|
||||
let orders_side = orders.side;
|
||||
|
||||
Ok(Self {
|
||||
dex_market,
|
||||
orders,
|
||||
orders_side,
|
||||
quote_token_mint,
|
||||
buy_token_mint,
|
||||
sell_token_mint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Simulate a trade
|
||||
pub fn simulate_trade(
|
||||
mut self,
|
||||
action: TradeAction,
|
||||
quantity: Decimal,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let token_mint = match action {
|
||||
TradeAction::Buy => self.buy_token_mint,
|
||||
TradeAction::Sell => self.sell_token_mint,
|
||||
};
|
||||
|
||||
let currency = if token_mint == self.quote_token_mint {
|
||||
Currency::Quote
|
||||
} else {
|
||||
Currency::Base
|
||||
};
|
||||
|
||||
let order_book_side = match (action, currency) {
|
||||
(TradeAction::Buy, Currency::Base) => Side::Ask,
|
||||
(TradeAction::Sell, Currency::Quote) => Side::Ask,
|
||||
(TradeAction::Buy, Currency::Quote) => Side::Bid,
|
||||
(TradeAction::Sell, Currency::Base) => Side::Bid,
|
||||
};
|
||||
|
||||
if order_book_side != self.orders_side {
|
||||
return Err(LendingError::DexInvalidOrderBookSide.into());
|
||||
}
|
||||
|
||||
let input_quantity: Decimal = quantity.try_div(self.dex_market.get_lots(currency))?;
|
||||
let output_quantity = self.exchange_with_order_book(input_quantity, currency)?;
|
||||
output_quantity.try_mul(self.dex_market.get_lots(currency.opposite()))
|
||||
}
|
||||
|
||||
/// Exchange tokens by filling orders
|
||||
fn exchange_with_order_book(
|
||||
&mut self,
|
||||
mut input_quantity: Decimal,
|
||||
currency: Currency,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let mut output_quantity = Decimal::zero();
|
||||
|
||||
let zero = Decimal::zero();
|
||||
while input_quantity > zero {
|
||||
let next_order = self
|
||||
.orders
|
||||
.next()
|
||||
.ok_or_else(|| ProgramError::from(LendingError::TradeSimulationError))?;
|
||||
|
||||
let next_order_price = next_order.price;
|
||||
let base_quantity = next_order.quantity;
|
||||
|
||||
let (filled, output) = if currency == Currency::Base {
|
||||
let filled = input_quantity.min(Decimal::from(base_quantity));
|
||||
(filled, filled.try_mul(next_order_price)?)
|
||||
} else {
|
||||
let quote_quantity = Decimal::from(base_quantity).try_mul(next_order_price)?;
|
||||
let filled = input_quantity.min(quote_quantity);
|
||||
(filled, filled.try_div(next_order_price)?)
|
||||
};
|
||||
|
||||
input_quantity = input_quantity.try_sub(filled)?;
|
||||
output_quantity = output_quantity.try_add(output)?;
|
||||
}
|
||||
|
||||
Ok(output_quantity)
|
||||
}
|
||||
}
|
||||
|
||||
/// Dex market order account info
|
||||
struct DexMarketOrders<'a> {
|
||||
heap: Option<RefMut<'a, Slab>>,
|
||||
side: Side,
|
||||
}
|
||||
|
||||
impl<'a> DexMarketOrders<'a> {
|
||||
/// Create a new DexMarketOrders
|
||||
fn new(
|
||||
dex_market: &DexMarket,
|
||||
orders: &AccountInfo,
|
||||
memory: &'a AccountInfo,
|
||||
) -> Result<Self, ProgramError> {
|
||||
let side = match orders.key {
|
||||
key if key == &dex_market.bids => Side::Bid,
|
||||
key if key == &dex_market.asks => Side::Ask,
|
||||
_ => return Err(LendingError::DexInvalidOrderBookSide.into()),
|
||||
};
|
||||
|
||||
if memory.data_len() < orders.data_len() {
|
||||
return Err(LendingError::MemoryTooSmall.into());
|
||||
}
|
||||
|
||||
let mut memory_data = memory.data.borrow_mut();
|
||||
fast_copy(&orders.data.borrow(), &mut memory_data);
|
||||
let heap = Some(RefMut::map(memory_data, |bytes| {
|
||||
// strip padding and header
|
||||
let start = 5 + 8;
|
||||
let end = bytes.len() - 7;
|
||||
Slab::new(&mut bytes[start..end])
|
||||
}));
|
||||
|
||||
Ok(Self { heap, side })
|
||||
}
|
||||
|
||||
fn best_order_price(&mut self) -> Option<u64> {
|
||||
let side = self.side;
|
||||
self.heap.as_mut().and_then(|heap| {
|
||||
let handle = match side {
|
||||
Side::Bid => heap.find_max(),
|
||||
Side::Ask => heap.find_min(),
|
||||
}?;
|
||||
|
||||
Some(heap.get_mut(handle)?.as_leaf_mut()?.price().get())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for DexMarketOrders<'_> {
|
||||
type Item = Order;
|
||||
|
||||
fn next(&mut self) -> Option<Order> {
|
||||
let leaf_node = match self.side {
|
||||
Side::Bid => self.heap.as_mut().and_then(|heap| heap.remove_max()),
|
||||
Side::Ask => self.heap.as_mut().and_then(|heap| heap.remove_min()),
|
||||
}?;
|
||||
|
||||
Some(Order {
|
||||
price: leaf_node.price().get(),
|
||||
quantity: leaf_node.quantity(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Offset for dex market base mint
|
||||
pub const BASE_MINT_OFFSET: usize = 6;
|
||||
/// Offset for dex market quote mint
|
||||
pub const QUOTE_MINT_OFFSET: usize = 10;
|
||||
|
||||
const BIDS_OFFSET: usize = 35;
|
||||
const ASKS_OFFSET: usize = 39;
|
||||
|
||||
/// Dex market info
|
||||
pub struct DexMarket {
|
||||
bids: Pubkey,
|
||||
asks: Pubkey,
|
||||
base_lots: u64,
|
||||
quote_lots: u64,
|
||||
}
|
||||
|
||||
impl DexMarket {
|
||||
/// Create a new DexMarket
|
||||
fn new(dex_market_info: &AccountInfo) -> Self {
|
||||
let dex_market_data = dex_market_info.data.borrow();
|
||||
let bids = Self::pubkey_at_offset(&dex_market_data, BIDS_OFFSET);
|
||||
let asks = Self::pubkey_at_offset(&dex_market_data, ASKS_OFFSET);
|
||||
let base_lots = Self::base_lots(&dex_market_data);
|
||||
let quote_lots = Self::quote_lots(&dex_market_data);
|
||||
|
||||
Self {
|
||||
bids,
|
||||
asks,
|
||||
base_lots,
|
||||
quote_lots,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_lots(&self, currency: Currency) -> u64 {
|
||||
match currency {
|
||||
Currency::Base => self.base_lots,
|
||||
Currency::Quote => self.quote_lots,
|
||||
}
|
||||
}
|
||||
|
||||
fn base_lots(data: &[u8]) -> u64 {
|
||||
let count_start = 5 + 43 * 8;
|
||||
let count_end = count_start + 8;
|
||||
u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap())
|
||||
}
|
||||
|
||||
fn quote_lots(data: &[u8]) -> u64 {
|
||||
let count_start = 5 + 44 * 8;
|
||||
let count_end = count_start + 8;
|
||||
u64::from_le_bytes(<[u8; 8]>::try_from(&data[count_start..count_end]).unwrap())
|
||||
}
|
||||
|
||||
/// Get pubkey located at offset
|
||||
pub fn pubkey_at_offset(data: &[u8], offset: usize) -> Pubkey {
|
||||
let count_start = 5 + offset * 8;
|
||||
let count_end = count_start + 32;
|
||||
Pubkey::new(&data[count_start..count_end])
|
||||
}
|
||||
}
|
||||
|
||||
/// A more efficient `copy_from_slice` implementation.
|
||||
fn fast_copy(mut src: &[u8], mut dst: &mut [u8]) {
|
||||
const COPY_SIZE: usize = 512;
|
||||
while src.len() >= COPY_SIZE {
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (src_word, src_rem) = array_refs![src, COPY_SIZE; ..;];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (dst_word, dst_rem) = mut_array_refs![dst, COPY_SIZE; ..;];
|
||||
*dst_word = *src_word;
|
||||
src = src_rem;
|
||||
dst = dst_rem;
|
||||
}
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), src.len());
|
||||
}
|
||||
}
|
|
@ -31,17 +31,20 @@ pub enum LendingError {
|
|||
/// The owner of the account input isn't set to the correct token program id.
|
||||
#[error("Input token account is not owned by the correct token program id")]
|
||||
InvalidTokenOwner,
|
||||
/// Expected an SPL Token account
|
||||
#[error("Input token account is not valid")]
|
||||
InvalidTokenAccount,
|
||||
/// Expected an SPL Token mint
|
||||
#[error("Input token mint account is not valid")]
|
||||
InvalidTokenMint,
|
||||
/// Expected a different SPL Token program
|
||||
#[error("Input token program account is not valid")]
|
||||
InvalidTokenProgram,
|
||||
|
||||
// 10
|
||||
/// Invalid amount, must be greater than zero
|
||||
#[error("Input amount is invalid")]
|
||||
InvalidAmount,
|
||||
|
||||
// 10
|
||||
/// Invalid config value
|
||||
#[error("Input config value is invalid")]
|
||||
InvalidConfig,
|
||||
|
@ -54,62 +57,8 @@ pub enum LendingError {
|
|||
/// Math operation overflow
|
||||
#[error("Math operation overflow")]
|
||||
MathOverflow,
|
||||
/// Negative interest rate
|
||||
#[error("Interest rate is negative")]
|
||||
NegativeInterestRate,
|
||||
|
||||
// 15
|
||||
/// Memory is too small
|
||||
#[error("Memory is too small")]
|
||||
MemoryTooSmall,
|
||||
/// The reserve lending market must be the same
|
||||
#[error("Reserve mints do not match dex market mints")]
|
||||
DexMarketMintMismatch,
|
||||
/// The reserve lending market must be the same
|
||||
#[error("Reserve lending market mismatch")]
|
||||
LendingMarketMismatch,
|
||||
/// The obligation token owner must be the same if reusing an obligation
|
||||
#[error("Obligation token owner mismatch")]
|
||||
ObligationTokenOwnerMismatch,
|
||||
/// Insufficient liquidity available
|
||||
#[error("Insufficient liquidity available")]
|
||||
InsufficientLiquidity,
|
||||
|
||||
// 20
|
||||
/// This reserve's collateral cannot be used for borrows
|
||||
#[error("Input reserve has collateral disabled")]
|
||||
ReserveCollateralDisabled,
|
||||
/// Input reserves cannot be the same
|
||||
#[error("Input reserves cannot be the same")]
|
||||
DuplicateReserve,
|
||||
/// Input reserves cannot use the same liquidity mint
|
||||
#[error("Input reserves cannot use the same liquidity mint")]
|
||||
DuplicateReserveMint,
|
||||
/// Obligation amount is empty
|
||||
#[error("Obligation amount is empty")]
|
||||
ObligationEmpty,
|
||||
/// Cannot liquidate healthy obligations
|
||||
#[error("Cannot liquidate healthy obligations")]
|
||||
HealthyObligation,
|
||||
|
||||
// 25
|
||||
/// Borrow amount too small
|
||||
#[error("Borrow amount too small")]
|
||||
BorrowTooSmall,
|
||||
/// Liquidation amount too small
|
||||
#[error("Liquidation amount too small to receive collateral")]
|
||||
LiquidationTooSmall,
|
||||
/// Reserve state stale
|
||||
#[error("Reserve state needs to be updated for the current slot")]
|
||||
ReserveStale,
|
||||
/// Trade simulation error
|
||||
#[error("Trade simulation error")]
|
||||
TradeSimulationError,
|
||||
/// Invalid dex order book side
|
||||
#[error("Invalid dex order book side")]
|
||||
DexInvalidOrderBookSide,
|
||||
|
||||
// 30
|
||||
/// Token initialize mint failed
|
||||
#[error("Token initialize mint failed")]
|
||||
TokenInitializeMintFailed,
|
||||
|
@ -126,16 +75,87 @@ pub enum LendingError {
|
|||
#[error("Token burn failed")]
|
||||
TokenBurnFailed,
|
||||
|
||||
// 20
|
||||
/// Insufficient liquidity available
|
||||
#[error("Insufficient liquidity available")]
|
||||
InsufficientLiquidity,
|
||||
/// This reserve's collateral cannot be used for borrows
|
||||
#[error("Input reserve has collateral disabled")]
|
||||
ReserveCollateralDisabled,
|
||||
/// Reserve state stale
|
||||
#[error("Reserve state needs to be refreshed")]
|
||||
ReserveStale,
|
||||
/// Withdraw amount too small
|
||||
#[error("Withdraw amount too small")]
|
||||
WithdrawTooSmall,
|
||||
/// Withdraw amount too large
|
||||
#[error("Withdraw amount too large")]
|
||||
WithdrawTooLarge,
|
||||
|
||||
// 25
|
||||
/// Borrow amount too small
|
||||
#[error("Borrow amount too small to receive liquidity after fees")]
|
||||
BorrowTooSmall,
|
||||
/// Borrow amount too large
|
||||
#[error("Borrow amount too large for deposited collateral")]
|
||||
BorrowTooLarge,
|
||||
/// Repay amount too small
|
||||
#[error("Repay amount too small to transfer liquidity")]
|
||||
RepayTooSmall,
|
||||
/// Liquidation amount too small
|
||||
#[error("Liquidation amount too small to receive collateral")]
|
||||
LiquidationTooSmall,
|
||||
/// Cannot liquidate healthy obligations
|
||||
#[error("Cannot liquidate healthy obligations")]
|
||||
ObligationHealthy,
|
||||
|
||||
// 30
|
||||
/// Obligation state stale
|
||||
#[error("Obligation state needs to be refreshed")]
|
||||
ObligationStale,
|
||||
/// Obligation reserve limit exceeded
|
||||
#[error("Obligation reserve limit exceeded")]
|
||||
ObligationReserveLimit,
|
||||
/// Expected a different obligation owner
|
||||
#[error("Obligation owner is invalid")]
|
||||
InvalidObligationOwner,
|
||||
/// Obligation deposits are empty
|
||||
#[error("Obligation deposits are empty")]
|
||||
ObligationDepositsEmpty,
|
||||
/// Obligation borrows are empty
|
||||
#[error("Obligation borrows are empty")]
|
||||
ObligationBorrowsEmpty,
|
||||
|
||||
// 35
|
||||
/// Invalid obligation collateral amount
|
||||
#[error("Invalid obligation collateral amount")]
|
||||
/// Obligation deposits have zero value
|
||||
#[error("Obligation deposits have zero value")]
|
||||
ObligationDepositsZero,
|
||||
/// Obligation borrows have zero value
|
||||
#[error("Obligation borrows have zero value")]
|
||||
ObligationBorrowsZero,
|
||||
/// Invalid obligation collateral
|
||||
#[error("Invalid obligation collateral")]
|
||||
InvalidObligationCollateral,
|
||||
/// Obligation collateral is already below required amount
|
||||
#[error("Obligation collateral is already below required amount")]
|
||||
ObligationCollateralBelowRequired,
|
||||
/// Obligation collateral cannot be withdrawn below required amount
|
||||
#[error("Obligation collateral cannot be withdrawn below required amount")]
|
||||
ObligationCollateralWithdrawBelowRequired,
|
||||
/// Invalid obligation liquidity
|
||||
#[error("Invalid obligation liquidity")]
|
||||
InvalidObligationLiquidity,
|
||||
/// Obligation collateral is empty
|
||||
#[error("Obligation collateral is empty")]
|
||||
ObligationCollateralEmpty,
|
||||
|
||||
// 40
|
||||
/// Obligation liquidity is empty
|
||||
#[error("Obligation liquidity is empty")]
|
||||
ObligationLiquidityEmpty,
|
||||
/// Negative interest rate
|
||||
#[error("Interest rate is negative")]
|
||||
NegativeInterestRate,
|
||||
/// Oracle config is invalid
|
||||
#[error("Input oracle config is invalid")]
|
||||
InvalidOracleConfig,
|
||||
/// Not enough liquidity after flash loan
|
||||
#[error("Not enough liquidity after flash loan")]
|
||||
NotEnoughLiquidityAfterFlashLoan,
|
||||
}
|
||||
|
||||
impl From<LendingError> for ProgramError {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@
|
|||
|
||||
//! A lending program for the Solana blockchain.
|
||||
|
||||
pub mod dex_market;
|
||||
pub mod entrypoint;
|
||||
pub mod error;
|
||||
pub mod instruction;
|
||||
|
|
|
@ -12,11 +12,12 @@
|
|||
#![allow(clippy::ptr_offset_with_cast)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
use crate::math::Rate;
|
||||
use crate::{error::LendingError, math::common::*};
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
math::{common::*, Rate},
|
||||
};
|
||||
use solana_program::program_error::ProgramError;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::{convert::TryFrom, fmt};
|
||||
use uint::construct_uint;
|
||||
|
||||
// U192 with 192 bits consisting of 3 x 64-bit words
|
||||
|
@ -55,6 +56,7 @@ impl Decimal {
|
|||
}
|
||||
|
||||
/// Return raw scaled value if it fits within u128
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn to_scaled_val(&self) -> Result<u128, ProgramError> {
|
||||
Ok(u128::try_from(self.0).map_err(|_| LendingError::MathOverflow)?)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,7 @@ impl Rate {
|
|||
}
|
||||
|
||||
/// Return raw scaled value
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn to_scaled_val(&self) -> u128 {
|
||||
self.0.as_u128()
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,58 @@
|
|||
use crate::error::LendingError;
|
||||
use solana_program::{clock::Slot, program_error::ProgramError};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
/// Number of slots to consider stale after
|
||||
pub const STALE_AFTER_SLOTS_ELAPSED: u64 = 1;
|
||||
|
||||
/// Last update state
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct LastUpdate {
|
||||
/// Last slot when updated
|
||||
pub slot: Slot,
|
||||
/// True when marked stale, false when slot updated
|
||||
pub stale: bool,
|
||||
}
|
||||
|
||||
impl LastUpdate {
|
||||
/// Create new last update
|
||||
pub fn new(slot: Slot) -> Self {
|
||||
Self { slot, stale: true }
|
||||
}
|
||||
|
||||
/// Return slots elapsed since given slot
|
||||
pub fn slots_elapsed(&self, slot: Slot) -> Result<u64, ProgramError> {
|
||||
let slots_elapsed = slot
|
||||
.checked_sub(self.slot)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(slots_elapsed)
|
||||
}
|
||||
|
||||
/// Set last update slot
|
||||
pub fn update_slot(&mut self, slot: Slot) {
|
||||
self.slot = slot;
|
||||
self.stale = false;
|
||||
}
|
||||
|
||||
/// Set stale to true
|
||||
pub fn mark_stale(&mut self) {
|
||||
self.stale = true;
|
||||
}
|
||||
|
||||
/// Check if marked stale or last update slot is too long ago
|
||||
pub fn is_stale(&self, slot: Slot) -> Result<bool, ProgramError> {
|
||||
Ok(self.stale || self.slots_elapsed(slot)? >= STALE_AFTER_SLOTS_ELAPSED)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for LastUpdate {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.slot == other.slot
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for LastUpdate {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
self.slot.partial_cmp(&other.slot)
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
use super::*;
|
||||
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
|
||||
use solana_program::{
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
program_pack::{IsInitialized, Pack, Sealed},
|
||||
pubkey::Pubkey,
|
||||
pubkey::{Pubkey, PUBKEY_BYTES},
|
||||
};
|
||||
|
||||
/// Lending market state
|
||||
|
@ -21,6 +22,36 @@ pub struct LendingMarket {
|
|||
pub token_program_id: Pubkey,
|
||||
}
|
||||
|
||||
impl LendingMarket {
|
||||
/// Create a new lending market
|
||||
pub fn new(params: InitLendingMarketParams) -> Self {
|
||||
let mut lending_market = Self::default();
|
||||
Self::init(&mut lending_market, params);
|
||||
lending_market
|
||||
}
|
||||
|
||||
/// Initialize a lending market
|
||||
pub fn init(&mut self, params: InitLendingMarketParams) {
|
||||
self.version = PROGRAM_VERSION;
|
||||
self.bump_seed = params.bump_seed;
|
||||
self.token_program_id = params.token_program_id;
|
||||
self.quote_token_mint = params.quote_token_mint;
|
||||
self.owner = params.owner;
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize a lending market
|
||||
pub struct InitLendingMarketParams {
|
||||
/// Bump seed for derived authority address
|
||||
pub bump_seed: u8,
|
||||
/// Owner authority which can add new reserves
|
||||
pub owner: Pubkey,
|
||||
/// Quote currency token mint
|
||||
pub quote_token_mint: Pubkey,
|
||||
/// Token program id
|
||||
pub token_program_id: Pubkey,
|
||||
}
|
||||
|
||||
impl Sealed for LendingMarket {}
|
||||
impl IsInitialized for LendingMarket {
|
||||
fn is_initialized(&self) -> bool {
|
||||
|
@ -28,18 +59,33 @@ impl IsInitialized for LendingMarket {
|
|||
}
|
||||
}
|
||||
|
||||
const LENDING_MARKET_LEN: usize = 160;
|
||||
const LENDING_MARKET_LEN: usize = 226; // 1 + 1 + 32 + 32 + 32 + 128
|
||||
impl Pack for LendingMarket {
|
||||
const LEN: usize = 160;
|
||||
const LEN: usize = LENDING_MARKET_LEN;
|
||||
|
||||
/// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html).
|
||||
fn pack_into_slice(&self, output: &mut [u8]) {
|
||||
let output = array_mut_ref![output, 0, LENDING_MARKET_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
|
||||
mut_array_refs![output, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128];
|
||||
|
||||
*version = self.version.to_le_bytes();
|
||||
*bump_seed = self.bump_seed.to_le_bytes();
|
||||
owner.copy_from_slice(self.owner.as_ref());
|
||||
quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref());
|
||||
token_program_id.copy_from_slice(self.token_program_id.as_ref());
|
||||
}
|
||||
|
||||
/// Unpacks a byte buffer into a [LendingMarketInfo](struct.LendingMarketInfo.html)
|
||||
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
|
||||
let input = array_ref![input, 0, LENDING_MARKET_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
|
||||
array_refs![input, 1, 1, 32, 32, 32, 62];
|
||||
array_refs![input, 1, 1, PUBKEY_BYTES, PUBKEY_BYTES, PUBKEY_BYTES, 128];
|
||||
|
||||
let version = u8::from_le_bytes(*version);
|
||||
if version > PROGRAM_VERSION {
|
||||
msg!("Lending market version does not match lending program version");
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
|
||||
|
@ -51,16 +97,4 @@ impl Pack for LendingMarket {
|
|||
token_program_id: Pubkey::new_from_array(*token_program_id),
|
||||
})
|
||||
}
|
||||
|
||||
fn pack_into_slice(&self, output: &mut [u8]) {
|
||||
let output = array_mut_ref![output, 0, LENDING_MARKET_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (version, bump_seed, owner, quote_token_mint, token_program_id, _padding) =
|
||||
mut_array_refs![output, 1, 1, 32, 32, 32, 62];
|
||||
*version = self.version.to_le_bytes();
|
||||
*bump_seed = self.bump_seed.to_le_bytes();
|
||||
owner.copy_from_slice(self.owner.as_ref());
|
||||
quote_token_mint.copy_from_slice(self.quote_token_mint.as_ref());
|
||||
token_program_id.copy_from_slice(self.token_program_id.as_ref());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
//! State types
|
||||
|
||||
mod last_update;
|
||||
mod lending_market;
|
||||
mod obligation;
|
||||
mod reserve;
|
||||
|
||||
pub use last_update::*;
|
||||
pub use lending_market::*;
|
||||
pub use obligation::*;
|
||||
pub use reserve::*;
|
||||
|
@ -12,13 +14,15 @@ use crate::math::{Decimal, WAD};
|
|||
use arrayref::{array_refs, mut_array_refs};
|
||||
use solana_program::{
|
||||
clock::{DEFAULT_TICKS_PER_SECOND, DEFAULT_TICKS_PER_SLOT, SECONDS_PER_DAY},
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
program_option::COption,
|
||||
pubkey::Pubkey,
|
||||
pubkey::{Pubkey, PUBKEY_BYTES},
|
||||
};
|
||||
|
||||
/// Collateral tokens are initially valued at a ratio of 5:1 (collateral:liquidity)
|
||||
pub const INITIAL_COLLATERAL_RATIO: u64 = 5;
|
||||
// @FIXME: restore to 5
|
||||
pub const INITIAL_COLLATERAL_RATIO: u64 = 1;
|
||||
const INITIAL_COLLATERAL_RATE: u64 = INITIAL_COLLATERAL_RATIO * WAD;
|
||||
|
||||
/// Current version of the program and all new accounts created
|
||||
|
@ -32,22 +36,10 @@ pub const UNINITIALIZED_VERSION: u8 = 0;
|
|||
pub const SLOTS_PER_YEAR: u64 =
|
||||
DEFAULT_TICKS_PER_SECOND / DEFAULT_TICKS_PER_SLOT * SECONDS_PER_DAY * 365;
|
||||
|
||||
/// Token converter
|
||||
pub trait TokenConverter {
|
||||
/// Return best price for specified token
|
||||
fn best_price(&mut self, token_mint: &Pubkey) -> Result<Decimal, ProgramError>;
|
||||
|
||||
/// Convert between two different tokens
|
||||
fn convert(
|
||||
self,
|
||||
from_amount: Decimal,
|
||||
from_token_mint: &Pubkey,
|
||||
) -> Result<Decimal, ProgramError>;
|
||||
}
|
||||
|
||||
// Helpers
|
||||
fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
|
||||
let (tag, body) = mut_array_refs![dst, 4, 32];
|
||||
fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 4 + PUBKEY_BYTES]) {
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (tag, body) = mut_array_refs![dst, 4, PUBKEY_BYTES];
|
||||
match src {
|
||||
COption::Some(key) => {
|
||||
*tag = [1, 0, 0, 0];
|
||||
|
@ -59,19 +51,23 @@ fn pack_coption_key(src: &COption<Pubkey>, dst: &mut [u8; 36]) {
|
|||
}
|
||||
}
|
||||
|
||||
fn unpack_coption_key(src: &[u8; 36]) -> Result<COption<Pubkey>, ProgramError> {
|
||||
let (tag, body) = array_refs![src, 4, 32];
|
||||
fn unpack_coption_key(src: &[u8; 4 + PUBKEY_BYTES]) -> Result<COption<Pubkey>, ProgramError> {
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (tag, body) = array_refs![src, 4, PUBKEY_BYTES];
|
||||
match *tag {
|
||||
[0, 0, 0, 0] => Ok(COption::None),
|
||||
[1, 0, 0, 0] => Ok(COption::Some(Pubkey::new_from_array(*body))),
|
||||
_ => Err(ProgramError::InvalidAccountData),
|
||||
_ => {
|
||||
msg!("COption<Pubkey> cannot be unpacked");
|
||||
Err(ProgramError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_decimal(decimal: Decimal, dst: &mut [u8; 16]) {
|
||||
*dst = decimal
|
||||
.to_scaled_val()
|
||||
.expect("could not pack decimal")
|
||||
.expect("Decimal cannot be packed")
|
||||
.to_le_bytes();
|
||||
}
|
||||
|
||||
|
@ -79,6 +75,21 @@ fn unpack_decimal(src: &[u8; 16]) -> Decimal {
|
|||
Decimal::from_scaled_val(u128::from_le_bytes(*src))
|
||||
}
|
||||
|
||||
fn pack_bool(boolean: bool, dst: &mut [u8; 1]) {
|
||||
*dst = (boolean as u8).to_le_bytes()
|
||||
}
|
||||
|
||||
fn unpack_bool(src: &[u8; 1]) -> Result<bool, ProgramError> {
|
||||
match u8::from_le_bytes(*src) {
|
||||
0 => Ok(false),
|
||||
1 => Ok(true),
|
||||
_ => {
|
||||
msg!("Boolean cannot be unpacked");
|
||||
Err(ProgramError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
|
@ -1,185 +1,220 @@
|
|||
use super::*;
|
||||
use crate::{
|
||||
error::LendingError,
|
||||
math::{Decimal, Rate, TryDiv, TryMul, TrySub},
|
||||
math::{Decimal, Rate, TryAdd, TryDiv, TryMul, TrySub},
|
||||
};
|
||||
use arrayref::{array_mut_ref, array_ref, array_refs, mut_array_refs};
|
||||
use solana_program::{
|
||||
clock::Slot,
|
||||
entrypoint::ProgramResult,
|
||||
msg,
|
||||
program_error::ProgramError,
|
||||
program_pack::{IsInitialized, Pack, Sealed},
|
||||
pubkey::Pubkey,
|
||||
pubkey::{Pubkey, PUBKEY_BYTES},
|
||||
};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
convert::{TryFrom, TryInto},
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
|
||||
/// Borrow obligation state
|
||||
/// Max number of collateral and liquidity reserve accounts combined for an obligation
|
||||
pub const MAX_OBLIGATION_RESERVES: usize = 10;
|
||||
|
||||
/// Lending market obligation state
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Obligation {
|
||||
/// Version of the obligation
|
||||
/// Version of the struct
|
||||
pub version: u8,
|
||||
/// Amount of collateral tokens deposited for this obligation
|
||||
pub deposited_collateral_tokens: u64,
|
||||
/// Reserve which collateral tokens were deposited into
|
||||
pub collateral_reserve: Pubkey,
|
||||
/// Borrow rate used for calculating interest.
|
||||
pub cumulative_borrow_rate_wads: Decimal,
|
||||
/// Amount of tokens borrowed for this obligation plus interest
|
||||
pub borrowed_liquidity_wads: Decimal,
|
||||
/// Reserve which tokens were borrowed from
|
||||
pub borrow_reserve: Pubkey,
|
||||
/// Mint address of the tokens for this obligation
|
||||
pub token_mint: Pubkey,
|
||||
/// Last update to collateral, liquidity, or their market values
|
||||
pub last_update: LastUpdate,
|
||||
/// Lending market address
|
||||
pub lending_market: Pubkey,
|
||||
/// Owner authority which can borrow liquidity
|
||||
pub owner: Pubkey,
|
||||
/// Deposited collateral for the obligation, unique by deposit reserve address
|
||||
pub deposits: Vec<ObligationCollateral>,
|
||||
/// Borrowed liquidity for the obligation, unique by borrow reserve address
|
||||
pub borrows: Vec<ObligationLiquidity>,
|
||||
/// Market value of deposits
|
||||
pub deposited_value: Decimal,
|
||||
/// Market value of borrows
|
||||
pub borrowed_value: Decimal,
|
||||
/// The maximum borrow value at the weighted average loan to value ratio
|
||||
pub allowed_borrow_value: Decimal,
|
||||
/// The dangerous borrow value at the weighted average liquidation threshold
|
||||
pub unhealthy_borrow_value: Decimal,
|
||||
}
|
||||
|
||||
impl Obligation {
|
||||
/// Create new obligation
|
||||
pub fn new(params: NewObligationParams) -> Self {
|
||||
let NewObligationParams {
|
||||
collateral_reserve,
|
||||
borrow_reserve,
|
||||
token_mint,
|
||||
cumulative_borrow_rate_wads,
|
||||
} = params;
|
||||
|
||||
Self {
|
||||
version: PROGRAM_VERSION,
|
||||
deposited_collateral_tokens: 0,
|
||||
collateral_reserve,
|
||||
cumulative_borrow_rate_wads,
|
||||
borrowed_liquidity_wads: Decimal::zero(),
|
||||
borrow_reserve,
|
||||
token_mint,
|
||||
}
|
||||
/// Create a new obligation
|
||||
pub fn new(params: InitObligationParams) -> Self {
|
||||
let mut obligation = Self::default();
|
||||
Self::init(&mut obligation, params);
|
||||
obligation
|
||||
}
|
||||
|
||||
/// Maximum amount of loan that can be closed out by a liquidator due
|
||||
/// to the remaining balance being too small to be liquidated normally.
|
||||
pub fn max_closeable_amount(&self) -> Result<u64, ProgramError> {
|
||||
if self.borrowed_liquidity_wads < Decimal::from(CLOSEABLE_AMOUNT) {
|
||||
self.borrowed_liquidity_wads.try_ceil_u64()
|
||||
/// Initialize an obligation
|
||||
pub fn init(&mut self, params: InitObligationParams) {
|
||||
self.version = PROGRAM_VERSION;
|
||||
self.last_update = LastUpdate::new(params.current_slot);
|
||||
self.lending_market = params.lending_market;
|
||||
self.owner = params.owner;
|
||||
self.deposits = params.deposits;
|
||||
self.borrows = params.borrows;
|
||||
}
|
||||
|
||||
/// Calculate the current ratio of borrowed value to deposited value
|
||||
pub fn loan_to_value(&self) -> Result<Decimal, ProgramError> {
|
||||
self.borrowed_value.try_div(self.deposited_value)
|
||||
}
|
||||
|
||||
/// Repay liquidity and remove it from borrows if zeroed out
|
||||
pub fn repay(&mut self, settle_amount: Decimal, liquidity_index: usize) -> ProgramResult {
|
||||
let liquidity = &mut self.borrows[liquidity_index];
|
||||
if settle_amount == liquidity.borrowed_amount_wads {
|
||||
self.borrows.remove(liquidity_index);
|
||||
} else {
|
||||
Ok(0)
|
||||
liquidity.repay(settle_amount)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Maximum amount of loan that can be repaid by liquidators
|
||||
pub fn max_liquidation_amount(&self) -> Result<u64, ProgramError> {
|
||||
self.borrowed_liquidity_wads
|
||||
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
|
||||
.try_floor_u64()
|
||||
/// Withdraw collateral and remove it from deposits if zeroed out
|
||||
pub fn withdraw(&mut self, withdraw_amount: u64, collateral_index: usize) -> ProgramResult {
|
||||
let collateral = &mut self.deposits[collateral_index];
|
||||
if withdraw_amount == collateral.deposited_amount {
|
||||
self.deposits.remove(collateral_index);
|
||||
} else {
|
||||
collateral.withdraw(withdraw_amount)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ratio of loan balance to collateral value
|
||||
pub fn loan_to_value(
|
||||
/// Calculate the maximum collateral value that can be withdrawn
|
||||
pub fn max_withdraw_value(&self) -> Result<Decimal, ProgramError> {
|
||||
let required_deposit_value = self
|
||||
.borrowed_value
|
||||
.try_mul(self.deposited_value)?
|
||||
.try_div(self.allowed_borrow_value)?;
|
||||
if required_deposit_value >= self.deposited_value {
|
||||
return Ok(Decimal::zero());
|
||||
}
|
||||
self.deposited_value.try_sub(required_deposit_value)
|
||||
}
|
||||
|
||||
/// Calculate the maximum liquidity value that can be borrowed
|
||||
pub fn remaining_borrow_value(&self) -> Result<Decimal, ProgramError> {
|
||||
self.allowed_borrow_value.try_sub(self.borrowed_value)
|
||||
}
|
||||
|
||||
/// Calculate the maximum liquidation amount for a given liquidity
|
||||
pub fn max_liquidation_amount(
|
||||
&self,
|
||||
collateral_exchange_rate: CollateralExchangeRate,
|
||||
borrow_token_price: Decimal,
|
||||
liquidity: &ObligationLiquidity,
|
||||
) -> Result<Decimal, ProgramError> {
|
||||
let loan = self.borrowed_liquidity_wads;
|
||||
let collateral_value = collateral_exchange_rate
|
||||
.decimal_collateral_to_liquidity(self.deposited_collateral_tokens.into())?
|
||||
.try_div(borrow_token_price)?;
|
||||
loan.try_div(collateral_value)
|
||||
let max_liquidation_value = self
|
||||
.borrowed_value
|
||||
.try_mul(Rate::from_percent(LIQUIDATION_CLOSE_FACTOR))?
|
||||
.min(liquidity.market_value);
|
||||
let max_liquidation_pct = max_liquidation_value.try_div(liquidity.market_value)?;
|
||||
liquidity.borrowed_amount_wads.try_mul(max_liquidation_pct)
|
||||
}
|
||||
|
||||
/// Amount of obligation tokens for given collateral
|
||||
pub fn collateral_to_obligation_token_amount(
|
||||
/// Find collateral by deposit reserve
|
||||
pub fn find_collateral_in_deposits(
|
||||
&self,
|
||||
collateral_amount: u64,
|
||||
obligation_token_supply: u64,
|
||||
) -> Result<u64, ProgramError> {
|
||||
let withdraw_pct =
|
||||
Decimal::from(collateral_amount).try_div(self.deposited_collateral_tokens)?;
|
||||
let token_amount: Decimal = withdraw_pct.try_mul(obligation_token_supply)?;
|
||||
token_amount.try_floor_u64()
|
||||
}
|
||||
|
||||
/// Accrue interest
|
||||
pub fn accrue_interest(&mut self, cumulative_borrow_rate: Decimal) -> ProgramResult {
|
||||
if cumulative_borrow_rate < self.cumulative_borrow_rate_wads {
|
||||
return Err(LendingError::NegativeInterestRate.into());
|
||||
deposit_reserve: Pubkey,
|
||||
) -> Result<(&ObligationCollateral, usize), ProgramError> {
|
||||
if self.deposits.is_empty() {
|
||||
msg!("Obligation has no deposits");
|
||||
return Err(LendingError::ObligationDepositsEmpty.into());
|
||||
}
|
||||
|
||||
let compounded_interest_rate: Rate = cumulative_borrow_rate
|
||||
.try_div(self.cumulative_borrow_rate_wads)?
|
||||
.try_into()?;
|
||||
|
||||
self.borrowed_liquidity_wads = self
|
||||
.borrowed_liquidity_wads
|
||||
.try_mul(compounded_interest_rate)?;
|
||||
|
||||
self.cumulative_borrow_rate_wads = cumulative_borrow_rate;
|
||||
|
||||
Ok(())
|
||||
let collateral_index = self
|
||||
._find_collateral_index_in_deposits(deposit_reserve)
|
||||
.ok_or(LendingError::InvalidObligationCollateral)?;
|
||||
Ok((&self.deposits[collateral_index], collateral_index))
|
||||
}
|
||||
|
||||
/// Liquidate part of obligation
|
||||
pub fn liquidate(&mut self, repay_amount: Decimal, withdraw_amount: u64) -> ProgramResult {
|
||||
self.borrowed_liquidity_wads = self.borrowed_liquidity_wads.try_sub(repay_amount)?;
|
||||
self.deposited_collateral_tokens = self
|
||||
.deposited_collateral_tokens
|
||||
.checked_sub(withdraw_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Repay borrowed tokens
|
||||
pub fn repay(
|
||||
/// Find or add collateral by deposit reserve
|
||||
pub fn find_or_add_collateral_to_deposits(
|
||||
&mut self,
|
||||
liquidity_amount: u64,
|
||||
obligation_token_supply: u64,
|
||||
) -> Result<RepayResult, ProgramError> {
|
||||
let decimal_repay_amount =
|
||||
Decimal::from(liquidity_amount).min(self.borrowed_liquidity_wads);
|
||||
let integer_repay_amount = decimal_repay_amount.try_ceil_u64()?;
|
||||
if integer_repay_amount == 0 {
|
||||
return Err(LendingError::ObligationEmpty.into());
|
||||
deposit_reserve: Pubkey,
|
||||
) -> Result<&mut ObligationCollateral, ProgramError> {
|
||||
if let Some(collateral_index) = self._find_collateral_index_in_deposits(deposit_reserve) {
|
||||
return Ok(&mut self.deposits[collateral_index]);
|
||||
}
|
||||
if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
|
||||
msg!(
|
||||
"Obligation cannot have more than {} deposits and borrows combined",
|
||||
MAX_OBLIGATION_RESERVES
|
||||
);
|
||||
return Err(LendingError::ObligationReserveLimit.into());
|
||||
}
|
||||
let collateral = ObligationCollateral::new(deposit_reserve);
|
||||
self.deposits.push(collateral);
|
||||
Ok(self.deposits.last_mut().unwrap())
|
||||
}
|
||||
|
||||
let repay_pct: Decimal = decimal_repay_amount.try_div(self.borrowed_liquidity_wads)?;
|
||||
let collateral_withdraw_amount = {
|
||||
let withdraw_amount: Decimal = repay_pct.try_mul(self.deposited_collateral_tokens)?;
|
||||
withdraw_amount.try_floor_u64()?
|
||||
};
|
||||
fn _find_collateral_index_in_deposits(&self, deposit_reserve: Pubkey) -> Option<usize> {
|
||||
self.deposits
|
||||
.iter()
|
||||
.position(|collateral| collateral.deposit_reserve == deposit_reserve)
|
||||
}
|
||||
|
||||
let obligation_token_amount = self.collateral_to_obligation_token_amount(
|
||||
collateral_withdraw_amount,
|
||||
obligation_token_supply,
|
||||
)?;
|
||||
/// Find liquidity by borrow reserve
|
||||
pub fn find_liquidity_in_borrows(
|
||||
&self,
|
||||
borrow_reserve: Pubkey,
|
||||
) -> Result<(&ObligationLiquidity, usize), ProgramError> {
|
||||
if self.borrows.is_empty() {
|
||||
msg!("Obligation has no borrows");
|
||||
return Err(LendingError::ObligationBorrowsEmpty.into());
|
||||
}
|
||||
let liquidity_index = self
|
||||
._find_liquidity_index_in_borrows(borrow_reserve)
|
||||
.ok_or(LendingError::InvalidObligationLiquidity)?;
|
||||
Ok((&self.borrows[liquidity_index], liquidity_index))
|
||||
}
|
||||
|
||||
self.liquidate(decimal_repay_amount, collateral_withdraw_amount)?;
|
||||
/// Find or add liquidity by borrow reserve
|
||||
pub fn find_or_add_liquidity_to_borrows(
|
||||
&mut self,
|
||||
borrow_reserve: Pubkey,
|
||||
) -> Result<&mut ObligationLiquidity, ProgramError> {
|
||||
if let Some(liquidity_index) = self._find_liquidity_index_in_borrows(borrow_reserve) {
|
||||
return Ok(&mut self.borrows[liquidity_index]);
|
||||
}
|
||||
if self.deposits.len() + self.borrows.len() >= MAX_OBLIGATION_RESERVES {
|
||||
msg!(
|
||||
"Obligation cannot have more than {} deposits and borrows combined",
|
||||
MAX_OBLIGATION_RESERVES
|
||||
);
|
||||
return Err(LendingError::ObligationReserveLimit.into());
|
||||
}
|
||||
let liquidity = ObligationLiquidity::new(borrow_reserve);
|
||||
self.borrows.push(liquidity);
|
||||
Ok(self.borrows.last_mut().unwrap())
|
||||
}
|
||||
|
||||
Ok(RepayResult {
|
||||
collateral_withdraw_amount,
|
||||
obligation_token_amount,
|
||||
decimal_repay_amount,
|
||||
integer_repay_amount,
|
||||
})
|
||||
fn _find_liquidity_index_in_borrows(&self, borrow_reserve: Pubkey) -> Option<usize> {
|
||||
self.borrows
|
||||
.iter()
|
||||
.position(|liquidity| liquidity.borrow_reserve == borrow_reserve)
|
||||
}
|
||||
}
|
||||
|
||||
/// Obligation repay result
|
||||
pub struct RepayResult {
|
||||
/// Amount of collateral to withdraw
|
||||
pub collateral_withdraw_amount: u64,
|
||||
/// Amount of obligation tokens to burn
|
||||
pub obligation_token_amount: u64,
|
||||
/// Amount that will be repaid as precise decimal
|
||||
pub decimal_repay_amount: Decimal,
|
||||
/// Amount that will be repaid as u64
|
||||
pub integer_repay_amount: u64,
|
||||
}
|
||||
|
||||
/// Create new obligation
|
||||
pub struct NewObligationParams {
|
||||
/// Collateral reserve address
|
||||
pub collateral_reserve: Pubkey,
|
||||
/// Borrow reserve address
|
||||
pub borrow_reserve: Pubkey,
|
||||
/// Obligation token mint address
|
||||
pub token_mint: Pubkey,
|
||||
/// Borrow rate used for calculating interest.
|
||||
pub cumulative_borrow_rate_wads: Decimal,
|
||||
/// Initialize an obligation
|
||||
pub struct InitObligationParams {
|
||||
/// Last update to collateral, liquidity, or their market values
|
||||
pub current_slot: Slot,
|
||||
/// Lending market address
|
||||
pub lending_market: Pubkey,
|
||||
/// Owner authority which can borrow liquidity
|
||||
pub owner: Pubkey,
|
||||
/// Deposited collateral for the obligation, unique by deposit reserve address
|
||||
pub deposits: Vec<ObligationCollateral>,
|
||||
/// Borrowed liquidity for the obligation, unique by borrow reserve address
|
||||
pub borrows: Vec<ObligationLiquidity>,
|
||||
}
|
||||
|
||||
impl Sealed for Obligation {}
|
||||
|
@ -189,55 +224,275 @@ impl IsInitialized for Obligation {
|
|||
}
|
||||
}
|
||||
|
||||
const OBLIGATION_LEN: usize = 265;
|
||||
impl Pack for Obligation {
|
||||
const LEN: usize = 265;
|
||||
/// Obligation collateral state
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ObligationCollateral {
|
||||
/// Reserve collateral is deposited to
|
||||
pub deposit_reserve: Pubkey,
|
||||
/// Amount of collateral deposited
|
||||
pub deposited_amount: u64,
|
||||
/// Collateral market value in quote currency
|
||||
pub market_value: Decimal,
|
||||
}
|
||||
|
||||
/// Unpacks a byte buffer into a [ObligationInfo](struct.ObligationInfo.html).
|
||||
fn unpack_from_slice(input: &[u8]) -> Result<Self, ProgramError> {
|
||||
let input = array_ref![input, 0, OBLIGATION_LEN];
|
||||
impl ObligationCollateral {
|
||||
/// Create new obligation collateral
|
||||
pub fn new(deposit_reserve: Pubkey) -> Self {
|
||||
Self {
|
||||
deposit_reserve,
|
||||
deposited_amount: 0,
|
||||
market_value: Decimal::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Increase deposited collateral
|
||||
pub fn deposit(&mut self, collateral_amount: u64) -> ProgramResult {
|
||||
self.deposited_amount = self
|
||||
.deposited_amount
|
||||
.checked_add(collateral_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decrease deposited collateral
|
||||
pub fn withdraw(&mut self, collateral_amount: u64) -> ProgramResult {
|
||||
self.deposited_amount = self
|
||||
.deposited_amount
|
||||
.checked_sub(collateral_amount)
|
||||
.ok_or(LendingError::MathOverflow)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Obligation liquidity state
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct ObligationLiquidity {
|
||||
/// Reserve liquidity is borrowed from
|
||||
pub borrow_reserve: Pubkey,
|
||||
/// Borrow rate used for calculating interest
|
||||
pub cumulative_borrow_rate_wads: Decimal,
|
||||
/// Amount of liquidity borrowed plus interest
|
||||
pub borrowed_amount_wads: Decimal,
|
||||
/// Liquidity market value in quote currency
|
||||
pub market_value: Decimal,
|
||||
}
|
||||
|
||||
impl ObligationLiquidity {
|
||||
/// Create new obligation liquidity
|
||||
pub fn new(borrow_reserve: Pubkey) -> Self {
|
||||
Self {
|
||||
borrow_reserve,
|
||||
cumulative_borrow_rate_wads: Decimal::one(),
|
||||
borrowed_amount_wads: Decimal::zero(),
|
||||
market_value: Decimal::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrease borrowed liquidity
|
||||
pub fn repay(&mut self, settle_amount: Decimal) -> ProgramResult {
|
||||
self.borrowed_amount_wads = self.borrowed_amount_wads.try_sub(settle_amount)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Increase borrowed liquidity
|
||||
pub fn borrow(&mut self, borrow_amount: Decimal) -> ProgramResult {
|
||||
self.borrowed_amount_wads = self.borrowed_amount_wads.try_add(borrow_amount)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Accrue interest
|
||||
pub fn accrue_interest(&mut self, cumulative_borrow_rate_wads: Decimal) -> ProgramResult {
|
||||
match cumulative_borrow_rate_wads.cmp(&self.cumulative_borrow_rate_wads) {
|
||||
Ordering::Less => {
|
||||
msg!("Interest rate cannot be negative");
|
||||
return Err(LendingError::NegativeInterestRate.into());
|
||||
}
|
||||
Ordering::Equal => {}
|
||||
Ordering::Greater => {
|
||||
let compounded_interest_rate: Rate = cumulative_borrow_rate_wads
|
||||
.try_div(self.cumulative_borrow_rate_wads)?
|
||||
.try_into()?;
|
||||
|
||||
self.borrowed_amount_wads = self
|
||||
.borrowed_amount_wads
|
||||
.try_mul(compounded_interest_rate)?;
|
||||
self.cumulative_borrow_rate_wads = cumulative_borrow_rate_wads;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
const OBLIGATION_COLLATERAL_LEN: usize = 56; // 32 + 8 + 16
|
||||
const OBLIGATION_LIQUIDITY_LEN: usize = 80; // 32 + 16 + 16 + 16
|
||||
const OBLIGATION_LEN: usize = 916; // 1 + 8 + 1 + 32 + 32 + 16 + 16 + 16 + 16 + 1 + 1 + (56 * 1) + (80 * 9)
|
||||
// @TODO: break this up by obligation / collateral / liquidity https://git.io/JOCca
|
||||
impl Pack for Obligation {
|
||||
const LEN: usize = OBLIGATION_LEN;
|
||||
|
||||
fn pack_into_slice(&self, dst: &mut [u8]) {
|
||||
let output = array_mut_ref![dst, 0, OBLIGATION_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (
|
||||
version,
|
||||
deposited_collateral_tokens,
|
||||
collateral_supply,
|
||||
cumulative_borrow_rate,
|
||||
borrowed_liquidity_wads,
|
||||
borrow_reserve,
|
||||
token_mint,
|
||||
_padding,
|
||||
) = array_refs![input, 1, 8, 32, 16, 16, 32, 32, 128];
|
||||
Ok(Self {
|
||||
version: u8::from_le_bytes(*version),
|
||||
deposited_collateral_tokens: u64::from_le_bytes(*deposited_collateral_tokens),
|
||||
collateral_reserve: Pubkey::new_from_array(*collateral_supply),
|
||||
cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate),
|
||||
borrowed_liquidity_wads: unpack_decimal(borrowed_liquidity_wads),
|
||||
borrow_reserve: Pubkey::new_from_array(*borrow_reserve),
|
||||
token_mint: Pubkey::new_from_array(*token_mint),
|
||||
})
|
||||
last_update_slot,
|
||||
last_update_stale,
|
||||
lending_market,
|
||||
owner,
|
||||
deposited_value,
|
||||
borrowed_value,
|
||||
allowed_borrow_value,
|
||||
unhealthy_borrow_value,
|
||||
deposits_len,
|
||||
borrows_len,
|
||||
data_flat,
|
||||
) = mut_array_refs![
|
||||
output,
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
PUBKEY_BYTES,
|
||||
PUBKEY_BYTES,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
1,
|
||||
1,
|
||||
OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
|
||||
];
|
||||
|
||||
// obligation
|
||||
*version = self.version.to_le_bytes();
|
||||
*last_update_slot = self.last_update.slot.to_le_bytes();
|
||||
pack_bool(self.last_update.stale, last_update_stale);
|
||||
lending_market.copy_from_slice(self.lending_market.as_ref());
|
||||
owner.copy_from_slice(self.owner.as_ref());
|
||||
pack_decimal(self.deposited_value, deposited_value);
|
||||
pack_decimal(self.borrowed_value, borrowed_value);
|
||||
pack_decimal(self.allowed_borrow_value, allowed_borrow_value);
|
||||
pack_decimal(self.unhealthy_borrow_value, unhealthy_borrow_value);
|
||||
*deposits_len = u8::try_from(self.deposits.len()).unwrap().to_le_bytes();
|
||||
*borrows_len = u8::try_from(self.borrows.len()).unwrap().to_le_bytes();
|
||||
|
||||
let mut offset = 0;
|
||||
|
||||
// deposits
|
||||
for collateral in &self.deposits {
|
||||
let deposits_flat = array_mut_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (deposit_reserve, deposited_amount, market_value) =
|
||||
mut_array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
|
||||
deposit_reserve.copy_from_slice(collateral.deposit_reserve.as_ref());
|
||||
*deposited_amount = collateral.deposited_amount.to_le_bytes();
|
||||
pack_decimal(collateral.market_value, market_value);
|
||||
offset += OBLIGATION_COLLATERAL_LEN;
|
||||
}
|
||||
|
||||
// borrows
|
||||
for liquidity in &self.borrows {
|
||||
let borrows_flat = array_mut_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
|
||||
mut_array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
|
||||
borrow_reserve.copy_from_slice(liquidity.borrow_reserve.as_ref());
|
||||
pack_decimal(
|
||||
liquidity.cumulative_borrow_rate_wads,
|
||||
cumulative_borrow_rate_wads,
|
||||
);
|
||||
pack_decimal(liquidity.borrowed_amount_wads, borrowed_amount_wads);
|
||||
pack_decimal(liquidity.market_value, market_value);
|
||||
offset += OBLIGATION_LIQUIDITY_LEN;
|
||||
}
|
||||
}
|
||||
|
||||
fn pack_into_slice(&self, output: &mut [u8]) {
|
||||
let output = array_mut_ref![output, 0, OBLIGATION_LEN];
|
||||
/// Unpacks a byte buffer into an [ObligationInfo](struct.ObligationInfo.html).
|
||||
fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
|
||||
let input = array_ref![src, 0, OBLIGATION_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (
|
||||
version,
|
||||
deposited_collateral_tokens,
|
||||
collateral_supply,
|
||||
cumulative_borrow_rate,
|
||||
borrowed_liquidity_wads,
|
||||
borrow_reserve,
|
||||
token_mint,
|
||||
_padding,
|
||||
) = mut_array_refs![output, 1, 8, 32, 16, 16, 32, 32, 128];
|
||||
last_update_slot,
|
||||
last_update_stale,
|
||||
lending_market,
|
||||
owner,
|
||||
deposited_value,
|
||||
borrowed_value,
|
||||
allowed_borrow_value,
|
||||
unhealthy_borrow_value,
|
||||
deposits_len,
|
||||
borrows_len,
|
||||
data_flat,
|
||||
) = array_refs![
|
||||
input,
|
||||
1,
|
||||
8,
|
||||
1,
|
||||
PUBKEY_BYTES,
|
||||
PUBKEY_BYTES,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
16,
|
||||
1,
|
||||
1,
|
||||
OBLIGATION_COLLATERAL_LEN + (OBLIGATION_LIQUIDITY_LEN * (MAX_OBLIGATION_RESERVES - 1))
|
||||
];
|
||||
|
||||
*version = self.version.to_le_bytes();
|
||||
*deposited_collateral_tokens = self.deposited_collateral_tokens.to_le_bytes();
|
||||
collateral_supply.copy_from_slice(self.collateral_reserve.as_ref());
|
||||
pack_decimal(self.cumulative_borrow_rate_wads, cumulative_borrow_rate);
|
||||
pack_decimal(self.borrowed_liquidity_wads, borrowed_liquidity_wads);
|
||||
borrow_reserve.copy_from_slice(self.borrow_reserve.as_ref());
|
||||
token_mint.copy_from_slice(self.token_mint.as_ref());
|
||||
let version = u8::from_le_bytes(*version);
|
||||
if version > PROGRAM_VERSION {
|
||||
msg!("Obligation version does not match lending program version");
|
||||
return Err(ProgramError::InvalidAccountData);
|
||||
}
|
||||
|
||||
let deposits_len = u8::from_le_bytes(*deposits_len);
|
||||
let borrows_len = u8::from_le_bytes(*borrows_len);
|
||||
let mut deposits = Vec::with_capacity(deposits_len as usize + 1);
|
||||
let mut borrows = Vec::with_capacity(borrows_len as usize + 1);
|
||||
|
||||
let mut offset = 0;
|
||||
for _ in 0..deposits_len {
|
||||
let deposits_flat = array_ref![data_flat, offset, OBLIGATION_COLLATERAL_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (deposit_reserve, deposited_amount, market_value) =
|
||||
array_refs![deposits_flat, PUBKEY_BYTES, 8, 16];
|
||||
deposits.push(ObligationCollateral {
|
||||
deposit_reserve: Pubkey::new(deposit_reserve),
|
||||
deposited_amount: u64::from_le_bytes(*deposited_amount),
|
||||
market_value: unpack_decimal(market_value),
|
||||
});
|
||||
offset += OBLIGATION_COLLATERAL_LEN;
|
||||
}
|
||||
for _ in 0..borrows_len {
|
||||
let borrows_flat = array_ref![data_flat, offset, OBLIGATION_LIQUIDITY_LEN];
|
||||
#[allow(clippy::ptr_offset_with_cast)]
|
||||
let (borrow_reserve, cumulative_borrow_rate_wads, borrowed_amount_wads, market_value) =
|
||||
array_refs![borrows_flat, PUBKEY_BYTES, 16, 16, 16];
|
||||
borrows.push(ObligationLiquidity {
|
||||
borrow_reserve: Pubkey::new(borrow_reserve),
|
||||
cumulative_borrow_rate_wads: unpack_decimal(cumulative_borrow_rate_wads),
|
||||
borrowed_amount_wads: unpack_decimal(borrowed_amount_wads),
|
||||
market_value: unpack_decimal(market_value),
|
||||
});
|
||||
offset += OBLIGATION_LIQUIDITY_LEN;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
version,
|
||||
last_update: LastUpdate {
|
||||
slot: u64::from_le_bytes(*last_update_slot),
|
||||
stale: unpack_bool(last_update_stale)?,
|
||||
},
|
||||
lending_market: Pubkey::new_from_array(*lending_market),
|
||||
owner: Pubkey::new_from_array(*owner),
|
||||
deposits,
|
||||
borrows,
|
||||
deposited_value: unpack_decimal(deposited_value),
|
||||
borrowed_value: unpack_decimal(borrowed_value),
|
||||
allowed_borrow_value: unpack_decimal(allowed_borrow_value),
|
||||
unhealthy_borrow_value: unpack_decimal(unhealthy_borrow_value),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -252,28 +507,28 @@ mod test {
|
|||
#[test]
|
||||
fn obligation_accrue_interest_failure() {
|
||||
assert_eq!(
|
||||
Obligation {
|
||||
ObligationLiquidity {
|
||||
cumulative_borrow_rate_wads: Decimal::zero(),
|
||||
..Obligation::default()
|
||||
..ObligationLiquidity::default()
|
||||
}
|
||||
.accrue_interest(Decimal::one()),
|
||||
Err(LendingError::MathOverflow.into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Obligation {
|
||||
ObligationLiquidity {
|
||||
cumulative_borrow_rate_wads: Decimal::from(2u64),
|
||||
..Obligation::default()
|
||||
..ObligationLiquidity::default()
|
||||
}
|
||||
.accrue_interest(Decimal::one()),
|
||||
Err(LendingError::NegativeInterestRate.into())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Obligation {
|
||||
ObligationLiquidity {
|
||||
cumulative_borrow_rate_wads: Decimal::one(),
|
||||
borrowed_liquidity_wads: Decimal::from(u64::MAX),
|
||||
..Obligation::default()
|
||||
borrowed_amount_wads: Decimal::from(u64::MAX),
|
||||
..ObligationLiquidity::default()
|
||||
}
|
||||
.accrue_interest(Decimal::from(10 * MAX_COMPOUNDED_INTEREST)),
|
||||
Err(LendingError::MathOverflow.into())
|
||||
|
@ -284,7 +539,7 @@ mod test {
|
|||
prop_compose! {
|
||||
fn cumulative_rates()(rate in 1..=u128::MAX)(
|
||||
current_rate in Just(rate),
|
||||
max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128)
|
||||
max_new_rate in rate..=rate.saturating_mul(MAX_COMPOUNDED_INTEREST as u128),
|
||||
) -> (u128, u128) {
|
||||
(current_rate, max_new_rate)
|
||||
}
|
||||
|
@ -294,89 +549,81 @@ mod test {
|
|||
|
||||
// Creates liquidity amounts (repay, borrow) where repay < borrow
|
||||
prop_compose! {
|
||||
fn repay_partial_amounts()(repay in 1..=u64::MAX)(
|
||||
liquidity_amount in Just(repay),
|
||||
borrowed_liquidity in (WAD as u128 * repay as u128 + 1)..=MAX_BORROWED
|
||||
) -> (u64, u128) {
|
||||
(liquidity_amount, borrowed_liquidity)
|
||||
fn repay_partial_amounts()(amount in 1..=u64::MAX)(
|
||||
repay_amount in Just(WAD as u128 * amount as u128),
|
||||
borrowed_amount in (WAD as u128 * amount as u128 + 1)..=MAX_BORROWED,
|
||||
) -> (u128, u128) {
|
||||
(repay_amount, borrowed_amount)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates liquidity amounts (repay, borrow) where repay >= borrow
|
||||
prop_compose! {
|
||||
fn repay_full_amounts()(repay in 1..=u64::MAX)(
|
||||
liquidity_amount in Just(repay),
|
||||
borrowed_liquidity in 0..=(WAD as u128 * repay as u128)
|
||||
) -> (u64, u128) {
|
||||
(liquidity_amount, borrowed_liquidity)
|
||||
}
|
||||
}
|
||||
|
||||
// Creates collateral amounts (collateral, obligation tokens) where c <= ot
|
||||
prop_compose! {
|
||||
fn collateral_amounts()(collateral in 1..=u64::MAX)(
|
||||
deposited_collateral_tokens in Just(collateral),
|
||||
obligation_tokens in collateral..=u64::MAX
|
||||
) -> (u64, u64) {
|
||||
(deposited_collateral_tokens, obligation_tokens)
|
||||
fn repay_full_amounts()(amount in 1..=u64::MAX)(
|
||||
repay_amount in Just(WAD as u128 * amount as u128),
|
||||
) -> (u128, u128) {
|
||||
(repay_amount, repay_amount)
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn repay_partial(
|
||||
(liquidity_amount, borrowed_liquidity) in repay_partial_amounts(),
|
||||
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
|
||||
(repay_amount, borrowed_amount) in repay_partial_amounts(),
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity);
|
||||
let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() };
|
||||
let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
|
||||
let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
|
||||
let mut obligation = Obligation {
|
||||
borrows: vec![ObligationLiquidity {
|
||||
borrowed_amount_wads,
|
||||
..ObligationLiquidity::default()
|
||||
}],
|
||||
..Obligation::default()
|
||||
};
|
||||
|
||||
let repay_result = state.repay(liquidity_amount, obligation_tokens)?;
|
||||
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount));
|
||||
assert!(repay_result.collateral_withdraw_amount < deposited_collateral_tokens);
|
||||
assert!(repay_result.obligation_token_amount < obligation_tokens);
|
||||
assert!(state.borrowed_liquidity_wads < borrowed_liquidity_wads);
|
||||
assert!(state.borrowed_liquidity_wads > Decimal::zero());
|
||||
assert!(state.deposited_collateral_tokens > 0);
|
||||
|
||||
let obligation_token_rate = Decimal::from(repay_result.obligation_token_amount).try_div(Decimal::from(obligation_tokens))?;
|
||||
let collateral_withdraw_rate = Decimal::from(repay_result.collateral_withdraw_amount).try_div(Decimal::from(deposited_collateral_tokens))?;
|
||||
assert!(obligation_token_rate <= collateral_withdraw_rate);
|
||||
obligation.repay(repay_amount_wads, 0)?;
|
||||
assert!(obligation.borrows[0].borrowed_amount_wads < borrowed_amount_wads);
|
||||
assert!(obligation.borrows[0].borrowed_amount_wads > Decimal::zero());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repay_full(
|
||||
(liquidity_amount, borrowed_liquidity) in repay_full_amounts(),
|
||||
(deposited_collateral_tokens, obligation_tokens) in collateral_amounts(),
|
||||
(repay_amount, borrowed_amount) in repay_full_amounts(),
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from_scaled_val(borrowed_liquidity);
|
||||
let mut state = Obligation { deposited_collateral_tokens, borrowed_liquidity_wads, ..Obligation::default() } ;
|
||||
let borrowed_amount_wads = Decimal::from_scaled_val(borrowed_amount);
|
||||
let repay_amount_wads = Decimal::from_scaled_val(repay_amount);
|
||||
let mut obligation = Obligation {
|
||||
borrows: vec![ObligationLiquidity {
|
||||
borrowed_amount_wads,
|
||||
..ObligationLiquidity::default()
|
||||
}],
|
||||
..Obligation::default()
|
||||
};
|
||||
|
||||
let repay_result = state.repay(liquidity_amount, obligation_tokens)?;
|
||||
assert!(repay_result.decimal_repay_amount <= Decimal::from(repay_result.integer_repay_amount));
|
||||
assert_eq!(repay_result.collateral_withdraw_amount, deposited_collateral_tokens);
|
||||
assert_eq!(repay_result.obligation_token_amount, obligation_tokens);
|
||||
assert_eq!(repay_result.decimal_repay_amount, borrowed_liquidity_wads);
|
||||
assert_eq!(state.borrowed_liquidity_wads, Decimal::zero());
|
||||
assert_eq!(state.deposited_collateral_tokens, 0);
|
||||
obligation.repay(repay_amount_wads, 0)?;
|
||||
assert_eq!(obligation.borrows.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accrue_interest(
|
||||
borrowed_liquidity in 0..=u64::MAX,
|
||||
(current_borrow_rate, new_borrow_rate) in cumulative_rates(),
|
||||
borrowed_amount in 0..=u64::MAX,
|
||||
) {
|
||||
let borrowed_liquidity_wads = Decimal::from(borrowed_liquidity);
|
||||
let cumulative_borrow_rate_wads = Decimal::one().try_add(Decimal::from_scaled_val(current_borrow_rate))?;
|
||||
let mut state = Obligation { cumulative_borrow_rate_wads, borrowed_liquidity_wads, ..Obligation::default() };
|
||||
let borrowed_amount_wads = Decimal::from(borrowed_amount);
|
||||
let mut liquidity = ObligationLiquidity {
|
||||
cumulative_borrow_rate_wads,
|
||||
borrowed_amount_wads,
|
||||
..ObligationLiquidity::default()
|
||||
};
|
||||
|
||||
let next_cumulative_borrow_rate = Decimal::one().try_add(Decimal::from_scaled_val(new_borrow_rate))?;
|
||||
state.accrue_interest(next_cumulative_borrow_rate)?;
|
||||
liquidity.accrue_interest(next_cumulative_borrow_rate)?;
|
||||
|
||||
if next_cumulative_borrow_rate > cumulative_borrow_rate_wads {
|
||||
assert!(state.borrowed_liquidity_wads > borrowed_liquidity_wads);
|
||||
assert!(liquidity.borrowed_amount_wads > borrowed_amount_wads);
|
||||
} else {
|
||||
assert!(state.borrowed_liquidity_wads == borrowed_liquidity_wads);
|
||||
assert!(liquidity.borrowed_amount_wads == borrowed_amount_wads);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,108 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
transaction::Transaction,
|
||||
};
|
||||
use spl_token_lending::{
|
||||
instruction::accrue_reserve_interest,
|
||||
math::{Decimal, Rate, TryMul},
|
||||
processor::process_instruction,
|
||||
state::SLOTS_PER_YEAR,
|
||||
};
|
||||
|
||||
const LAMPORTS_TO_SOL: u64 = 1_000_000_000;
|
||||
const FRACTIONAL_TO_USDC: u64 = 1_000_000;
|
||||
|
||||
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL;
|
||||
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 100 * FRACTIONAL_TO_USDC;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_success() {
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(80_000);
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 80;
|
||||
|
||||
// Configure reserve to a fixed borrow rate of 1%
|
||||
const BORROW_RATE: u8 = 1;
|
||||
reserve_config.min_borrow_rate = BORROW_RATE;
|
||||
reserve_config.optimal_borrow_rate = BORROW_RATE;
|
||||
reserve_config.optimal_utilization_rate = 100;
|
||||
|
||||
let usdc_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
borrow_amount: 100,
|
||||
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
slots_elapsed: SLOTS_PER_YEAR,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let sol_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
borrow_amount: 100,
|
||||
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
||||
liquidity_mint_decimals: 9,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
slots_elapsed: SLOTS_PER_YEAR,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[accrue_reserve_interest(
|
||||
spl_token_lending::id(),
|
||||
vec![usdc_reserve.pubkey, sol_reserve.pubkey],
|
||||
)],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(&[&payer], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let sol_reserve = sol_reserve.get_state(&mut banks_client).await;
|
||||
let usdc_reserve = usdc_reserve.get_state(&mut banks_client).await;
|
||||
|
||||
let borrow_rate = Rate::from_percent(100u8 + BORROW_RATE);
|
||||
assert!(sol_reserve.cumulative_borrow_rate_wads > borrow_rate.into());
|
||||
assert_eq!(
|
||||
sol_reserve.cumulative_borrow_rate_wads,
|
||||
usdc_reserve.cumulative_borrow_rate_wads
|
||||
);
|
||||
assert!(
|
||||
sol_reserve.liquidity.borrowed_amount_wads
|
||||
> Decimal::from(100u64).try_mul(borrow_rate).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
sol_reserve.liquidity.borrowed_amount_wads,
|
||||
usdc_reserve.liquidity.borrowed_amount_wads
|
||||
);
|
||||
}
|
|
@ -1,325 +0,0 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{pubkey::Pubkey, signature::Keypair};
|
||||
use spl_token_lending::{
|
||||
instruction::BorrowAmountType, math::Decimal, processor::process_instruction,
|
||||
state::INITIAL_COLLATERAL_RATIO,
|
||||
};
|
||||
|
||||
const LAMPORTS_TO_SOL: u64 = 1_000_000_000;
|
||||
const FRACTIONAL_TO_USDC: u64 = 1_000_000;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_quote_currency() {
|
||||
// Using SOL/USDC max 3 bids:
|
||||
// $13.988, 300.0 SOL
|
||||
// $13.960, 206.8 SOL
|
||||
// $13.928, 1000.0 SOL
|
||||
//
|
||||
// Collateral amount = 750 * 0.8 (LTV) = 600 SOL
|
||||
// Borrow amount = 13.988 * 300 + 13.960 * 206.8 + 13.928 * 93.2 = 8,381.4176 USDC
|
||||
const SOL_COLLATERAL_AMOUNT_LAMPORTS: u64 = 750 * LAMPORTS_TO_SOL;
|
||||
const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 8_381_417_600;
|
||||
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 20_000 * FRACTIONAL_TO_USDC;
|
||||
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 2 * SOL_COLLATERAL_AMOUNT_LAMPORTS;
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(230_000);
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC);
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 80;
|
||||
|
||||
let usdc_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let sol_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
dex_market_pubkey: Some(sol_usdc_dex_market.pubkey),
|
||||
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
liquidity_mint_decimals: 9,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let usdc_obligation = add_obligation(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddObligationArgs {
|
||||
borrow_reserve: &usdc_reserve,
|
||||
collateral_reserve: &sol_reserve,
|
||||
collateral_amount: 0,
|
||||
borrowed_liquidity_wads: Decimal::zero(),
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, _recent_blockhash) = test.start().await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, 0);
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, 0);
|
||||
|
||||
let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * SOL_COLLATERAL_AMOUNT_LAMPORTS;
|
||||
lending_market
|
||||
.borrow(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
BorrowArgs {
|
||||
deposit_reserve: &sol_reserve,
|
||||
borrow_reserve: &usdc_reserve,
|
||||
dex_market: &sol_usdc_dex_market,
|
||||
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
|
||||
amount: collateral_deposit_amount,
|
||||
user_accounts_owner: &user_accounts_owner,
|
||||
obligation: &usdc_obligation,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL);
|
||||
|
||||
let borrow_fees = TEST_RESERVE_CONFIG
|
||||
.fees
|
||||
.calculate_borrow_fees(collateral_deposit_amount)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees);
|
||||
|
||||
lending_market
|
||||
.borrow(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
BorrowArgs {
|
||||
deposit_reserve: &sol_reserve,
|
||||
borrow_reserve: &usdc_reserve,
|
||||
dex_market: &sol_usdc_dex_market,
|
||||
borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount,
|
||||
amount: borrow_amount,
|
||||
user_accounts_owner: &user_accounts_owner,
|
||||
obligation: &usdc_obligation,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, 2 * USDC_BORROW_AMOUNT_FRACTIONAL);
|
||||
|
||||
let user_collateral_balance =
|
||||
get_token_balance(&mut banks_client, sol_reserve.user_collateral_account).await;
|
||||
assert_eq!(user_collateral_balance, 0);
|
||||
|
||||
let collateral_deposited = 2 * collateral_deposit_amount;
|
||||
let (total_fee, host_fee) = TEST_RESERVE_CONFIG
|
||||
.fees
|
||||
.calculate_borrow_fees(collateral_deposited)
|
||||
.unwrap();
|
||||
|
||||
assert!(total_fee > 0);
|
||||
assert!(host_fee > 0);
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, sol_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, collateral_deposited - total_fee);
|
||||
|
||||
let fee_balance =
|
||||
get_token_balance(&mut banks_client, sol_reserve.collateral_fees_receiver).await;
|
||||
assert_eq!(fee_balance, total_fee - host_fee);
|
||||
|
||||
let host_fee_balance = get_token_balance(&mut banks_client, sol_reserve.collateral_host).await;
|
||||
assert_eq!(host_fee_balance, host_fee);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_base_currency() {
|
||||
// Using SOL/USDC min 2 asks:
|
||||
// $14.074, 4707.1 SOL
|
||||
// $14.055, 1751.7 SOL
|
||||
// $13.989, 12.1 SOL
|
||||
//
|
||||
// Borrow amount = 2000 SOL
|
||||
// Collateral amount = 13.989 * 12.1 + 14.055 * 1751.7 + 14.074 * 236.2 = 28,113.6892 USDC
|
||||
const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 2000 * LAMPORTS_TO_SOL;
|
||||
const USDC_COLLATERAL_LAMPORTS: u64 = 28_113_689_200;
|
||||
const INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS: u64 = 5000 * LAMPORTS_TO_SOL;
|
||||
const INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL: u64 = 2 * USDC_COLLATERAL_LAMPORTS;
|
||||
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(230_000);
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let sol_usdc_dex_market = TestDexMarket::setup(&mut test, TestDexMarketPair::SOL_USDC);
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 100;
|
||||
|
||||
let usdc_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: INITIAL_USDC_RESERVE_SUPPLY_FRACTIONAL,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let sol_reserve = add_reserve(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddReserveArgs {
|
||||
dex_market_pubkey: Some(sol_usdc_dex_market.pubkey),
|
||||
liquidity_amount: INITIAL_SOL_RESERVE_SUPPLY_LAMPORTS,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
liquidity_mint_decimals: 9,
|
||||
config: reserve_config,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let sol_obligation = add_obligation(
|
||||
&mut test,
|
||||
&user_accounts_owner,
|
||||
&lending_market,
|
||||
AddObligationArgs {
|
||||
borrow_reserve: &sol_reserve,
|
||||
collateral_reserve: &usdc_reserve,
|
||||
collateral_amount: 0,
|
||||
borrowed_liquidity_wads: Decimal::zero(),
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, _recent_blockhash) = test.start().await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, 0);
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, 0);
|
||||
|
||||
let collateral_deposit_amount = INITIAL_COLLATERAL_RATIO * USDC_COLLATERAL_LAMPORTS;
|
||||
lending_market
|
||||
.borrow(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
BorrowArgs {
|
||||
deposit_reserve: &usdc_reserve,
|
||||
borrow_reserve: &sol_reserve,
|
||||
dex_market: &sol_usdc_dex_market,
|
||||
borrow_amount_type: BorrowAmountType::CollateralDepositAmount,
|
||||
amount: collateral_deposit_amount,
|
||||
user_accounts_owner: &user_accounts_owner,
|
||||
obligation: &sol_obligation,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS);
|
||||
|
||||
let borrow_fees = TEST_RESERVE_CONFIG
|
||||
.fees
|
||||
.calculate_borrow_fees(collateral_deposit_amount)
|
||||
.unwrap()
|
||||
.0;
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, collateral_deposit_amount - borrow_fees);
|
||||
|
||||
lending_market
|
||||
.borrow(
|
||||
&mut banks_client,
|
||||
&payer,
|
||||
BorrowArgs {
|
||||
deposit_reserve: &usdc_reserve,
|
||||
borrow_reserve: &sol_reserve,
|
||||
dex_market: &sol_usdc_dex_market,
|
||||
borrow_amount_type: BorrowAmountType::LiquidityBorrowAmount,
|
||||
amount: borrow_amount,
|
||||
user_accounts_owner: &user_accounts_owner,
|
||||
obligation: &sol_obligation,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, sol_reserve.user_liquidity_account).await;
|
||||
assert_eq!(borrow_amount, 2 * SOL_BORROW_AMOUNT_LAMPORTS);
|
||||
|
||||
let (mut total_fee, mut host_fee) = TEST_RESERVE_CONFIG
|
||||
.fees
|
||||
.calculate_borrow_fees(collateral_deposit_amount)
|
||||
.unwrap();
|
||||
|
||||
// avoid rounding error by assessing fees individually
|
||||
total_fee *= 2;
|
||||
host_fee *= 2;
|
||||
|
||||
assert!(total_fee > 0);
|
||||
assert!(host_fee > 0);
|
||||
|
||||
let collateral_supply =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.collateral_supply).await;
|
||||
assert_eq!(collateral_supply, 2 * collateral_deposit_amount - total_fee);
|
||||
|
||||
let fee_balance =
|
||||
get_token_balance(&mut banks_client, usdc_reserve.collateral_fees_receiver).await;
|
||||
assert_eq!(fee_balance, total_fee - host_fee);
|
||||
|
||||
let host_fee_balance = get_token_balance(&mut banks_client, usdc_reserve.collateral_host).await;
|
||||
assert_eq!(host_fee_balance, host_fee);
|
||||
}
|
|
@ -0,0 +1,400 @@
|
|||
#![cfg(feature = "test-bpf")]
|
||||
|
||||
mod helpers;
|
||||
|
||||
use helpers::*;
|
||||
use solana_program_test::*;
|
||||
use solana_sdk::{
|
||||
instruction::InstructionError,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
transaction::{Transaction, TransactionError},
|
||||
};
|
||||
use spl_token_lending::{
|
||||
error::LendingError,
|
||||
instruction::{borrow_obligation_liquidity, refresh_obligation},
|
||||
math::Decimal,
|
||||
processor::process_instruction,
|
||||
state::{FeeCalculation, INITIAL_COLLATERAL_RATIO},
|
||||
};
|
||||
use std::u64;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_quote_currency() {
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(41_000);
|
||||
|
||||
const USDC_TOTAL_BORROW_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC;
|
||||
const FEE_AMOUNT: u64 = 100;
|
||||
const HOST_FEE_AMOUNT: u64 = 20;
|
||||
|
||||
const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO;
|
||||
const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = USDC_TOTAL_BORROW_FRACTIONAL - FEE_AMOUNT;
|
||||
const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS;
|
||||
const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_TOTAL_BORROW_FRACTIONAL;
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 50;
|
||||
|
||||
let sol_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
liquidity_mint_decimals: 9,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let usdc_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let test_obligation = add_obligation(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddObligationArgs {
|
||||
deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)],
|
||||
..AddObligationArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let initial_liquidity_supply =
|
||||
get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
refresh_obligation(
|
||||
spl_token_lending::id(),
|
||||
test_obligation.pubkey,
|
||||
vec![sol_test_reserve.pubkey],
|
||||
),
|
||||
borrow_obligation_liquidity(
|
||||
spl_token_lending::id(),
|
||||
USDC_BORROW_AMOUNT_FRACTIONAL,
|
||||
usdc_test_reserve.liquidity_supply_pubkey,
|
||||
usdc_test_reserve.user_liquidity_pubkey,
|
||||
usdc_test_reserve.pubkey,
|
||||
usdc_test_reserve.liquidity_fee_receiver_pubkey,
|
||||
test_obligation.pubkey,
|
||||
lending_market.pubkey,
|
||||
test_obligation.owner,
|
||||
Some(usdc_test_reserve.liquidity_host_pubkey),
|
||||
),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let usdc_reserve = usdc_test_reserve.get_state(&mut banks_client).await;
|
||||
let obligation = test_obligation.get_state(&mut banks_client).await;
|
||||
|
||||
let (total_fee, host_fee) = usdc_reserve
|
||||
.config
|
||||
.fees
|
||||
.calculate_borrow_fees(
|
||||
USDC_BORROW_AMOUNT_FRACTIONAL.into(),
|
||||
FeeCalculation::Exclusive,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(total_fee, FEE_AMOUNT);
|
||||
assert_eq!(host_fee, HOST_FEE_AMOUNT);
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, usdc_test_reserve.user_liquidity_pubkey).await;
|
||||
assert_eq!(borrow_amount, USDC_BORROW_AMOUNT_FRACTIONAL);
|
||||
|
||||
let liquidity = &obligation.borrows[0];
|
||||
assert_eq!(
|
||||
liquidity.borrowed_amount_wads,
|
||||
Decimal::from(USDC_TOTAL_BORROW_FRACTIONAL)
|
||||
);
|
||||
assert_eq!(
|
||||
usdc_reserve.liquidity.borrowed_amount_wads,
|
||||
liquidity.borrowed_amount_wads
|
||||
);
|
||||
|
||||
let liquidity_supply =
|
||||
get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_supply_pubkey).await;
|
||||
assert_eq!(
|
||||
liquidity_supply,
|
||||
initial_liquidity_supply - USDC_TOTAL_BORROW_FRACTIONAL
|
||||
);
|
||||
|
||||
let fee_balance = get_token_balance(
|
||||
&mut banks_client,
|
||||
usdc_test_reserve.liquidity_fee_receiver_pubkey,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT);
|
||||
|
||||
let host_fee_balance =
|
||||
get_token_balance(&mut banks_client, usdc_test_reserve.liquidity_host_pubkey).await;
|
||||
assert_eq!(host_fee_balance, HOST_FEE_AMOUNT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_max_base_currency() {
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
// limit to track compute unit increase
|
||||
test.set_bpf_compute_max_units(42_000);
|
||||
|
||||
const FEE_AMOUNT: u64 = 5000;
|
||||
const HOST_FEE_AMOUNT: u64 = 1000;
|
||||
|
||||
const USDC_DEPOSIT_AMOUNT_FRACTIONAL: u64 =
|
||||
2_000 * FRACTIONAL_TO_USDC * INITIAL_COLLATERAL_RATIO;
|
||||
const SOL_BORROW_AMOUNT_LAMPORTS: u64 = 50 * LAMPORTS_TO_SOL;
|
||||
const USDC_RESERVE_COLLATERAL_FRACTIONAL: u64 = 2 * USDC_DEPOSIT_AMOUNT_FRACTIONAL;
|
||||
const SOL_RESERVE_LIQUIDITY_LAMPORTS: u64 = 2 * SOL_BORROW_AMOUNT_LAMPORTS;
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 50;
|
||||
|
||||
let usdc_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: USDC_RESERVE_COLLATERAL_FRACTIONAL,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let sol_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: SOL_RESERVE_LIQUIDITY_LAMPORTS,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
liquidity_mint_decimals: 9,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let test_obligation = add_obligation(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddObligationArgs {
|
||||
deposits: &[(&usdc_test_reserve, USDC_DEPOSIT_AMOUNT_FRACTIONAL)],
|
||||
..AddObligationArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let initial_liquidity_supply =
|
||||
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
refresh_obligation(
|
||||
spl_token_lending::id(),
|
||||
test_obligation.pubkey,
|
||||
vec![usdc_test_reserve.pubkey],
|
||||
),
|
||||
borrow_obligation_liquidity(
|
||||
spl_token_lending::id(),
|
||||
u64::MAX,
|
||||
sol_test_reserve.liquidity_supply_pubkey,
|
||||
sol_test_reserve.user_liquidity_pubkey,
|
||||
sol_test_reserve.pubkey,
|
||||
sol_test_reserve.liquidity_fee_receiver_pubkey,
|
||||
test_obligation.pubkey,
|
||||
lending_market.pubkey,
|
||||
test_obligation.owner,
|
||||
Some(sol_test_reserve.liquidity_host_pubkey),
|
||||
),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
|
||||
assert!(banks_client.process_transaction(transaction).await.is_ok());
|
||||
|
||||
let sol_reserve = sol_test_reserve.get_state(&mut banks_client).await;
|
||||
let obligation = test_obligation.get_state(&mut banks_client).await;
|
||||
|
||||
let (total_fee, host_fee) = sol_reserve
|
||||
.config
|
||||
.fees
|
||||
.calculate_borrow_fees(SOL_BORROW_AMOUNT_LAMPORTS.into(), FeeCalculation::Inclusive)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(total_fee, FEE_AMOUNT);
|
||||
assert_eq!(host_fee, HOST_FEE_AMOUNT);
|
||||
|
||||
let borrow_amount =
|
||||
get_token_balance(&mut banks_client, sol_test_reserve.user_liquidity_pubkey).await;
|
||||
assert_eq!(borrow_amount, SOL_BORROW_AMOUNT_LAMPORTS - FEE_AMOUNT);
|
||||
|
||||
let liquidity = &obligation.borrows[0];
|
||||
assert_eq!(
|
||||
liquidity.borrowed_amount_wads,
|
||||
Decimal::from(SOL_BORROW_AMOUNT_LAMPORTS)
|
||||
);
|
||||
|
||||
let liquidity_supply =
|
||||
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_supply_pubkey).await;
|
||||
assert_eq!(
|
||||
liquidity_supply,
|
||||
initial_liquidity_supply - SOL_BORROW_AMOUNT_LAMPORTS
|
||||
);
|
||||
|
||||
let fee_balance = get_token_balance(
|
||||
&mut banks_client,
|
||||
sol_test_reserve.liquidity_fee_receiver_pubkey,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(fee_balance, FEE_AMOUNT - HOST_FEE_AMOUNT);
|
||||
|
||||
let host_fee_balance =
|
||||
get_token_balance(&mut banks_client, sol_test_reserve.liquidity_host_pubkey).await;
|
||||
assert_eq!(host_fee_balance, HOST_FEE_AMOUNT);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_borrow_too_large() {
|
||||
let mut test = ProgramTest::new(
|
||||
"spl_token_lending",
|
||||
spl_token_lending::id(),
|
||||
processor!(process_instruction),
|
||||
);
|
||||
|
||||
const SOL_DEPOSIT_AMOUNT_LAMPORTS: u64 = 100 * LAMPORTS_TO_SOL * INITIAL_COLLATERAL_RATIO;
|
||||
const USDC_BORROW_AMOUNT_FRACTIONAL: u64 = 1_000 * FRACTIONAL_TO_USDC + 1;
|
||||
const SOL_RESERVE_COLLATERAL_LAMPORTS: u64 = 2 * SOL_DEPOSIT_AMOUNT_LAMPORTS;
|
||||
const USDC_RESERVE_LIQUIDITY_FRACTIONAL: u64 = 2 * USDC_BORROW_AMOUNT_FRACTIONAL;
|
||||
|
||||
let user_accounts_owner = Keypair::new();
|
||||
let usdc_mint = add_usdc_mint(&mut test);
|
||||
let lending_market = add_lending_market(&mut test, usdc_mint.pubkey);
|
||||
|
||||
let mut reserve_config = TEST_RESERVE_CONFIG;
|
||||
reserve_config.loan_to_value_ratio = 50;
|
||||
|
||||
let sol_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
collateral_amount: SOL_RESERVE_COLLATERAL_LAMPORTS,
|
||||
liquidity_mint_pubkey: spl_token::native_mint::id(),
|
||||
liquidity_mint_decimals: 9,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let usdc_test_reserve = add_reserve(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddReserveArgs {
|
||||
liquidity_amount: USDC_RESERVE_LIQUIDITY_FRACTIONAL,
|
||||
liquidity_mint_pubkey: usdc_mint.pubkey,
|
||||
liquidity_mint_decimals: usdc_mint.decimals,
|
||||
config: reserve_config,
|
||||
mark_fresh: true,
|
||||
..AddReserveArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let test_obligation = add_obligation(
|
||||
&mut test,
|
||||
&lending_market,
|
||||
&user_accounts_owner,
|
||||
AddObligationArgs {
|
||||
deposits: &[(&sol_test_reserve, SOL_DEPOSIT_AMOUNT_LAMPORTS)],
|
||||
..AddObligationArgs::default()
|
||||
},
|
||||
);
|
||||
|
||||
let (mut banks_client, payer, recent_blockhash) = test.start().await;
|
||||
|
||||
let mut transaction = Transaction::new_with_payer(
|
||||
&[
|
||||
refresh_obligation(
|
||||
spl_token_lending::id(),
|
||||
test_obligation.pubkey,
|
||||
vec![sol_test_reserve.pubkey],
|
||||
),
|
||||
borrow_obligation_liquidity(
|
||||
spl_token_lending::id(),
|
||||
USDC_BORROW_AMOUNT_FRACTIONAL,
|
||||
usdc_test_reserve.liquidity_supply_pubkey,
|
||||
usdc_test_reserve.user_liquidity_pubkey,
|
||||
usdc_test_reserve.pubkey,
|
||||
usdc_test_reserve.liquidity_fee_receiver_pubkey,
|
||||
test_obligation.pubkey,
|
||||
lending_market.pubkey,
|
||||
test_obligation.owner,
|
||||
Some(usdc_test_reserve.liquidity_host_pubkey),
|
||||
),
|
||||
],
|
||||
Some(&payer.pubkey()),
|
||||
);
|
||||
|
||||
transaction.sign(&[&payer, &user_accounts_owner], recent_blockhash);
|
||||
|
||||
// check that transaction fails
|
||||
assert_eq!(
|
||||
banks_client
|
||||
.process_transaction(transaction)
|
||||
.await
|
||||
.unwrap_err()
|
||||
.unwrap(),
|
||||
TransactionError::InstructionError(
|
||||
1,
|
||||
InstructionError::Custom(LendingError::BorrowTooLarge as u32)
|
||||
)
|
||||
);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue