Compare commits

..

No commits in common. "@solana/spl-token@v0.1.5" and "master" have entirely different histories.

251 changed files with 19556 additions and 51758 deletions

View File

@ -10,34 +10,7 @@ on:
- 'docs/**'
jobs:
check_non_docs:
outputs:
run_all_github_action_checks: ${{ steps.check_files.outputs.run_all_github_action_checks }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
with:
fetch-depth: 2
- name: check modified files
id: check_files
run: |
echo "========== check paths of modified files =========="
echo "::set-output name=run_all_github_action_checks::true"
git diff --name-only HEAD^ HEAD > files.txt
while IFS= read -r file
do
if [[ $file != docs/** ]]; then
echo "Found modified non-'docs' file(s)"
echo "::set-output name=run_all_github_action_checks::false"
break
fi
done < files.txt
all_github_action_checks:
runs-on: ubuntu-latest
needs: check_non_docs
if: needs.check_non_docs.outputs.run_all_github_action_checks == 'true'
steps:
- run: echo "Done"

2
.gitignore vendored
View File

@ -8,5 +8,3 @@ node_modules
hfuzz_target
hfuzz_workspace
**/*.so
**/.DS_Store
test-ledger

1219
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -9,15 +9,14 @@ members = [
"examples/rust/transfer-lamports",
"feature-proposal/program",
"feature-proposal/cli",
"governance/program",
"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",
@ -30,6 +29,3 @@ 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"

View File

@ -12,12 +12,12 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -13,7 +13,7 @@ test-bpf = []
[dependencies]
num-derive = "0.3"
num-traits = "0.2"
solana-program = "1.6.7"
solana-program = "1.6.2"
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.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -4,10 +4,13 @@ set -ex
cd "$(dirname "$0")/.."
source ./ci/solana-version.sh install
(cd token/js && npm install)
cd token-swap/js
npm install
npm run lint
npm run build
npm run flow
npx tsc module.d.ts
npm run start-with-test-validator
(cd ../../target/deploy && mv spl_token_swap_production.so spl_token_swap.so)
SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" npm run start-with-test-validator

View File

@ -18,13 +18,13 @@
if [[ -n $RUST_STABLE_VERSION ]]; then
stable_version="$RUST_STABLE_VERSION"
else
stable_version=1.52.1
stable_version=1.50.0
fi
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
nightly_version="$RUST_NIGHTLY_VERSION"
else
nightly_version=2021-04-18
nightly_version=2021-02-18
fi

View File

@ -14,7 +14,7 @@
if [[ -n $SOLANA_VERSION ]]; then
solana_version="$SOLANA_VERSION"
else
solana_version=v1.6.7
solana_version=v1.5.15
fi
export solana_version="$solana_version"

View File

@ -8,7 +8,6 @@ module.exports = {
"token-lending",
"associated-token-account",
"memo",
"name-service",
"shared-memory",
"stake-pool",
"feature-proposal",

View File

@ -1,57 +0,0 @@
---
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 by 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 arbitrarily 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 the
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
able to modify the data of his Twitter name registry.

View File

@ -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
validators 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 staker, allowing SOL holders to
program pools together SOL to be staked by a manager, allowing SOL holders to
stake and earn rewards without managing stakes.
Additional information regarding staking and stake programming is available at:
@ -24,18 +24,16 @@ Additional information regarding staking and stake programming is available at:
## Motivation
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
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.
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, stakers, and users must be comfortable with
creating and delegating stakes, which are more advanced operations than sending and
This means that stake pool managers 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:
@ -48,28 +46,27 @@ like [Token Swap](token-swap.md).
## Operation
A stake pool manager creates a stake pool, and the staker includes validators that will
A stake pool manager creates a stake pool and 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 staker adds it to the stake pool.
active, the stake pool manager 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 rewards
earned by the pool goes to the pool manager as a fee.
representing their fractional ownership in pool. A percentage of the user's
deposit goes to the pool manager as a fee.
Over time, as the stakes in the stake pool accrue staking rewards, the user's fractional
Over time, as the stake pool accrues 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 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.
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 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.
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.
## Background
@ -133,105 +130,32 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
solana config set --keypair usb://ledger/
```
#### 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
### Stake Pool Administrator Examples
#### Create a stake pool
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:
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:
```sh
$ 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
$ 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
```
The unique stake pool identifier is `EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1`.
The unique stake pool identifier is `3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC`.
The identifier for the SPL token for staking derivatives is
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ`. The stake pool has full control
`Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS`. The stake pool has full control
over the mint.
The pool creator's fee account identifier is
`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
`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.
#### Create a validator stake account
@ -246,7 +170,7 @@ lists, we choose some validators at random and start with identity
delegated to that vote account.
```sh
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
```
@ -255,13 +179,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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
```
@ -311,21 +235,22 @@ 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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
$ spl-stake-pool add-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Creating account to receive tokens Gu8xqzYFg2sPHWHhUivKNBeF9uikiauihLs9hLzziKu7
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
`FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN` in this example. You can also
`FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` 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: 1.000000000 SOL
Active Stake: 1.000000000 SOL
Delegated Stake: 0.000000001 SOL
Active Stake: 0.000000001 SOL
Activating Stake: 0 SOL
Stake activates starting from epoch: 161
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
@ -335,31 +260,26 @@ Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
#### Remove validator stake account
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.
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`.
```sh
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
$ spl-stake-pool remove-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
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. 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
```
removed from the stake pool and now belongs to the administrator.
We can check the removed stake account:
```sh
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
Balance: 1.002282880 SOL
Balance: 1.002282881 SOL
Rent Exempt Reserve: 0.00228288 SOL
Delegated Stake: 1.000000000 SOL
Active Stake: 1.000000000 SOL
Delegated Stake: 1.000000001 SOL
Active Stake: 1.000000001 SOL
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
@ -371,7 +291,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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
@ -380,14 +300,14 @@ Total: ◎15.849959206
#### Rebalance the stake pool
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.
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.
For example, let's say the staker wants the same delegation to every validator
For example, let's say the manager 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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
@ -395,63 +315,75 @@ Total: ◎15.849959206
```
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
has too much allocated. For their strategy, the staker wants the `15.849959206`
has too much allocated. For their strategy, the manager 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`.
##### 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:
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:
```sh
$ spl-stake-pool decrease-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz 6.153483916
Signature: ZpQGwT85rJ8Y9afdkXhKo3TVv4xgTz741mmZj2vW7mihYseAkFsazWxza2y8eNGY4HDJm15c1cStwyiQzaM3RpH
$ spl-token supply Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
0.034692168
```
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.
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:
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.
##### 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 increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm 4.281036854
Signature: 3GJACzjUGLPjcd9RLUW86AfBLWKapZRkxnEMc2yHT6erYtcKBgCapzyrVH6VN8Utxj7e2mtvzcigwLm6ZafXyTMw
```
sol_to_withdraw * total_pool_tokens / total_sol_staked = pool_tokens_to_withdraw
6.153483916 * 0.034692168 / 15.849959206 ~ 0.013468659
```
And they add 1.872447062 SOL to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`:
They withdraw that amount of pool tokens:
```sh
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 1.872447062
Signature: 4zaKYu3MQ3as8reLbuHKaXN8FNaHvpHuiZtsJeARo67UKMo6wUUoWE88Fy8N4EYQYicuwULTNffcUD3a9jY88PoU
$ 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
```
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.
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:
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:
```
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:
```sh
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
$ solana deactivate-stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
Signature: 4SuwZK5JvYkYVkM5yfu2x8x6iou6558teMwzphGECLmstMVoWbSvngUH48Ra24PrxtgUDyVDA8SXYS1qMyx3fjMj
```
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.
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!
```sh
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
@ -459,7 +391,33 @@ Total: ◎15.851269888
```
Due to staking rewards that accrued during the rebalancing process, the pool is
not perfectly balanced. This is completely normal.
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
```
### User Examples
@ -471,7 +429,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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
@ -483,13 +441,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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
No accounts found.
```
#### Deposit stake
Stake pools only accept deposits from active accounts, so we must first
Stake pools only accept deposits from fully staked 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
@ -515,19 +473,17 @@ 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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
```
@ -549,8 +505,7 @@ 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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Updating stake pool...
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
```
@ -558,33 +513,13 @@ If another user already updated the stake pool balance for the current epoch, we
see a different output.
```sh
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
Update not required
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
Stake pool balances are up to date, no update 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. 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
```
can run it before depositing or withdrawing.
#### Withdraw stake
@ -594,7 +529,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 EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
@ -615,58 +550,15 @@ Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
```
Alternatively, the user can specify an existing uninitialized stake account to
receive their stake using the `--stake-receiver` parameter.
Alternatively, the user can specify an existing stake account to receive their
stake using the `stake-receiver` parameter.
```sh
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --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
@ -677,22 +569,6 @@ 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

View File

@ -83,15 +83,6 @@ 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
@ -118,7 +109,7 @@ Signature: 42Sa5eK9dMEQyvD9GMHuKxXf55WLZ7tfjabUKDhNoZRAxj9MsnN7omriWMEHXLea3aYpj
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
```sh
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
0
```
@ -135,7 +126,7 @@ The token `supply` and account `balance` now reflect the result of minting:
```sh
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
100
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
100
```
@ -175,7 +166,7 @@ address by running `solana address` and provides it to the sender.
The sender then runs:
```
$ spl-token transfer AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
Transfer 50 tokens
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
@ -193,7 +184,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 AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
Transfer 50 tokens
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
@ -237,9 +228,9 @@ CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe
### Example: Create a non-fungible token
Create the token type with nine decimal places,
Create the token type,
```
$ spl-token create-token --decimals 9
$ spl-token create-token
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
```
@ -273,7 +264,7 @@ Now the `7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM` account holds the
one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token:
```
$ spl-token account-info 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
$ spl-token account-info 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
Balance: 1
@ -529,7 +520,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 `getProgramAccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
Additionally the versatile `getProgramAcccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
### Finding all token accounts for a specific mint
@ -562,7 +553,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
```
The `"dataSize": 165` filter selects all [Token
Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
and then the `"memcmp": ...` filter selects based on the
[mint](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L88)
address within each token account.
@ -597,7 +588,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
```
The `"dataSize": 165` filter selects all [Token
Account](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
and then the `"memcmp": ...` filter selects based on the
[owner](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L90)
address within each token account.
@ -861,13 +852,3 @@ the maximum allowed transaction size, remove those extra clean up instructions.
They can be cleaned up during the next send operation.
The `spl-token gc` command provides an example implementation of this cleanup process.
### Token Vesting Contract:
This program allows you to lock arbitrary SPL tokens and release the locked tokens with a determined unlock schedule. An `unlock schedule` is made of a `unix timestamp` and a token `amount`, when initializing a vesting contract, the creator can pass an array of `unlock schedule` with an arbitrary size giving the creator of the contract complete control of how the tokens unlock over time.
Unlocking works by pushing a permissionless crank on the contract that moves the tokens to the pre-specified address. The recipient address of a vesting contract can be modified by the owner of the current recipient key, meaning that vesting contract locked tokens can be traded.
- Code: [https://github.com/Bonfida/token-vesting](https://github.com/Bonfida/token-vesting)
- UI: [https://vesting.bonfida.com/#/](https://vesting.bonfida.com/#/)
- Audit: The audit was conducted by Kudelski, the report can be found [here](https://github.com/Bonfida/token-vesting/blob/master/audit/Bonfida_SecurityAssessment_Vesting_Final050521.pdf)

View File

@ -22,7 +22,7 @@ extern uint64_t do_invoke(SolParameters *params) {
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
SolPubkey expected_allocated_key;
if (SUCCESS != sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds),
if (SUCCESS == sol_create_program_address(seeds, SOL_ARRAY_SIZE(seeds),
params->program_id,
&expected_allocated_key)) {
return ERROR_INVALID_INSTRUCTION_DATA;
@ -31,7 +31,8 @@ extern uint64_t do_invoke(SolParameters *params) {
return ERROR_INVALID_ARGUMENT;
}
SolAccountMeta arguments[] = {{allocated_info->key, true, true}};
SolAccountMeta arguments[] = {{system_program_info->key, false, false},
{allocated_info->key, true, true}};
uint8_t data[4 + 8]; // Enough room for the Allocate instruction
*(uint16_t *)data = 8; // Allocate instruction enum value
*(uint64_t *)(data + 4) = SIZE; // Size to allocate

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -15,11 +15,11 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -13,11 +13,11 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -17,28 +17,24 @@ pub fn process_instruction(
// Create in iterator to safety reference accounts in the slice
let account_info_iter = &mut accounts.iter();
// Get the clock sysvar via syscall
let clock_via_sysvar = Clock::get()?;
// Or deserialize the account into a clock struct
// The first account is the clock sysvar
let clock_sysvar_info = next_account_info(account_info_iter)?;
let clock_via_account = Clock::from_account_info(&clock_sysvar_info)?;
// Both produce the same sysvar
assert_eq!(clock_via_sysvar, clock_via_account);
// Note: `format!` can be very expensive, use cautiously
msg!("{:?}", clock_via_sysvar);
// Get the rent sysvar via syscall
let rent_via_sysvar = Rent::get()?;
// Or deserialize the account into a rent struct
// The second account is the rent sysvar
let rent_sysvar_info = next_account_info(account_info_iter)?;
let rent_via_account = Rent::from_account_info(&rent_sysvar_info)?;
// Both produce the same sysvar
assert_eq!(rent_via_sysvar, rent_via_account);
// Deserialize the account into a clock struct
let clock = Clock::from_account_info(&clock_sysvar_info)?;
// Deserialize the account into a rent struct
let rent = Rent::from_account_info(&rent_sysvar_info)?;
// Note: `format!` can be very expensive, use cautiously
msg!("{:?}", clock);
// Can't print `exemption_threshold` because BPF does not support printing floats
msg!(
"Rent: lamports_per_byte_year: {:?}, burn_percent: {:?}",
rent_via_sysvar.lamports_per_byte_year,
rent_via_sysvar.burn_percent
rent.lamports_per_byte_year,
rent.burn_percent
);
Ok(())

View File

@ -12,11 +12,11 @@ no-entrypoint = []
test-bpf = []
[dependencies]
solana-program = "1.6.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -10,11 +10,11 @@ edition = "2018"
[dependencies]
chrono = "0.4.19"
clap = "2.33.3"
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-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"
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
[[bin]]

View File

@ -12,14 +12,15 @@ no-entrypoint = []
test-bpf = []
[dependencies]
borsh = "0.8"
borsh = "0.7.1"
borsh-derive = "0.8.1"
solana-program = "1.6.7"
solana-program = "1.6.2"
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
futures = "0.3"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -155,12 +155,13 @@ 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(),
solana_program::borsh::get_packed_len::<FeatureProposalInstruction>()
borsh_utils::get_packed_len::<FeatureProposalInstruction>()
)
}

View File

@ -2,6 +2,7 @@
#![deny(missing_docs)]
#![forbid(unsafe_code)]
pub mod borsh_utils;
mod entrypoint;
pub mod instruction;
pub mod processor;

View File

@ -59,12 +59,13 @@ 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(),
solana_program::borsh::get_packed_len::<FeatureProposal>()
borsh_utils::get_packed_len::<FeatureProposal>()
);
}

View File

@ -1,20 +1,21 @@
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
#![cfg(feature = "test-bpf")]
use {
solana_program::{
feature::{self, Feature},
program_option::COption,
pubkey::Pubkey,
system_program,
},
solana_program_test::*,
solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
},
spl_feature_proposal::{instruction::*, state::*, *},
use futures::{Future, FutureExt};
use solana_program::{
feature::{self, Feature},
program_option::COption,
program_pack::Pack,
pubkey::Pubkey,
system_program,
};
use solana_program_test::*;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use spl_feature_proposal::{instruction::*, state::*, *};
use std::io;
fn program_test() -> ProgramTest {
ProgramTest::new(
@ -24,6 +25,21 @@ fn program_test() -> ProgramTest {
)
}
/// Fetch and unpack account data
fn get_account_data<T: Pack>(
banks_client: &mut BanksClient,
address: Pubkey,
) -> impl Future<Output = std::io::Result<T>> + '_ {
banks_client.get_account(address).map(|result| {
let account =
result?.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "account not found"))?;
T::unpack_from_slice(&account.data)
.ok()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Failed to deserialize account"))
})
}
#[tokio::test]
async fn test_basic() {
let feature_proposal = Keypair::new();
@ -52,17 +68,16 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is now funded and allocated, but not assigned
let feature_id_account = banks_client
let feature_id_acccount = banks_client
.get_account(feature_id_address)
.await
.expect("success")
.expect("some account");
assert_eq!(feature_id_account.owner, system_program::id());
assert_eq!(feature_id_account.data.len(), Feature::size_of());
assert_eq!(feature_id_acccount.owner, system_program::id());
assert_eq!(feature_id_acccount.data.len(), Feature::size_of());
// Confirm mint account state
let mint = banks_client
.get_packed_account_data::<spl_token::state::Mint>(mint_address)
let mint = get_account_data::<spl_token::state::Mint>(&mut banks_client, mint_address)
.await
.unwrap();
assert_eq!(mint.supply, 42);
@ -71,20 +86,20 @@ async fn test_basic() {
assert_eq!(mint.mint_authority, COption::Some(mint_address));
// Confirm distributor token account state
let distributor_token = banks_client
.get_packed_account_data::<spl_token::state::Account>(distributor_token_address)
.await
.unwrap();
let distributor_token =
get_account_data::<spl_token::state::Account>(&mut banks_client, distributor_token_address)
.await
.unwrap();
assert_eq!(distributor_token.amount, 42);
assert_eq!(distributor_token.mint, mint_address);
assert_eq!(distributor_token.owner, feature_proposal.pubkey());
assert!(distributor_token.close_authority.is_none());
// Confirm acceptance token account state
let acceptance_token = banks_client
.get_packed_account_data::<spl_token::state::Account>(acceptance_token_address)
.await
.unwrap();
let acceptance_token =
get_account_data::<spl_token::state::Account>(&mut banks_client, acceptance_token_address)
.await
.unwrap();
assert_eq!(acceptance_token.amount, 0);
assert_eq!(acceptance_token.mint, mint_address);
assert_eq!(acceptance_token.owner, id());
@ -100,17 +115,15 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is not yet assigned
let feature_id_account = banks_client
let feature_id_acccount = banks_client
.get_account(feature_id_address)
.await
.expect("success")
.expect("some account");
assert_eq!(feature_id_account.owner, system_program::id());
assert_eq!(feature_id_acccount.owner, system_program::id());
assert!(matches!(
banks_client
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
.await,
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
Ok(FeatureProposal::Pending(_))
));
@ -145,18 +158,16 @@ async fn test_basic() {
banks_client.process_transaction(transaction).await.unwrap();
// Confirm feature id account is now assigned
let feature_id_account = banks_client
let feature_id_acccount = banks_client
.get_account(feature_id_address)
.await
.expect("success")
.expect("some account");
assert_eq!(feature_id_account.owner, feature::id());
assert_eq!(feature_id_acccount.owner, feature::id());
// Confirm feature proposal account state
assert!(matches!(
banks_client
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
.await,
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
Ok(FeatureProposal::Accepted {
tokens_upon_acceptance: 42
})
@ -186,9 +197,7 @@ async fn test_expired() {
banks_client.process_transaction(transaction).await.unwrap();
assert!(matches!(
banks_client
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
.await,
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
Ok(FeatureProposal::Pending(_))
));
@ -199,9 +208,7 @@ async fn test_expired() {
banks_client.process_transaction(transaction).await.unwrap();
assert!(matches!(
banks_client
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
.await,
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
Ok(FeatureProposal::Expired)
));
}

View File

@ -1,96 +0,0 @@
# Governance
Governance is a program the chief purpose of which is to control the upgrade of other programs through democratic means.
It can also be used as an authority provider for mints and other forms of access control as well where we may want
a voting population to vote on disbursement of access or funds collectively.
## Architecture
### Accounts diagram
![Accounts diagram](./resources/governance-accounts.jpg)
### Governance Realm account
Governance Realm ties Community Token Mint and optional Council Token mint to create a realm
for any governance pertaining to the community of the token holders.
For example a trading protocol can issue a governance token and use it to create its governance realm.
Once a realm is created voters can deposit Governing tokens (Community or Council) to the realm and
use the deposited amount as their voting weight to vote on Proposals within that realm.
### Program Governance account
The basic building block of governance to update programs is the ProgramGovernance account.
It ties a governed Program ID and holds configuration options defining governance rules.
The governed Program ID is used as the seed for a [Program Derived Address](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses),
and this program derived address is what is used as the address of the Governance account for your Program ID
and the corresponding Governance mint and Council mint (if provided).
What this means is that there can only ever be ONE Governance account for a given Program.
The governance program validates at creation time of the Governance account that the current upgrade authority of the program
taken under governance signed the transaction.
Note: In future versions, once allowed in solana runtime, the governance program will take over the upgrade authority
of the governed program when the Governance account is created.
### How does authority work?
Governance can handle arbitrary executions of code, but it's real power lies in the power to upgrade programs.
It does this through executing commands to the bpf-upgradable-loader program.
Bpf-upgradable-loader allows any signer who has Upgrade authority over a Buffer account and the Program account itself
to upgrade it using its Upgrade command.
Normally, this is the developer who created and deployed the program, and this creation of the Buffer account containing
the new program data and overwriting of the existing Program account's data with it is handled in the background for you
by the Solana program deploy cli command.
However, in order for Governance to be useful, Governance now needs this authority.
### Proposal accounts
A Proposal is an instance of a Governance created to vote on and execute given set of changes.
It is created by someone (Proposal Admin) and tied to a given Governance account
and has a set of executable commands to it, a name and a description.
It goes through various states (draft, voting, executing) and users can vote on it
if they have relevant Community or Council tokens.
It's rules are determined by the Governance account that it is tied to, and when it executes,
it is only eligible to use the [Program Derived Address](https://docs.solana.com/developing/programming-model/calling-between-programs#program-derived-addresses)
authority given by the Governance account.
So a Proposal for Sushi cannot for instance upgrade the Program for Uniswap.
When a Proposal is created by a user then the user becomes Proposal Admin and receives an Admin an Signatory token.
With this power the Admin can add other Signatories to the Proposal.
These Signatories can then add commands to the Proposal and/or sign off on the Proposal.
Once all Signatories have signed off on the Proposal the Proposal leaves Draft state and enters Voting state.
Voting state lasts as long as the Governance has it configured to last, and during this time
people holding Community (or Council) tokens may vote on the Proposal.
Once the Proposal is "tipped" it either enters the Defeated or Executing state.
If Executed, it enters Completed state once all commands have been run.
A command can be run by any one at any time after the `instruction_hold_up_time` length has transpired on the given command.
### SingleSignerInstruction
We only support one kind of executable command right now, and this is the `SingleSignerInstruction` type.
A Proposal can have a certain number of these, and they run independently of each other.
These contain the actual data for a command, and how long after the voting phase a user must wait before they can be executed.
### Voting Dynamics
When a Proposal is created and signed by its Signatories voters can start voting on it using their voting weight,
equal to deposited governing tokens into the realm. A vote is tipped once it passes the defined `vote_threshold` of votes
and enters Succeeded or Defeated state. If Succeeded then Proposal instructions can be executed after they hold_up_time passes.
Users can relinquish their vote any time during Proposal lifetime, but once Proposal it tipped their vote can't be changed.
### Community and Councils governing tokens
Each Governance Realm that gets created has the option to also have a Council mint.
A council mint is simply a separate mint from the Community mint.
What this means is that users can submit Proposals that have a different voting population from a different mint
that can affect the same program. A practical application of this policy may be to have a very large population control
major version bumps of Solana via normal SOL, for instance, but hot fixes be controlled via Council tokens,
of which there may be only 30, and which may be themselves minted and distributed via proposals by the governing population.
### Proposal Workflow
![Proposal Workflow](./resources/governance-workflow.jpg)

View File

@ -1,33 +0,0 @@
[package]
name = "spl-governance"
version = "0.1.0"
description = "Solana Program Library Governance"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
arrayref = "0.3.6"
bincode = "1.3.2"
borsh = "0.8.1"
num-derive = "0.3"
num-traits = "0.2"
serde = "1.0.121"
serde_derive = "1.0.103"
solana-program = "1.6.7"
spl-token = { path = "../../token/program", features = [ "no-entrypoint" ] }
thiserror = "1.0"
[dev-dependencies]
assert_matches = "1.5.0"
proptest = "0.10"
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
[lib]
crate-type = ["cdylib", "lib"]

View File

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

View File

@ -1,22 +0,0 @@
//! Program entrypoint definitions
#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))]
use crate::{error::GovernanceError, processor};
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult,
program_error::PrintProgramError, pubkey::Pubkey,
};
entrypoint!(process_instruction);
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
if let Err(error) = processor::process_instruction(program_id, accounts, instruction_data) {
// catch the error so we can print it
error.print::<GovernanceError>();
return Err(error);
}
Ok(())
}

View File

@ -1,149 +0,0 @@
//! Error types
use num_derive::FromPrimitive;
use solana_program::{
decode_error::DecodeError,
msg,
program_error::{PrintProgramError, ProgramError},
};
use thiserror::Error;
/// Errors that may be returned by the Governance program
#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)]
pub enum GovernanceError {
/// Invalid instruction passed to program
#[error("Invalid instruction passed to program")]
InvalidInstruction,
/// Realm with the given name and governing mints already exists
#[error("Realm with the given name and governing mints already exists")]
RealmAlreadyExists,
/// Invalid Realm
#[error("Invalid realm")]
InvalidRealm,
/// Invalid Governing Token Mint
#[error("Invalid Governing Token Mint")]
InvalidGoverningTokenMint,
/// Governing Token Owner must sign transaction
#[error("Governing Token Owner must sign transaction")]
GoverningTokenOwnerMustSign,
/// Governing Token Owner or Delegate must sign transaction
#[error("Governing Token Owner or Delegate must sign transaction")]
GoverningTokenOwnerOrDelegateMustSign,
/// All active votes must be relinquished to withdraw governing tokens
#[error("All active votes must be relinquished to withdraw governing tokens")]
CannotWithdrawGoverningTokensWhenActiveVotesExist,
/// Invalid Token Owner Record account address
#[error("Invalid Token Owner Record account address")]
InvalidTokenOwnerRecordAccountAddress,
/// Invalid Token Owner Record Governing mint
#[error("Invalid Token Owner Record Governing mint")]
InvalidTokenOwnerRecordGoverningMint,
/// Invalid Token Owner Record Realm
#[error("Invalid Token Owner Record Realm")]
InvalidTokenOwnerRecordRealm,
/// Invalid Signatory account address
#[error("Invalid Signatory account address")]
InvalidSignatoryAddress,
/// Signatory already signed off
#[error("Signatory already signed off")]
SignatoryAlreadySignedOff,
/// Signatory must sign
#[error("Signatory must sign")]
SignatoryMustSign,
/// Invalid Proposal Owner
#[error("Invalid Proposal Owner")]
InvalidProposalOwnerAccount,
/// Invalid Governance config
#[error("Invalid Governance config")]
InvalidGovernanceConfig,
/// Proposal for the given Governance, Governing Token Mint and index already exists
#[error("Proposal for the given Governance, Governing Token Mint and index already exists")]
ProposalAlreadyExists,
/// Owner doesn't have enough governing tokens to create Proposal
#[error("Owner doesn't have enough governing tokens to create Proposal")]
NotEnoughTokensToCreateProposal,
/// Invalid State: Can't edit Signatories
#[error("Invalid State: Can't edit Signatories")]
InvalidStateCannotEditSignatories,
/// Invalid State: Can't sign off
#[error("Invalid State: Can't sign off")]
InvalidStateCannotSignOff,
/// Invalid Signatory Mint
#[error("Invalid Signatory Mint")]
InvalidSignatoryMint,
/// ---- Account Tools Errors ----
/// Invalid account owner
#[error("Invalid account owner")]
InvalidAccountOwner,
/// Invalid Account type
#[error("Invalid Account type")]
InvalidAccountType,
/// ---- Token Tools Errors ----
/// Invalid Token account owner
#[error("Invalid Token account owner")]
InvalidTokenAccountOwner,
/// ---- Bpf Upgradable Loader Tools Errors ----
/// Invalid ProgramData account Address
#[error("Invalid ProgramData account address")]
InvalidProgramDataAccountAddress,
/// Invalid ProgramData account data
#[error("Invalid ProgramData account Data")]
InvalidProgramDataAccountData,
/// Provided upgrade authority doesn't match current program upgrade authority
#[error("Provided upgrade authority doesn't match current program upgrade authority")]
InvalidUpgradeAuthority,
/// Current program upgrade authority must sign transaction
#[error("Current program upgrade authority must sign transaction")]
UpgradeAuthorityMustSign,
/// Given program is not upgradable
#[error("Given program is not upgradable")]
ProgramNotUpgradable,
}
impl PrintProgramError for GovernanceError {
fn print<E>(&self) {
msg!("GOVERNANCE-ERROR: {}", &self.to_string());
}
}
impl From<GovernanceError> for ProgramError {
fn from(e: GovernanceError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for GovernanceError {
fn type_of() -> &'static str {
"Governance Error"
}
}

View File

@ -1,617 +0,0 @@
//! Program instructions
use crate::{
id,
state::{
governance::{
get_account_governance_address, get_program_governance_address, GovernanceConfig,
},
proposal::get_proposal_address,
realm::{get_governing_token_holding_address, get_realm_address},
signatory_record::get_signatory_record_address,
single_signer_instruction::InstructionData,
token_owner_record::get_token_owner_record_address,
},
tools::bpf_loader_upgradeable::get_program_data_address,
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
bpf_loader_upgradeable,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
system_program, sysvar,
};
/// Yes/No Vote
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum Vote {
/// Yes vote
Yes,
/// No vote
No,
}
/// Instructions supported by the Governance program
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
#[repr(C)]
#[allow(clippy::large_enum_variant)]
pub enum GovernanceInstruction {
/// Creates Governance Realm account which aggregates governances for given Community Mint and optional Council Mint
///
/// 0. `[writable]` Governance Realm account. PDA seeds:['governance',name]
/// 1. `[]` Community Token Mint
/// 2. `[writable]` Community Token Holding account. PDA seeds: ['governance',realm,community_mint]
/// The account will be created with the Realm PDA as its owner
/// 3. `[signer]` Payer
/// 4. `[]` System
/// 5. `[]` SPL Token
/// 6. `[]` Sysvar Rent
/// 7. `[]` Council Token Mint - optional
/// 8. `[writable]` Council Token Holding account - optional. . PDA seeds: ['governance',realm,council_mint]
/// The account will be created with the Realm PDA as its owner
CreateRealm {
#[allow(dead_code)]
/// UTF-8 encoded Governance Realm name
name: String,
},
/// Deposits governing tokens (Community or Council) to Governance Realm and establishes your voter weight to be used for voting within the Realm
/// Note: If subsequent (top up) deposit is made and there are active votes for the Voter then the vote weights won't be updated automatically
/// It can be done by relinquishing votes on active Proposals and voting again with the new weight
///
/// 0. `[]` Governance Realm account
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
/// 2. `[writable]` Governing Token Source account. All tokens from the account will be transferred to the Holding account
/// 3. `[signer]` Governing Token Owner account
/// 4. `[signer]` Governing Token Transfer authority
/// 5. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 6. `[signer]` Payer
/// 7. `[]` System
/// 8. `[]` SPL Token
/// 9. `[]` Sysvar Rent
DepositGoverningTokens {},
/// Withdraws governing tokens (Community or Council) from Governance Realm and downgrades your voter weight within the Realm
/// Note: It's only possible to withdraw tokens if the Voter doesn't have any outstanding active votes
/// If there are any outstanding votes then they must be relinquished before tokens could be withdrawn
///
/// 0. `[]` Governance Realm account
/// 1. `[writable]` Governing Token Holding account. PDA seeds: ['governance',realm, governing_token_mint]
/// 2. `[writable]` Governing Token Destination account. All tokens will be transferred to this account
/// 3. `[signer]` Governing Token Owner account
/// 4. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 5. `[]` SPL Token
WithdrawGoverningTokens {},
/// Sets Governance Delegate for the given Realm and Governing Token Mint (Community or Council)
/// The Delegate would have voting rights and could vote on behalf of the Governing Token Owner
/// The Delegate would also be able to create Proposals on behalf of the Governing Token Owner
/// Note: This doesn't take voting rights from the Token Owner who still can vote and change governance_delegate
///
/// 0. `[signer]` Current Governance Delegate or Governing Token owner
/// 1. `[writable]` Token Owner Record
SetGovernanceDelegate {
#[allow(dead_code)]
/// New Governance Delegate
new_governance_delegate: Option<Pubkey>,
},
/// Creates Account Governance account which can be used to govern an arbitrary account
///
/// 0. `[]` Realm account the created Governance belongs to
/// 1. `[writable]` Account Governance account. PDA seeds: ['account-governance', realm, governed_account]
/// 2. `[signer]` Payer
/// 3. `[]` System program
/// 4. `[]` Sysvar Rent
CreateAccountGovernance {
/// Governance config
#[allow(dead_code)]
config: GovernanceConfig,
},
/// Creates Program Governance account which governs an upgradable program
///
/// 0. `[]` Realm account the created Governance belongs to
/// 1. `[writable]` Program Governance account. PDA seeds: ['program-governance', realm, governed_program]
/// 2. `[writable]` Program Data account of the Program governed by this Governance account
/// 3. `[signer]` Current Upgrade Authority account of the Program governed by this Governance account
/// 4. `[signer]` Payer
/// 5. `[]` bpf_upgradeable_loader program
/// 6. `[]` System program
/// 7. `[]` Sysvar Rent
CreateProgramGovernance {
/// Governance config
#[allow(dead_code)]
config: GovernanceConfig,
#[allow(dead_code)]
/// Indicate whether Program's upgrade_authority should be transferred to the Governance PDA
/// If it's set to false then it can be done at a later time
/// However the instruction would validate the current upgrade_authority signed the transaction nonetheless
transfer_upgrade_authority: bool,
},
/// Creates Proposal account for Instructions that will be executed at various slots in the future
///
/// 0. `[writable]` Proposal account. PDA seeds ['governance',governance, governing_token_mint, proposal_index]
/// 1. `[writable]` Governance account
/// 2. `[]` Token Owner Record account
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 4. `[signer]` Payer
/// 5. `[]` System program
/// 6. `[]` Rent sysvar
/// 7. `[]` Clock sysvar
CreateProposal {
#[allow(dead_code)]
/// UTF-8 encoded name of the proposal
name: String,
#[allow(dead_code)]
/// Link to gist explaining proposal
description_link: String,
#[allow(dead_code)]
/// Governing Token Mint the Proposal is created for
governing_token_mint: Pubkey,
},
/// Adds a signatory to the Proposal which means this Proposal can't leave Draft state until yet another Signatory signs
///
/// 0. `[writable]` Proposal account
/// 1. `[]` Token Owner Record account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 3. `[writable]` Signatory Record Account
/// 4. `[signer]` Payer
/// 5. `[]` System program
/// 6. `[]` Rent sysvar
AddSignatory {
#[allow(dead_code)]
/// Signatory to add to the Proposal
signatory: Pubkey,
},
/// Removes a Signatory from the Proposal
///
/// 0. `[writable]` Proposal account
/// 1. `[]` Token Owner Record account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
/// 3. `[writable]` Signatory Record Account
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed Signatory Record Account
/// 5. `[]` Clock sysvar
RemoveSignatory {
#[allow(dead_code)]
/// Signatory to remove from the Proposal
signatory: Pubkey,
},
/// Adds an instruction to the Proposal. Max of 5 of any type. More than 5 will throw error
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Uninitialized Proposal SingleSignerInstruction account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
AddSingleSignerInstruction {
#[allow(dead_code)]
/// Slot waiting time between vote period ending and this being eligible for execution
hold_up_time: u64,
#[allow(dead_code)]
/// Instruction
instruction: InstructionData,
#[allow(dead_code)]
/// Position in instruction array
position: u8,
},
/// Remove instruction from the Proposal
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Proposal SingleSignerInstruction account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
RemoveInstruction,
/// Update instruction hold up time in the Proposal
///
/// 0. `[]` Proposal account
/// 1. `[writable]` Proposal SingleSignerInstruction account
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
UpdateInstructionHoldUpTime {
#[allow(dead_code)]
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
hold_up_time: u64,
},
/// Cancels Proposal and moves it into Canceled
///
/// 0. `[writable]` Proposal account
/// 1. `[signer]` Governance Authority (Token Owner or Governance Delegate)
CancelProposal,
/// Signs off Proposal indicating the Signatory approves the Proposal
/// When the last Signatory signs the Proposal state moves to Voting state
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Signatory Record account
/// 2. `[signer]` Signatory account
/// 3. `[]` Clock sysvar
SignOffProposal,
/// Uses your voter weight (deposited Community or Council tokens) to cast a vote on a Proposal
/// By doing so you indicate you approve or disapprove of running the Proposal set of instructions
/// If you tip the consensus then the instructions can begin to be run after their hold up time
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
/// 3. `[signer]` Governance Authority account
/// 4. `[]` Governance account
Vote {
#[allow(dead_code)]
/// Yes/No vote
vote: Vote,
},
/// Relinquish Vote removes voter weight from a Proposal and removes it from voter's active votes
/// If the Proposal is still being voted on then the voter's weight won't count towards the vote outcome
/// If the Proposal is already in decided state then the instruction has no impact on the Proposal
/// and only allows voters to prune their outstanding votes in case they wanted to withdraw Governing tokens from the Realm
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
/// 2. `[writable]` Proposal Vote Record account. PDA seeds: ['governance',proposal,governing_token_owner]
/// 3. `[signer]` Governance Authority account
RelinquishVote,
/// Executes an instruction in the Proposal
/// Anybody can execute transaction once Proposal has been voted Yes and transaction_hold_up time has passed
/// The actual instruction being executed will be signed by Governance PDA
/// For example to execute Program upgrade the ProgramGovernance PDA would be used as the singer
///
/// 0. `[writable]` Proposal account
/// 1. `[writable]` Instruction account you wish to execute
/// 2. `[]` Program being invoked account
/// 3. `[]` Governance account (PDA)
/// 4. `[]` Clock sysvar
/// 5+ Any extra accounts that are part of the instruction, in order
Execute,
}
/// Creates CreateRealm instruction
pub fn create_realm(
// Accounts
community_token_mint: &Pubkey,
payer: &Pubkey,
council_token_mint: Option<Pubkey>,
// Args
name: String,
) -> Instruction {
let realm_address = get_realm_address(&name);
let community_token_holding_address =
get_governing_token_holding_address(&realm_address, &community_token_mint);
let mut accounts = vec![
AccountMeta::new(realm_address, false),
AccountMeta::new_readonly(*community_token_mint, false),
AccountMeta::new(community_token_holding_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
if let Some(council_token_mint) = council_token_mint {
let council_token_holding_address =
get_governing_token_holding_address(&realm_address, &council_token_mint);
accounts.push(AccountMeta::new_readonly(council_token_mint, false));
accounts.push(AccountMeta::new(council_token_holding_address, false));
}
let instruction = GovernanceInstruction::CreateRealm { name };
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates DepositGoverningTokens instruction
pub fn deposit_governing_tokens(
// Accounts
realm: &Pubkey,
governing_token_source: &Pubkey,
governing_token_owner: &Pubkey,
governing_token_transfer_authority: &Pubkey,
payer: &Pubkey,
// Args
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
let accounts = vec![
AccountMeta::new_readonly(*realm, false),
AccountMeta::new(governing_token_holding_address, false),
AccountMeta::new(*governing_token_source, false),
AccountMeta::new_readonly(*governing_token_owner, true),
AccountMeta::new_readonly(*governing_token_transfer_authority, true),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::DepositGoverningTokens {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates WithdrawGoverningTokens instruction
pub fn withdraw_governing_tokens(
// Accounts
realm: &Pubkey,
governing_token_destination: &Pubkey,
governing_token_owner: &Pubkey,
// Args
governing_token_mint: &Pubkey,
) -> Instruction {
let vote_record_address =
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let governing_token_holding_address =
get_governing_token_holding_address(realm, governing_token_mint);
let accounts = vec![
AccountMeta::new_readonly(*realm, false),
AccountMeta::new(governing_token_holding_address, false),
AccountMeta::new(*governing_token_destination, false),
AccountMeta::new_readonly(*governing_token_owner, true),
AccountMeta::new(vote_record_address, false),
AccountMeta::new_readonly(spl_token::id(), false),
];
let instruction = GovernanceInstruction::WithdrawGoverningTokens {};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates SetGovernanceDelegate instruction
pub fn set_governance_delegate(
// Accounts
governance_authority: &Pubkey,
// Args
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
new_governance_delegate: &Option<Pubkey>,
) -> Instruction {
let vote_record_address =
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let accounts = vec![
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(vote_record_address, false),
];
let instruction = GovernanceInstruction::SetGovernanceDelegate {
new_governance_delegate: *new_governance_delegate,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CreateAccountGovernance instruction
pub fn create_account_governance(
// Accounts
payer: &Pubkey,
// Args
config: GovernanceConfig,
) -> Instruction {
let account_governance_address =
get_account_governance_address(&config.realm, &config.governed_account);
let accounts = vec![
AccountMeta::new_readonly(config.realm, false),
AccountMeta::new(account_governance_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::CreateAccountGovernance { config };
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CreateProgramGovernance instruction
pub fn create_program_governance(
// Accounts
governed_program_upgrade_authority: &Pubkey,
payer: &Pubkey,
// Args
config: GovernanceConfig,
transfer_upgrade_authority: bool,
) -> Instruction {
let program_governance_address =
get_program_governance_address(&config.realm, &config.governed_account);
let governed_program_data_address = get_program_data_address(&config.governed_account);
let accounts = vec![
AccountMeta::new_readonly(config.realm, false),
AccountMeta::new(program_governance_address, false),
AccountMeta::new(governed_program_data_address, false),
AccountMeta::new_readonly(*governed_program_upgrade_authority, true),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(bpf_loader_upgradeable::id(), false),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::CreateProgramGovernance {
config,
transfer_upgrade_authority,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates CreateProposal instruction
#[allow(clippy::too_many_arguments)]
pub fn create_proposal(
// Accounts
governance: &Pubkey,
governing_token_owner: &Pubkey,
governance_authority: &Pubkey,
payer: &Pubkey,
// Args
realm: &Pubkey,
name: String,
description_link: String,
governing_token_mint: &Pubkey,
proposal_index: u16,
) -> Instruction {
let proposal_address = get_proposal_address(
governance,
governing_token_mint,
&proposal_index.to_le_bytes(),
);
let token_owner_record_address =
get_token_owner_record_address(realm, governing_token_mint, governing_token_owner);
let accounts = vec![
AccountMeta::new(proposal_address, false),
AccountMeta::new(*governance, false),
AccountMeta::new_readonly(token_owner_record_address, false),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::CreateProposal {
name,
description_link,
governing_token_mint: *governing_token_mint,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates AddSignatory instruction
pub fn add_signatory(
// Accounts
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
payer: &Pubkey,
// Args
signatory: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*token_owner_record, false),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new_readonly(*payer, true),
AccountMeta::new_readonly(system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
];
let instruction = GovernanceInstruction::AddSignatory {
signatory: *signatory,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates RemoveSignatory instruction
pub fn remove_signatory(
// Accounts
proposal: &Pubkey,
token_owner_record: &Pubkey,
governance_authority: &Pubkey,
signatory: &Pubkey,
beneficiary: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new_readonly(*token_owner_record, false),
AccountMeta::new_readonly(*governance_authority, true),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new(*beneficiary, false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::RemoveSignatory {
signatory: *signatory,
};
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}
/// Creates SignOffProposal instruction
pub fn sign_off_proposal(
// Accounts
proposal: &Pubkey,
signatory: &Pubkey,
) -> Instruction {
let signatory_record_address = get_signatory_record_address(proposal, signatory);
let accounts = vec![
AccountMeta::new(*proposal, false),
AccountMeta::new(signatory_record_address, false),
AccountMeta::new_readonly(*signatory, true),
AccountMeta::new_readonly(sysvar::clock::id(), false),
];
let instruction = GovernanceInstruction::SignOffProposal;
Instruction {
program_id: id(),
accounts,
data: instruction.try_to_vec().unwrap(),
}
}

View File

@ -1,17 +0,0 @@
#![deny(missing_docs)]
//! A Governance program for the Solana blockchain.
pub mod entrypoint;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod state;
pub mod tools;
// Export current sdk types for downstream users building with a different sdk version
pub use solana_program;
solana_program::declare_id!("GovernancerdmUu324nahyv33G5poQdLUEZ1nEytDeP");
/// Seed prefix for Governance PDAs
pub const PROGRAM_AUTHORITY_SEED: &[u8] = b"governance";

View File

@ -1,94 +0,0 @@
//! Program processor
mod process_add_signatory;
mod process_create_account_governance;
mod process_create_program_governance;
mod process_create_proposal;
mod process_create_realm;
mod process_deposit_governing_tokens;
mod process_remove_signatory;
mod process_set_governance_delegate;
mod process_sign_off_proposal;
mod process_withdraw_governing_tokens;
use crate::instruction::GovernanceInstruction;
use borsh::BorshDeserialize;
use process_add_signatory::*;
use process_create_account_governance::*;
use process_create_program_governance::*;
use process_create_proposal::*;
use process_create_realm::*;
use process_deposit_governing_tokens::*;
use process_remove_signatory::*;
use process_set_governance_delegate::*;
use process_sign_off_proposal::*;
use process_withdraw_governing_tokens::*;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, msg, program_error::ProgramError,
pubkey::Pubkey,
};
/// Processes an instruction
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
let instruction = GovernanceInstruction::try_from_slice(input)
.map_err(|_| ProgramError::InvalidInstructionData)?;
msg!("GOVERNANCE-INSTRUCTION: {:?}", instruction);
match instruction {
GovernanceInstruction::CreateRealm { name } => {
process_create_realm(program_id, accounts, name)
}
GovernanceInstruction::DepositGoverningTokens {} => {
process_deposit_governing_tokens(program_id, accounts)
}
GovernanceInstruction::WithdrawGoverningTokens {} => {
process_withdraw_governing_tokens(program_id, accounts)
}
GovernanceInstruction::SetGovernanceDelegate {
new_governance_delegate,
} => process_set_governance_delegate(accounts, &new_governance_delegate),
GovernanceInstruction::CreateProgramGovernance {
config,
transfer_upgrade_authority,
} => process_create_program_governance(
program_id,
accounts,
config,
transfer_upgrade_authority,
),
GovernanceInstruction::CreateAccountGovernance { config } => {
process_create_account_governance(program_id, accounts, config)
}
GovernanceInstruction::CreateProposal {
name,
description_link,
governing_token_mint,
} => process_create_proposal(
program_id,
accounts,
name,
description_link,
governing_token_mint,
),
GovernanceInstruction::AddSignatory { signatory } => {
process_add_signatory(program_id, accounts, signatory)
}
GovernanceInstruction::RemoveSignatory { signatory } => {
process_remove_signatory(program_id, accounts, signatory)
}
GovernanceInstruction::SignOffProposal {} => {
process_sign_off_proposal(program_id, accounts)
}
_ => todo!("Instruction not implemented yet"),
}
}

View File

@ -1,76 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
state::{
enums::GovernanceAccountType,
proposal::deserialize_proposal_raw,
signatory_record::{get_signatory_record_address_seeds, SignatoryRecord},
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
},
tools::{
account::create_and_serialize_account_signed,
asserts::assert_token_owner_or_delegate_is_signer,
},
};
/// Processes AddSignatory instruction
pub fn process_add_signatory(
program_id: &Pubkey,
accounts: &[AccountInfo],
signatory: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
let governance_authority_info = next_account_info(account_info_iter)?; // 2
let signatory_record_info = next_account_info(account_info_iter)?; // 3
let payer_info = next_account_info(account_info_iter)?; // 4
let system_info = next_account_info(account_info_iter)?; // 5
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
token_owner_record_info,
&proposal_data.token_owner_record,
)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
let signatory_record_data = SignatoryRecord {
account_type: GovernanceAccountType::SignatoryRecord,
proposal: *proposal_info.key,
signatory,
signed_off: false,
};
create_and_serialize_account_signed::<SignatoryRecord>(
payer_info,
signatory_record_info,
&signatory_record_data,
&get_signatory_record_address_seeds(proposal_info.key, &signatory),
program_id,
system_info,
rent,
)?;
proposal_data.signatories_count = proposal_data.signatories_count.checked_add(1).unwrap();
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,56 +0,0 @@
//! Program state processor
use crate::{
state::{
enums::GovernanceAccountType,
governance::{
assert_is_valid_governance_config, get_account_governance_address_seeds, Governance,
GovernanceConfig,
},
},
tools::account::create_and_serialize_account_signed,
};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
/// Processes CreateAccountGovernance instruction
pub fn process_create_account_governance(
program_id: &Pubkey,
accounts: &[AccountInfo],
config: GovernanceConfig,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let account_governance_info = next_account_info(account_info_iter)?; // 0
let payer_info = next_account_info(account_info_iter)?; // 1
let system_info = next_account_info(account_info_iter)?; // 2
let rent_sysvar_info = next_account_info(account_info_iter)?; // 3
let rent = &Rent::from_account_info(rent_sysvar_info)?;
assert_is_valid_governance_config(&config, realm_info)?;
let account_governance_data = Governance {
account_type: GovernanceAccountType::AccountGovernance,
config: config.clone(),
proposals_count: 0,
};
create_and_serialize_account_signed::<Governance>(
payer_info,
&account_governance_info,
&account_governance_data,
&get_account_governance_address_seeds(&config.realm, &config.governed_account),
program_id,
system_info,
rent,
)?;
Ok(())
}

View File

@ -1,85 +0,0 @@
//! Program state processor
use crate::{
state::governance::Governance,
state::{
enums::GovernanceAccountType,
governance::{
assert_is_valid_governance_config, get_program_governance_address_seeds,
GovernanceConfig,
},
},
tools::{
account::create_and_serialize_account_signed,
bpf_loader_upgradeable::{
assert_program_upgrade_authority_is_signer, set_program_upgrade_authority,
},
},
};
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
/// Processes CreateProgramGovernance instruction
pub fn process_create_program_governance(
program_id: &Pubkey,
accounts: &[AccountInfo],
config: GovernanceConfig,
transfer_upgrade_authority: bool,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let program_governance_info = next_account_info(account_info_iter)?; // 0
let governed_program_data_info = next_account_info(account_info_iter)?; // 1
let governed_program_upgrade_authority_info = next_account_info(account_info_iter)?; // 2
let payer_info = next_account_info(account_info_iter)?; // 3
let bpf_upgrade_loader_info = next_account_info(account_info_iter)?; // 4
let system_info = next_account_info(account_info_iter)?; // 5
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
assert_is_valid_governance_config(&config, &realm_info)?;
let program_governance_data = Governance {
account_type: GovernanceAccountType::ProgramGovernance,
config: config.clone(),
proposals_count: 0,
};
create_and_serialize_account_signed::<Governance>(
payer_info,
&program_governance_info,
&program_governance_data,
&get_program_governance_address_seeds(&config.realm, &config.governed_account),
program_id,
system_info,
rent,
)?;
if transfer_upgrade_authority {
set_program_upgrade_authority(
&config.governed_account,
governed_program_data_info,
governed_program_upgrade_authority_info,
program_governance_info,
bpf_upgrade_loader_info,
)?;
} else {
assert_program_upgrade_authority_is_signer(
&config.governed_account,
&governed_program_data_info,
&governed_program_upgrade_authority_info,
)?;
}
Ok(())
}

View File

@ -1,115 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
state::{
enums::{GovernanceAccountType, ProposalState},
governance::deserialize_governance_raw,
proposal::{get_proposal_address_seeds, Proposal},
token_owner_record::deserialize_token_owner_record_for_realm_and_governing_mint,
},
tools::{
account::create_and_serialize_account_signed,
asserts::assert_token_owner_or_delegate_is_signer,
},
};
/// Processes CreateProposal instruction
pub fn process_create_proposal(
program_id: &Pubkey,
accounts: &[AccountInfo],
name: String,
description_link: String,
governing_token_mint: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let governance_info = next_account_info(account_info_iter)?; // 1
let token_owner_record_info = next_account_info(account_info_iter)?; // 2
let governance_authority_info = next_account_info(account_info_iter)?; // 3
let payer_info = next_account_info(account_info_iter)?; // 4
let system_info = next_account_info(account_info_iter)?; // 5
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let clock_info = next_account_info(account_info_iter)?; // 7
let clock = Clock::from_account_info(clock_info)?;
if !proposal_info.data_is_empty() {
return Err(GovernanceError::ProposalAlreadyExists.into());
}
let mut governance_data = deserialize_governance_raw(governance_info)?;
let token_owner_record_data = deserialize_token_owner_record_for_realm_and_governing_mint(
&token_owner_record_info,
&governance_data.config.realm,
&governing_token_mint,
)?;
// proposal_owner must be either governing token owner or governance_delegate and must sign this transaction
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
if token_owner_record_data.governing_token_deposit_amount
< governance_data.config.min_tokens_to_create_proposal as u64
{
return Err(GovernanceError::NotEnoughTokensToCreateProposal.into());
}
let proposal_data = Proposal {
account_type: GovernanceAccountType::Proposal,
governance: *governance_info.key,
governing_token_mint,
state: ProposalState::Draft,
token_owner_record: *token_owner_record_info.key,
signatories_count: 0,
signatories_signed_off_count: 0,
name,
description_link,
draft_at: clock.slot,
signing_off_at: None,
voting_at: None,
voting_completed_at: None,
executing_at: None,
closed_at: None,
number_of_executed_instructions: 0,
number_of_instructions: 0,
};
create_and_serialize_account_signed::<Proposal>(
payer_info,
proposal_info,
&proposal_data,
&get_proposal_address_seeds(
governance_info.key,
&governing_token_mint,
&governance_data.proposals_count.to_le_bytes(),
),
program_id,
system_info,
rent,
)?;
governance_data.proposals_count = governance_data.proposals_count.checked_add(1).unwrap();
governance_data.serialize(&mut *governance_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,97 +0,0 @@
//! Program state processor
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
state::{
enums::GovernanceAccountType,
realm::{get_governing_token_holding_address_seeds, get_realm_address_seeds, Realm},
},
tools::{account::create_and_serialize_account_signed, token::create_spl_token_account_signed},
};
/// Processes CreateRealm instruction
pub fn process_create_realm(
program_id: &Pubkey,
accounts: &[AccountInfo],
name: String,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let governance_token_mint_info = next_account_info(account_info_iter)?; // 1
let governance_token_holding_info = next_account_info(account_info_iter)?; // 2
let payer_info = next_account_info(account_info_iter)?; // 3
let system_info = next_account_info(account_info_iter)?; // 4
let spl_token_info = next_account_info(account_info_iter)?; // 5
let rent_sysvar_info = next_account_info(account_info_iter)?; // 6
let rent = &Rent::from_account_info(rent_sysvar_info)?;
if !realm_info.data_is_empty() {
return Err(GovernanceError::RealmAlreadyExists.into());
}
create_spl_token_account_signed(
payer_info,
governance_token_holding_info,
&get_governing_token_holding_address_seeds(realm_info.key, governance_token_mint_info.key),
governance_token_mint_info,
realm_info,
program_id,
system_info,
spl_token_info,
rent_sysvar_info,
rent,
)?;
let council_token_mint_address = if let Ok(council_token_mint_info) =
next_account_info(account_info_iter)
// 7
{
let council_token_holding_info = next_account_info(account_info_iter)?; //8
create_spl_token_account_signed(
payer_info,
council_token_holding_info,
&get_governing_token_holding_address_seeds(realm_info.key, council_token_mint_info.key),
council_token_mint_info,
realm_info,
program_id,
system_info,
spl_token_info,
rent_sysvar_info,
rent,
)?;
Some(*council_token_mint_info.key)
} else {
None
};
let realm_data = Realm {
account_type: GovernanceAccountType::Realm,
community_mint: *governance_token_mint_info.key,
council_mint: council_token_mint_address,
name: name.clone(),
};
create_and_serialize_account_signed::<Realm>(
payer_info,
&realm_info,
&realm_data,
&get_realm_address_seeds(&name),
program_id,
system_info,
rent,
)?;
Ok(())
}

View File

@ -1,116 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
rent::Rent,
sysvar::Sysvar,
};
use crate::{
error::GovernanceError,
state::{
enums::GovernanceAccountType,
realm::deserialize_realm_raw,
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds, TokenOwnerRecord,
},
},
tools::{
account::create_and_serialize_account_signed,
token::{
get_amount_from_token_account, get_mint_from_token_account,
get_owner_from_token_account, transfer_spl_tokens,
},
},
};
/// Processes DepositGoverningTokens instruction
pub fn process_deposit_governing_tokens(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
let governing_token_source_info = next_account_info(account_info_iter)?; // 2
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
let governing_token_transfer_authority_info = next_account_info(account_info_iter)?; // 4
let token_owner_record_info = next_account_info(account_info_iter)?; // 5
let payer_info = next_account_info(account_info_iter)?; // 6
let system_info = next_account_info(account_info_iter)?; // 7
let spl_token_info = next_account_info(account_info_iter)?; // 8
let rent_sysvar_info = next_account_info(account_info_iter)?; // 9
let rent = &Rent::from_account_info(rent_sysvar_info)?;
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?;
let amount = get_amount_from_token_account(governing_token_source_info)?;
transfer_spl_tokens(
&governing_token_source_info,
&governing_token_holding_info,
&governing_token_transfer_authority_info,
amount,
spl_token_info,
)?;
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
realm_info.key,
&governing_token_mint,
governing_token_owner_info.key,
);
if token_owner_record_info.data_is_empty() {
// Deposited tokens can only be withdrawn by the owner so let's make sure the owner signed the transaction
let governing_token_owner = get_owner_from_token_account(&governing_token_source_info)?;
if !(governing_token_owner == *governing_token_owner_info.key
&& governing_token_owner_info.is_signer)
{
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let token_owner_record_data = TokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecord,
realm: *realm_info.key,
governing_token_owner: *governing_token_owner_info.key,
governing_token_deposit_amount: amount,
governing_token_mint,
governance_delegate: None,
active_votes_count: 0,
total_votes_count: 0,
};
create_and_serialize_account_signed(
payer_info,
token_owner_record_info,
&token_owner_record_data,
&token_owner_record_address_seeds,
program_id,
system_info,
rent,
)?;
} else {
let mut token_owner_record_data = deserialize_token_owner_record(
token_owner_record_info,
&token_owner_record_address_seeds,
)?;
token_owner_record_data.governing_token_deposit_amount = token_owner_record_data
.governing_token_deposit_amount
.checked_add(amount)
.unwrap();
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
}
Ok(())
}

View File

@ -1,68 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
sysvar::Sysvar,
};
use crate::{
state::{
enums::ProposalState, proposal::deserialize_proposal_raw,
signatory_record::deserialize_signatory_record,
token_owner_record::deserialize_token_owner_record_for_proposal_owner,
},
tools::{account::dispose_account, asserts::assert_token_owner_or_delegate_is_signer},
};
/// Processes RemoveSignatory instruction
pub fn process_remove_signatory(
_program_id: &Pubkey,
accounts: &[AccountInfo],
signatory: Pubkey,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
let governance_authority_info = next_account_info(account_info_iter)?; // 2
let signatory_record_info = next_account_info(account_info_iter)?; // 3
let beneficiary_info = next_account_info(account_info_iter)?; // 4
let clock_info = next_account_info(account_info_iter)?; // 5
let clock = Clock::from_account_info(clock_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
proposal_data.assert_can_edit_signatories()?;
let token_owner_record_data = deserialize_token_owner_record_for_proposal_owner(
token_owner_record_info,
&proposal_data.token_owner_record,
)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, governance_authority_info)?;
let signatory_record_data =
deserialize_signatory_record(signatory_record_info, proposal_info.key, &signatory)?;
signatory_record_data.assert_can_remove_signatory()?;
proposal_data.signatories_count = proposal_data.signatories_count.checked_sub(1).unwrap();
// If all the remaining signatories signed already then we can start voting
if proposal_data.signatories_count > 0
&& proposal_data.signatories_signed_off_count == proposal_data.signatories_count
{
proposal_data.voting_at = Some(clock.slot);
proposal_data.state = ProposalState::Voting;
}
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
dispose_account(signatory_record_info, beneficiary_info);
Ok(())
}

View File

@ -1,33 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
state::token_owner_record::deserialize_token_owner_record_raw,
tools::asserts::assert_token_owner_or_delegate_is_signer,
};
/// Processes SetGovernanceDelegate instruction
pub fn process_set_governance_delegate(
accounts: &[AccountInfo],
new_governance_delegate: &Option<Pubkey>,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let governance_authority_info = next_account_info(account_info_iter)?; // 0
let token_owner_record_info = next_account_info(account_info_iter)?; // 1
let mut token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
assert_token_owner_or_delegate_is_signer(&token_owner_record_data, &governance_authority_info)?;
token_owner_record_data.governance_delegate = *new_governance_delegate;
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,58 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint::ProgramResult,
pubkey::Pubkey,
sysvar::Sysvar,
};
use crate::state::{
enums::ProposalState, proposal::deserialize_proposal_raw,
signatory_record::deserialize_signatory_record,
};
/// Processes SignOffProposal instruction
pub fn process_sign_off_proposal(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let proposal_info = next_account_info(account_info_iter)?; // 0
let signatory_record_info = next_account_info(account_info_iter)?; // 1
let signatory_info = next_account_info(account_info_iter)?; // 2
let clock_info = next_account_info(account_info_iter)?; // 3
let clock = Clock::from_account_info(clock_info)?;
let mut proposal_data = deserialize_proposal_raw(proposal_info)?;
proposal_data.assert_can_sign_off()?;
let mut signatory_record_data =
deserialize_signatory_record(signatory_record_info, proposal_info.key, signatory_info.key)?;
signatory_record_data.assert_can_sign_off(signatory_info)?;
signatory_record_data.signed_off = true;
signatory_record_data.serialize(&mut *signatory_record_info.data.borrow_mut())?;
if proposal_data.signatories_signed_off_count == 0 {
proposal_data.signing_off_at = Some(clock.slot);
proposal_data.state = ProposalState::SigningOff;
}
proposal_data.signatories_signed_off_count = proposal_data
.signatories_signed_off_count
.checked_add(1)
.unwrap();
// If all Signatories signed off we can start voting
if proposal_data.signatories_signed_off_count == proposal_data.signatories_count {
proposal_data.voting_at = Some(clock.slot);
proposal_data.state = ProposalState::Voting;
}
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,69 +0,0 @@
//! Program state processor
use borsh::BorshSerialize;
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
state::{
realm::{deserialize_realm_raw, get_realm_address_seeds},
token_owner_record::{
deserialize_token_owner_record, get_token_owner_record_address_seeds,
},
},
tools::token::{get_mint_from_token_account, transfer_spl_tokens_signed},
};
/// Processes WithdrawGoverningTokens instruction
pub fn process_withdraw_governing_tokens(
program_id: &Pubkey,
accounts: &[AccountInfo],
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let realm_info = next_account_info(account_info_iter)?; // 0
let governing_token_holding_info = next_account_info(account_info_iter)?; // 1
let governing_token_destination_info = next_account_info(account_info_iter)?; // 2
let governing_token_owner_info = next_account_info(account_info_iter)?; // 3
let token_owner_record_info = next_account_info(account_info_iter)?; // 4
let spl_token_info = next_account_info(account_info_iter)?; // 5
if !governing_token_owner_info.is_signer {
return Err(GovernanceError::GoverningTokenOwnerMustSign.into());
}
let realm_data = deserialize_realm_raw(realm_info)?;
let governing_token_mint = get_mint_from_token_account(governing_token_holding_info)?;
let token_owner_record_address_seeds = get_token_owner_record_address_seeds(
realm_info.key,
&governing_token_mint,
governing_token_owner_info.key,
);
let mut token_owner_record_data =
deserialize_token_owner_record(token_owner_record_info, &token_owner_record_address_seeds)?;
if token_owner_record_data.active_votes_count > 0 {
return Err(GovernanceError::CannotWithdrawGoverningTokensWhenActiveVotesExist.into());
}
transfer_spl_tokens_signed(
&governing_token_holding_info,
&governing_token_destination_info,
&realm_info,
&get_realm_address_seeds(&realm_data.name),
program_id,
token_owner_record_data.governing_token_deposit_amount,
spl_token_info,
)?;
token_owner_record_data.governing_token_deposit_amount = 0;
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
Ok(())
}

View File

@ -1,88 +0,0 @@
//! State enumerations
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Defines all Governance accounts types
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum GovernanceAccountType {
/// Default uninitialized account state
Uninitialized,
/// Top level aggregation for governances with Community Token (and optional Council Token)
Realm,
/// Token Owner Record for given governing token owner within a Realm
TokenOwnerRecord,
/// Generic Account Governance account
AccountGovernance,
/// Program Governance account
ProgramGovernance,
/// Proposal account for Governance account. A single Governance account can have multiple Proposal accounts
Proposal,
/// Proposal Signatory account
SignatoryRecord,
/// Vote record account for a given Proposal. Proposal can have 0..n voting records
ProposalVoteRecord,
/// Single Signer Instruction account which holds an instruction to execute for Proposal
SingleSignerInstruction,
}
impl Default for GovernanceAccountType {
fn default() -> Self {
GovernanceAccountType::Uninitialized
}
}
/// Vote with number of votes
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum VoteWeight {
/// Yes vote
Yes(u64),
/// No vote
No(u64),
}
/// What state a Proposal is in
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum ProposalState {
/// Draft - Proposal enters Draft state when it's created
Draft,
/// SigningOff - The Proposal is being signed off by Signatories
/// Proposal enters the state when first Signatory Sings and leaves it when last Signatory signs
SigningOff,
/// Taking votes
Voting,
/// Voting ended with success
Succeeded,
/// Voting completed and now instructions are being execute. Proposal enter this state when first instruction is executed and leaves when the last instruction is executed
Executing,
/// Completed
Completed,
/// Cancelled
Cancelled,
/// Defeated
Defeated,
}
impl Default for ProposalState {
fn default() -> Self {
ProposalState::Draft
}
}

View File

@ -1,137 +0,0 @@
//! Governance Account
use crate::{
error::GovernanceError, id, state::enums::GovernanceAccountType,
tools::account::deserialize_account, tools::account::AccountMaxSize,
};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use super::realm::assert_is_valid_realm;
/// Governance config
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct GovernanceConfig {
/// Governance Realm
pub realm: Pubkey,
/// Account governed by this Governance. It can be for example Program account, Mint account or Token Account
pub governed_account: Pubkey,
/// Voting threshold in % required to tip the vote
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
pub vote_threshold_percentage: u8,
/// Minimum number of tokens a governance token owner must possess to be able to create a proposal
pub min_tokens_to_create_proposal: u16,
/// Minimum waiting time in slots for an instruction to be executed after proposal is voted on
pub min_instruction_hold_up_time: u64,
/// Time limit in slots for proposal to be open for voting
pub max_voting_time: u64,
}
/// Governance Account
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Governance {
/// Account type. It can be Uninitialized, AccountGovernance or ProgramGovernance
pub account_type: GovernanceAccountType,
/// Governance config
pub config: GovernanceConfig,
/// Running count of proposals
pub proposals_count: u16,
}
impl AccountMaxSize for Governance {}
impl IsInitialized for Governance {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::AccountGovernance
|| self.account_type == GovernanceAccountType::ProgramGovernance
}
}
/// Deserializes account and checks owner program
pub fn deserialize_governance_raw(
governance_info: &AccountInfo,
) -> Result<Governance, ProgramError> {
deserialize_account::<Governance>(governance_info, &id())
}
/// Returns ProgramGovernance PDA seeds
pub fn get_program_governance_address_seeds<'a>(
realm: &'a Pubkey,
governed_program: &'a Pubkey,
) -> [&'a [u8]; 3] {
// 'program-governance' prefix ensures uniqueness of the PDA
// Note: Only the current program upgrade authority can create an account with this PDA using CreateProgramGovernance instruction
[
b"program-governance",
&realm.as_ref(),
&governed_program.as_ref(),
]
}
/// Returns ProgramGovernance PDA address
pub fn get_program_governance_address<'a>(
realm: &'a Pubkey,
governed_program: &'a Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_program_governance_address_seeds(realm, governed_program),
&id(),
)
.0
}
/// Returns AccountGovernance PDA seeds
pub fn get_account_governance_address_seeds<'a>(
realm: &'a Pubkey,
governed_account: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
b"account-governance",
&realm.as_ref(),
&governed_account.as_ref(),
]
}
/// Returns AccountGovernance PDA address
pub fn get_account_governance_address<'a>(
realm: &'a Pubkey,
governed_account: &'a Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_account_governance_address_seeds(realm, governed_account),
&id(),
)
.0
}
/// Validates governance config
pub fn assert_is_valid_governance_config(
governance_config: &GovernanceConfig,
realm_info: &AccountInfo,
) -> Result<(), ProgramError> {
if realm_info.key != &governance_config.realm {
return Err(GovernanceError::InvalidGovernanceConfig.into());
}
assert_is_valid_realm(realm_info)?;
if governance_config.vote_threshold_percentage < 50
|| governance_config.vote_threshold_percentage > 100
{
return Err(GovernanceError::InvalidGovernanceConfig.into());
}
Ok(())
}

View File

@ -1,10 +0,0 @@
//! Program accounts
pub mod enums;
pub mod governance;
pub mod proposal;
pub mod proposal_vote_record;
pub mod realm;
pub mod signatory_record;
pub mod single_signer_instruction;
pub mod token_owner_record;

View File

@ -1,254 +0,0 @@
//! Proposal Account
use solana_program::{
account_info::AccountInfo, epoch_schedule::Slot, program_error::ProgramError,
program_pack::IsInitialized, pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::{GovernanceAccountType, ProposalState};
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Governance Proposal
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Proposal {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Governance account the Proposal belongs to
pub governance: Pubkey,
/// Indicates which Governing Token is used to vote on the Proposal
/// Whether the general Community token owners or the Council tokens owners vote on this Proposal
pub governing_token_mint: Pubkey,
/// Current proposal state
pub state: ProposalState,
/// The TokenOwnerRecord representing the user who created and owns this Proposal
pub token_owner_record: Pubkey,
/// The number of signatories assigned to the Proposal
pub signatories_count: u8,
/// The number of signatories who already signed
pub signatories_signed_off_count: u8,
/// Link to proposal's description
pub description_link: String,
/// Proposal name
pub name: String,
/// When the Proposal was created and entered Draft state
pub draft_at: Slot,
/// When Signatories started signing off the Proposal
pub signing_off_at: Option<Slot>,
/// When the Proposal began voting
pub voting_at: Option<Slot>,
/// When the Proposal ended voting and entered either Succeeded or Defeated
pub voting_completed_at: Option<Slot>,
/// When the Proposal entered Executing state
pub executing_at: Option<Slot>,
/// When the Proposal entered final state Completed or Cancelled and was closed
pub closed_at: Option<Slot>,
/// The number of the instructions already executed
pub number_of_executed_instructions: u8,
/// The number of instructions included in the proposal
pub number_of_instructions: u8,
}
impl AccountMaxSize for Proposal {
fn get_max_size(&self) -> Option<usize> {
Some(self.name.len() + self.description_link.len() + 163)
}
}
impl IsInitialized for Proposal {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::Proposal
}
}
impl Proposal {
/// Checks if Signatories can be edited (added or removed) for the Proposal in the given state
pub fn assert_can_edit_signatories(&self) -> Result<(), ProgramError> {
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
return Err(GovernanceError::InvalidStateCannotEditSignatories.into());
}
Ok(())
}
/// Checks if Proposal can be singed off
pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
if !(self.state == ProposalState::Draft || self.state == ProposalState::SigningOff) {
return Err(GovernanceError::InvalidStateCannotSignOff.into());
}
Ok(())
}
}
/// Deserializes Proposal account and checks owner program
pub fn deserialize_proposal_raw(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
deserialize_account::<Proposal>(proposal_info, &id())
}
/// Returns Proposal PDA seeds
pub fn get_proposal_address_seeds<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_le_bytes: &'a [u8],
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
governance.as_ref(),
governing_token_mint.as_ref(),
&proposal_index_le_bytes,
]
}
/// Returns Proposal PDA address
pub fn get_proposal_address<'a>(
governance: &'a Pubkey,
governing_token_mint: &'a Pubkey,
proposal_index_bytes: &'a [u8],
) -> Pubkey {
Pubkey::find_program_address(
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_bytes),
&id(),
)
.0
}
#[cfg(test)]
mod test {
use {super::*, proptest::prelude::*};
fn create_test_proposal() -> Proposal {
Proposal {
account_type: GovernanceAccountType::TokenOwnerRecord,
governance: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
state: ProposalState::Draft,
token_owner_record: Pubkey::new_unique(),
signatories_count: 10,
signatories_signed_off_count: 5,
description_link: "This is my description".to_string(),
name: "This is my name".to_string(),
draft_at: 10,
signing_off_at: Some(10),
voting_at: Some(10),
voting_completed_at: Some(10),
executing_at: Some(10),
closed_at: Some(10),
number_of_executed_instructions: 10,
number_of_instructions: 10,
}
}
#[test]
fn test_max_size() {
let proposal = create_test_proposal();
let size = proposal.try_to_vec().unwrap().len();
assert_eq!(proposal.get_max_size(), Some(size));
}
fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::Draft), Just(ProposalState::SigningOff),]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories(state in editable_signatory_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_edit_signatories().unwrap();
}
}
fn none_editable_signatory_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_edit_signatories_with_invalid_state_error(state in none_editable_signatory_states()) {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = state;
// Act
let err = proposal.assert_can_edit_signatories().err().unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidStateCannotEditSignatories.into());
}
}
fn sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![Just(ProposalState::SigningOff), Just(ProposalState::Draft),]
}
proptest! {
#[test]
fn test_assert_can_sign_off(state in sign_off_states()) {
let mut proposal = create_test_proposal();
proposal.state = state;
proposal.assert_can_sign_off().unwrap();
}
}
fn none_sign_off_states() -> impl Strategy<Value = ProposalState> {
prop_oneof![
Just(ProposalState::Voting),
Just(ProposalState::Succeeded),
Just(ProposalState::Executing),
Just(ProposalState::Completed),
Just(ProposalState::Cancelled),
Just(ProposalState::Defeated),
]
}
proptest! {
#[test]
fn test_assert_can_sign_off_with_state_error(state in none_sign_off_states()) {
// Arrange
let mut proposal = create_test_proposal();
proposal.state = state;
// Act
let err = proposal.assert_can_sign_off().err().unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidStateCannotSignOff.into());
}
}
}

View File

@ -1,24 +0,0 @@
//! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::pubkey::Pubkey;
use crate::state::enums::{GovernanceAccountType, VoteWeight};
/// Proposal Vote Record
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ProposalVoteRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Proposal account
pub proposal: Pubkey,
/// The user who casted this vote
/// This is the Governing Token Owner who deposited governing tokens into the Realm
pub governing_token_owner: Pubkey,
/// Voter's vote: Yes/No and amount
pub vote: Option<VoteWeight>,
}

View File

@ -1,104 +0,0 @@
//! Realm Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
id,
tools::account::{assert_is_valid_account, deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::GovernanceAccountType;
/// Governance Realm Account
/// Account PDA seeds" ['governance', name]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct Realm {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Community mint
pub community_mint: Pubkey,
/// Council mint
pub council_mint: Option<Pubkey>,
/// Governance Realm name
pub name: String,
}
impl AccountMaxSize for Realm {}
impl IsInitialized for Realm {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::Realm
}
}
impl Realm {
/// Asserts the given mint is either Community or Council mint of the Realm
pub fn assert_is_valid_governing_token_mint(
&self,
governing_token_mint: &Pubkey,
) -> Result<(), ProgramError> {
if self.community_mint == *governing_token_mint {
return Ok(());
}
if self.council_mint == Some(*governing_token_mint) {
return Ok(());
}
Err(GovernanceError::InvalidGoverningTokenMint.into())
}
}
/// Checks whether realm account exists, is initialized and owned by Governance program
pub fn assert_is_valid_realm(realm_info: &AccountInfo) -> Result<(), ProgramError> {
assert_is_valid_account(realm_info, GovernanceAccountType::Realm, &id())
}
/// Deserializes account and checks owner program
pub fn deserialize_realm_raw(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
deserialize_account::<Realm>(realm_info, &id())
}
/// Returns Realm PDA seeds
pub fn get_realm_address_seeds(name: &str) -> [&[u8]; 2] {
[PROGRAM_AUTHORITY_SEED, &name.as_bytes()]
}
/// Returns Realm PDA address
pub fn get_realm_address(name: &str) -> Pubkey {
Pubkey::find_program_address(&get_realm_address_seeds(&name), &id()).0
}
/// Returns Realm Token Holding PDA seeds
pub fn get_governing_token_holding_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
]
}
/// Returns Realm Token Holding PDA address
pub fn get_governing_token_holding_address(
realm: &Pubkey,
governing_token_mint: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_governing_token_holding_address_seeds(realm, governing_token_mint),
&id(),
)
.0
}

View File

@ -1,108 +0,0 @@
//! Signatory Record
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::GovernanceAccountType;
/// Account PDA seeds: ['governance', proposal, signatory]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct SignatoryRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Proposal the signatory is assigned for
pub proposal: Pubkey,
/// The account of the signatory who can sign off the proposal
pub signatory: Pubkey,
/// Indicates whether the signatory signed off the proposal
pub signed_off: bool,
}
impl AccountMaxSize for SignatoryRecord {}
impl IsInitialized for SignatoryRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::SignatoryRecord
}
}
impl SignatoryRecord {
/// Checks signatory hasn't signed off yet and is transaction signer
pub fn assert_can_sign_off(&self, signatory_info: &AccountInfo) -> Result<(), ProgramError> {
if self.signed_off {
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
}
if !signatory_info.is_signer {
return Err(GovernanceError::SignatoryMustSign.into());
}
Ok(())
}
/// Checks signatory can be removed from Proposal
pub fn assert_can_remove_signatory(&self) -> Result<(), ProgramError> {
if self.signed_off {
return Err(GovernanceError::SignatoryAlreadySignedOff.into());
}
Ok(())
}
}
/// Returns SignatoryRecord PDA seeds
pub fn get_signatory_record_address_seeds<'a>(
proposal: &'a Pubkey,
signatory: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
proposal.as_ref(),
signatory.as_ref(),
]
}
/// Returns SignatoryRecord PDA address
pub fn get_signatory_record_address<'a>(proposal: &'a Pubkey, signatory: &'a Pubkey) -> Pubkey {
Pubkey::find_program_address(
&get_signatory_record_address_seeds(proposal, signatory),
&id(),
)
.0
}
/// Deserializes SignatoryRecord account and checks owner program
pub fn deserialize_signatory_record_raw(
signatory_record_info: &AccountInfo,
) -> Result<SignatoryRecord, ProgramError> {
deserialize_account::<SignatoryRecord>(signatory_record_info, &id())
}
/// Deserializes SignatoryRecord and validates its PDA
pub fn deserialize_signatory_record(
signatory_record_info: &AccountInfo,
proposal: &Pubkey,
signatory: &Pubkey,
) -> Result<SignatoryRecord, ProgramError> {
let (signatory_record_address, _) = Pubkey::find_program_address(
&get_signatory_record_address_seeds(proposal, signatory),
&id(),
);
if signatory_record_address != *signatory_record_info.key {
return Err(GovernanceError::InvalidSignatoryAddress.into());
}
deserialize_signatory_record_raw(signatory_record_info)
}

View File

@ -1,28 +0,0 @@
//! SingleSignerInstruction Account
use crate::state::enums::GovernanceAccountType;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
/// Account for an instruction to be executed for Proposal
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct SingleSignerInstruction {
/// Governance Account type
pub account_type: GovernanceAccountType,
/// Minimum waiting time in slots for the instruction to be executed once proposal is voted on
pub hold_up_time: u64,
/// Instruction to execute
/// The instruction will be signed by Governance PDA the Proposal belongs to
// For example for ProgramGovernance the instruction to upgrade program will be signed by ProgramGovernance PDA
pub instruction: InstructionData,
/// Executed flag
pub executed: bool,
}
/// Temp. placeholder until I get Borsh serialization for Instruction working
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
#[repr(C)]
pub struct InstructionData {}

View File

@ -1,166 +0,0 @@
//! Token Owner Record Account
use crate::{
error::GovernanceError,
id,
tools::account::{deserialize_account, AccountMaxSize},
PROGRAM_AUTHORITY_SEED,
};
use crate::state::enums::GovernanceAccountType;
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
pubkey::Pubkey,
};
/// Governance Token Owner Record
/// Account PDA seeds: ['governance', realm, token_mint, token_owner ]
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct TokenOwnerRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// The Realm the TokenOwnerRecord belongs to
pub realm: Pubkey,
/// Governing Token Mint the TokenOwnerRecord holds deposit for
pub governing_token_mint: Pubkey,
/// The owner (either single or multisig) of the deposited governing SPL Tokens
/// This is who can authorize a withdrawal
pub governing_token_owner: Pubkey,
/// The amount of governing tokens deposited into the Realm
/// This amount is the voter weight used when voting on proposals
pub governing_token_deposit_amount: u64,
/// A single account that is allowed to operate governance with the deposited governing tokens
/// It's delegated to by the governing token owner or current governance_delegate
pub governance_delegate: Option<Pubkey>,
/// The number of active votes cast by TokenOwner
pub active_votes_count: u16,
/// The total number of votes cast by the TokenOwner
pub total_votes_count: u16,
}
impl AccountMaxSize for TokenOwnerRecord {
fn get_max_size(&self) -> Option<usize> {
Some(142)
}
}
impl IsInitialized for TokenOwnerRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::TokenOwnerRecord
}
}
/// Returns TokenOwnerRecord PDA address
pub fn get_token_owner_record_address(
realm: &Pubkey,
governing_token_mint: &Pubkey,
governing_token_owner: &Pubkey,
) -> Pubkey {
Pubkey::find_program_address(
&get_token_owner_record_address_seeds(realm, governing_token_mint, governing_token_owner),
&id(),
)
.0
}
/// Returns TokenOwnerRecord PDA seeds
pub fn get_token_owner_record_address_seeds<'a>(
realm: &'a Pubkey,
governing_token_mint: &'a Pubkey,
governing_token_owner: &'a Pubkey,
) -> [&'a [u8]; 4] {
[
PROGRAM_AUTHORITY_SEED,
realm.as_ref(),
governing_token_mint.as_ref(),
governing_token_owner.as_ref(),
]
}
/// Deserializes TokenOwnerRecord account and checks owner program
pub fn deserialize_token_owner_record_raw(
token_owner_record_info: &AccountInfo,
) -> Result<TokenOwnerRecord, ProgramError> {
deserialize_account::<TokenOwnerRecord>(token_owner_record_info, &id())
}
/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds
pub fn deserialize_token_owner_record(
token_owner_record_info: &AccountInfo,
token_owner_record_seeds: &[&[u8]],
) -> Result<TokenOwnerRecord, ProgramError> {
let (token_owner_record_address, _) =
Pubkey::find_program_address(token_owner_record_seeds, &id());
if token_owner_record_address != *token_owner_record_info.key {
return Err(GovernanceError::InvalidTokenOwnerRecordAccountAddress.into());
}
deserialize_token_owner_record_raw(token_owner_record_info)
}
/// Deserializes TokenOwnerRecord account and checks that its PDA matches the given realm and governing mint
pub fn deserialize_token_owner_record_for_realm_and_governing_mint(
token_owner_record_info: &AccountInfo,
realm: &Pubkey,
governing_token_mint: &Pubkey,
) -> Result<TokenOwnerRecord, ProgramError> {
let token_owner_record_data = deserialize_token_owner_record_raw(token_owner_record_info)?;
if token_owner_record_data.governing_token_mint != *governing_token_mint {
return Err(GovernanceError::InvalidTokenOwnerRecordGoverningMint.into());
}
if token_owner_record_data.realm != *realm {
return Err(GovernanceError::InvalidTokenOwnerRecordRealm.into());
}
Ok(token_owner_record_data)
}
/// Deserializes TokenOwnerRecord account and checks its address is the give proposal_owner
pub fn deserialize_token_owner_record_for_proposal_owner(
token_owner_record_info: &AccountInfo,
proposal_owner: &Pubkey,
) -> Result<TokenOwnerRecord, ProgramError> {
if token_owner_record_info.key != proposal_owner {
return Err(GovernanceError::InvalidProposalOwnerAccount.into());
}
deserialize_token_owner_record_raw(token_owner_record_info)
}
#[cfg(test)]
mod test {
use solana_program::borsh::get_packed_len;
use super::*;
#[test]
fn test_max_size() {
let token_owner_record = TokenOwnerRecord {
account_type: GovernanceAccountType::TokenOwnerRecord,
realm: Pubkey::new_unique(),
governing_token_mint: Pubkey::new_unique(),
governing_token_owner: Pubkey::new_unique(),
governing_token_deposit_amount: 10,
governance_delegate: Some(Pubkey::new_unique()),
active_votes_count: 1,
total_votes_count: 1,
};
let size = get_packed_len::<TokenOwnerRecord>();
assert_eq!(token_owner_record.get_max_size(), Some(size));
}
}

View File

@ -1,55 +0,0 @@
//! Proposal Vote Record Account
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
use solana_program::{program_pack::IsInitialized, pubkey::Pubkey};
use crate::{id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
use crate::state::enums::{GovernanceAccountType, VoteWeight};
/// Proposal VoteRecord
#[repr(C)]
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct VoteRecord {
/// Governance account type
pub account_type: GovernanceAccountType,
/// Proposal account
pub proposal: Pubkey,
/// The user who casted this vote
/// This is the Governing Token Owner who deposited governing tokens into the Realm
pub governing_token_owner: Pubkey,
/// Voter's vote: Yes/No and amount
pub vote_weight: VoteWeight,
}
impl AccountMaxSize for VoteRecord {}
impl IsInitialized for VoteRecord {
fn is_initialized(&self) -> bool {
self.account_type == GovernanceAccountType::VoteRecord
}
}
/// Returns VoteRecord PDA seeds
pub fn get_vote_record_address_seeds<'a>(
proposal: &'a Pubkey,
token_owner_record: &'a Pubkey,
) -> [&'a [u8]; 3] {
[
PROGRAM_AUTHORITY_SEED,
proposal.as_ref(),
token_owner_record.as_ref(),
]
}
/// Returns VoteRecord PDA address
pub fn get_vote_record_address<'a>(proposal: &'a Pubkey, token_owner_record: &'a Pubkey) -> Pubkey {
Pubkey::find_program_address(
&get_vote_record_address_seeds(proposal, token_owner_record),
&id(),
)
.0
}

View File

@ -1,143 +0,0 @@
//! General purpose account utility functions
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo, borsh::try_from_slice_unchecked, msg, program::invoke_signed,
program_error::ProgramError, program_pack::IsInitialized, pubkey::Pubkey, rent::Rent,
system_instruction::create_account,
};
use crate::error::GovernanceError;
/// Trait for accounts to return their max size
pub trait AccountMaxSize {
/// Returns max account size or None if max size is not known and actual instance size should be used
fn get_max_size(&self) -> Option<usize> {
None
}
}
/// Creates a new account and serializes data into it using the provided seeds to invoke signed CPI call
/// Note: This functions also checks the provided account PDA matches the supplied seeds
pub fn create_and_serialize_account_signed<'a, T: BorshSerialize + AccountMaxSize>(
payer_info: &AccountInfo<'a>,
account_info: &AccountInfo<'a>,
account_data: &T,
account_address_seeds: &[&[u8]],
program_id: &Pubkey,
system_info: &AccountInfo<'a>,
rent: &Rent,
) -> Result<(), ProgramError> {
// Get PDA and assert it's the same as the requested account address
let (account_address, bump_seed) =
Pubkey::find_program_address(account_address_seeds, program_id);
if account_address != *account_info.key {
msg!(
"Create account with PDA: {:?} was requested while PDA: {:?} was expected",
account_info.key,
account_address
);
return Err(ProgramError::InvalidSeeds);
}
let (serialized_data, account_size) = if let Some(max_size) = account_data.get_max_size() {
(None, max_size)
} else {
let serialized_data = account_data.try_to_vec()?;
let account_size = serialized_data.len();
(Some(serialized_data), account_size)
};
let create_account_instruction = create_account(
payer_info.key,
account_info.key,
rent.minimum_balance(account_size),
account_size as u64,
program_id,
);
let mut signers_seeds = account_address_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&create_account_instruction,
&[
payer_info.clone(),
account_info.clone(),
system_info.clone(),
],
&[&signers_seeds[..]],
)?;
if let Some(serialized_data) = serialized_data {
account_info
.data
.borrow_mut()
.copy_from_slice(&serialized_data);
} else {
account_data.serialize(&mut *account_info.data.borrow_mut())?;
}
Ok(())
}
/// Deserializes account and checks it's initialized and owned by the specified program
pub fn deserialize_account<T: BorshDeserialize + IsInitialized>(
account_info: &AccountInfo,
owner_program_id: &Pubkey,
) -> Result<T, ProgramError> {
if account_info.data_is_empty() {
return Err(ProgramError::UninitializedAccount);
}
if account_info.owner != owner_program_id {
return Err(GovernanceError::InvalidAccountOwner.into());
}
let account: T = try_from_slice_unchecked(&account_info.data.borrow())?;
if !account.is_initialized() {
Err(ProgramError::UninitializedAccount)
} else {
Ok(account)
}
}
/// Asserts the given account is not empty, owned given program and of the expected type
pub fn assert_is_valid_account<T: BorshDeserialize + PartialEq>(
account_info: &AccountInfo,
expected_account_type: T,
owner_program_id: &Pubkey,
) -> Result<(), ProgramError> {
if account_info.owner != owner_program_id {
return Err(GovernanceError::InvalidAccountOwner.into());
}
if account_info.data_is_empty() {
return Err(ProgramError::UninitializedAccount);
}
let account_type: T = try_from_slice_unchecked(&account_info.data.borrow())?;
if account_type != expected_account_type {
return Err(GovernanceError::InvalidAccountType.into());
};
Ok(())
}
/// Disposes account by transferring its lamports to the beneficiary account and zeros its data
// After transaction completes the runtime would remove the account with no lamports
pub fn dispose_account(account_info: &AccountInfo, beneficiary_account: &AccountInfo) {
let account_lamports = account_info.lamports();
**account_info.lamports.borrow_mut() = 0;
**beneficiary_account.lamports.borrow_mut() = beneficiary_account
.lamports()
.checked_add(account_lamports)
.unwrap();
let mut account_data = account_info.data.borrow_mut();
account_data.fill(0);
}

View File

@ -1,25 +0,0 @@
//! Governance asserts
use solana_program::{account_info::AccountInfo, program_error::ProgramError};
use crate::{error::GovernanceError, state::token_owner_record::TokenOwnerRecord};
/// Checks whether the provided Governance Authority signed transaction
pub fn assert_token_owner_or_delegate_is_signer(
token_owner_record: &TokenOwnerRecord,
governance_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
if governance_authority_info.is_signer {
if &token_owner_record.governing_token_owner == governance_authority_info.key {
return Ok(());
}
if let Some(governance_delegate) = token_owner_record.governance_delegate {
if &governance_delegate == governance_authority_info.key {
return Ok(());
}
};
}
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
}

View File

@ -1,96 +0,0 @@
//! General purpose bpf_loader_upgradeable utility functions
use solana_program::{
account_info::AccountInfo,
bpf_loader_upgradeable::{self, UpgradeableLoaderState},
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
};
use bincode::deserialize;
use crate::error::GovernanceError;
/// Returns ProgramData account address for the given Program
pub fn get_program_data_address(program: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[program.as_ref()], &bpf_loader_upgradeable::id()).0
}
/// Returns upgrade_authority from the given Upgradable Loader Account
pub fn get_program_upgrade_authority(
upgradable_loader_state: &UpgradeableLoaderState,
) -> Result<Option<Pubkey>, ProgramError> {
let upgrade_authority = match upgradable_loader_state {
UpgradeableLoaderState::ProgramData {
slot: _,
upgrade_authority_address,
} => *upgrade_authority_address,
_ => return Err(ProgramError::InvalidAccountData),
};
Ok(upgrade_authority)
}
/// Sets new upgrade authority for the given upgradable program
pub fn set_program_upgrade_authority<'a>(
program_address: &Pubkey,
program_data_info: &AccountInfo<'a>,
program_upgrade_authority_info: &AccountInfo<'a>,
new_authority_info: &AccountInfo<'a>,
bpf_upgrade_loader_info: &AccountInfo<'a>,
) -> Result<(), ProgramError> {
let set_upgrade_authority_instruction = bpf_loader_upgradeable::set_upgrade_authority(
program_address,
&program_upgrade_authority_info.key,
Some(&new_authority_info.key),
);
invoke(
&set_upgrade_authority_instruction,
&[
program_data_info.clone(),
program_upgrade_authority_info.clone(),
bpf_upgrade_loader_info.clone(),
new_authority_info.clone(),
],
)
}
/// Asserts the program is upgradable and its upgrade authority is a signer of the transaction
pub fn assert_program_upgrade_authority_is_signer(
program_address: &Pubkey,
program_data_info: &AccountInfo,
program_upgrade_authority_info: &AccountInfo,
) -> Result<(), ProgramError> {
if program_data_info.owner != &bpf_loader_upgradeable::id() {
return Err(ProgramError::IncorrectProgramId);
}
let program_data_address = get_program_data_address(program_address);
if program_data_address != *program_data_info.key {
return Err(GovernanceError::InvalidProgramDataAccountAddress.into());
}
let upgrade_authority = if let UpgradeableLoaderState::ProgramData {
slot: _,
upgrade_authority_address,
} = deserialize(&program_data_info.data.borrow())
.map_err(|_| GovernanceError::InvalidProgramDataAccountData)?
{
upgrade_authority_address
} else {
None
};
let upgrade_authority = upgrade_authority.ok_or(GovernanceError::ProgramNotUpgradable)?;
if upgrade_authority != *program_upgrade_authority_info.key {
return Err(GovernanceError::InvalidUpgradeAuthority.into());
}
if !program_upgrade_authority_info.is_signer {
return Err(GovernanceError::UpgradeAuthorityMustSign.into());
}
Ok(())
}

View File

@ -1,9 +0,0 @@
//! Utility functions
pub mod account;
pub mod token;
pub mod asserts;
pub mod bpf_loader_upgradeable;

View File

@ -1,211 +0,0 @@
//! General purpose SPL token utility functions
use arrayref::array_ref;
use solana_program::{
account_info::AccountInfo,
entrypoint::ProgramResult,
msg,
program::{invoke, invoke_signed},
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
system_instruction,
};
use crate::error::GovernanceError;
/// Creates and initializes SPL token account with PDA using the provided PDA seeds
#[allow(clippy::too_many_arguments)]
pub fn create_spl_token_account_signed<'a>(
payer_info: &AccountInfo<'a>,
token_account_info: &AccountInfo<'a>,
token_account_address_seeds: &[&[u8]],
token_mint_info: &AccountInfo<'a>,
token_account_owner_info: &AccountInfo<'a>,
program_id: &Pubkey,
system_info: &AccountInfo<'a>,
spl_token_info: &AccountInfo<'a>,
rent_sysvar_info: &AccountInfo<'a>,
rent: &Rent,
) -> Result<(), ProgramError> {
let create_account_instruction = system_instruction::create_account(
payer_info.key,
token_account_info.key,
1.max(rent.minimum_balance(spl_token::state::Account::get_packed_len())),
spl_token::state::Account::get_packed_len() as u64,
&spl_token::id(),
);
let (account_address, bump_seed) =
Pubkey::find_program_address(token_account_address_seeds, program_id);
if account_address != *token_account_info.key {
msg!(
"Create SPL Token Account with PDA: {:?} was requested while PDA: {:?} was expected",
token_account_info.key,
account_address
);
return Err(ProgramError::InvalidSeeds);
}
let mut signers_seeds = token_account_address_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&create_account_instruction,
&[
payer_info.clone(),
token_account_info.clone(),
system_info.clone(),
],
&[&signers_seeds[..]],
)?;
let initialize_account_instruction = spl_token::instruction::initialize_account(
&spl_token::id(),
token_account_info.key,
token_mint_info.key,
token_account_owner_info.key,
)?;
invoke(
&initialize_account_instruction,
&[
payer_info.clone(),
token_account_info.clone(),
token_account_owner_info.clone(),
token_mint_info.clone(),
spl_token_info.clone(),
rent_sysvar_info.clone(),
],
)?;
Ok(())
}
/// Transfers SPL Tokens
pub fn transfer_spl_tokens<'a>(
source_info: &AccountInfo<'a>,
destination_info: &AccountInfo<'a>,
authority_info: &AccountInfo<'a>,
amount: u64,
spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::id(),
source_info.key,
destination_info.key,
authority_info.key,
&[],
amount,
)
.unwrap();
invoke(
&transfer_instruction,
&[
spl_token_info.clone(),
authority_info.clone(),
source_info.clone(),
destination_info.clone(),
],
)?;
Ok(())
}
/// Transfers SPL Tokens from a token account owned by the provided PDA authority with seeds
pub fn transfer_spl_tokens_signed<'a>(
source_info: &AccountInfo<'a>,
destination_info: &AccountInfo<'a>,
authority_info: &AccountInfo<'a>,
authority_seeds: &[&[u8]],
program_id: &Pubkey,
amount: u64,
spl_token_info: &AccountInfo<'a>,
) -> ProgramResult {
let (authority_address, bump_seed) = Pubkey::find_program_address(authority_seeds, program_id);
if authority_address != *authority_info.key {
msg!(
"Transfer SPL Token with Authority PDA: {:?} was requested while PDA: {:?} was expected",
authority_info.key,
authority_address
);
return Err(ProgramError::InvalidSeeds);
}
let transfer_instruction = spl_token::instruction::transfer(
&spl_token::id(),
source_info.key,
destination_info.key,
authority_info.key,
&[],
amount,
)
.unwrap();
let mut signers_seeds = authority_seeds.to_vec();
let bump = &[bump_seed];
signers_seeds.push(bump);
invoke_signed(
&transfer_instruction,
&[
spl_token_info.clone(),
authority_info.clone(),
source_info.clone(),
destination_info.clone(),
],
&[&signers_seeds[..]],
)?;
Ok(())
}
/// Computationally cheap method to get amount from a token account
/// It reads amount without deserializing full account data
pub fn get_amount_from_token_account(
token_account_info: &AccountInfo,
) -> Result<u64, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data()?;
let amount = array_ref![data, 64, 8];
Ok(u64::from_le_bytes(*amount))
}
/// Computationally cheap method to get mint from a token account
/// It reads mint without deserializing full account data
pub fn get_mint_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8), ...
let data = token_account_info.try_borrow_data()?;
let mint_data = array_ref![data, 0, 32];
Ok(Pubkey::new_from_array(*mint_data))
}
/// Computationally cheap method to get owner from a token account
/// It reads owner without deserializing full account data
pub fn get_owner_from_token_account(
token_account_info: &AccountInfo,
) -> Result<Pubkey, ProgramError> {
if token_account_info.owner != &spl_token::id() {
return Err(GovernanceError::InvalidTokenAccountOwner.into());
}
// TokeAccount layout: mint(32), owner(32), amount(8)
let data = token_account_info.try_borrow_data()?;
let owner_data = array_ref![data, 32, 32];
Ok(Pubkey::new_from_array(*owner_data))
}

View File

@ -1 +0,0 @@
!*.so

View File

@ -1,132 +0,0 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::error::GovernanceError;
#[tokio::test]
async fn test_add_signatory() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Act
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let signatory_record_account = governance_test
.get_signatory_record_account(&signatory_record_cookie.address)
.await;
assert_eq!(signatory_record_cookie.account, signatory_record_account);
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(1, proposal_account.signatories_count);
}
#[tokio::test]
async fn test_add_signatory_with_owner_or_delegate_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
// Act
let err = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_add_signatory_with_invalid_proposal_owner_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
// Act
let err = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
}

View File

@ -1,110 +0,0 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::*;
use program_test::*;
use spl_governance::{error::GovernanceError, state::governance::GovernanceConfig};
#[tokio::test]
async fn test_create_account_governance() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
// Act
let account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
// Assert
let account_governance_account = governance_test
.get_governance_account(&account_governance_cookie.address)
.await;
assert_eq!(
account_governance_cookie.account,
account_governance_account
);
}
#[tokio::test]
async fn test_create_account_governance_with_invalid_realm_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let mut realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
realm_cookie.address = account_governance_cookie.address;
// Act
let err = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidAccountType.into());
}
#[tokio::test]
async fn test_create_account_governance_with_invalid_config_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
// Arrange below 50% threshold
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
vote_threshold_percentage: 49, // below 50% threshold
min_tokens_to_create_proposal: 1,
min_instruction_hold_up_time: 1,
max_voting_time: 1,
};
// Act
let err = governance_test
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidGovernanceConfig.into());
// Arrange above 100% threshold
let config = GovernanceConfig {
realm: realm_cookie.address,
governed_account: governed_account_cookie.address,
vote_threshold_percentage: 101, // Above 100% threshold
min_tokens_to_create_proposal: 1,
min_instruction_hold_up_time: 1,
max_voting_time: 1,
};
// Act
let err = governance_test
.with_account_governance_config(&realm_cookie, &governed_account_cookie, config)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidGovernanceConfig.into());
}

View File

@ -1,180 +0,0 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::*;
use program_test::{tools::ProgramInstructionError, *};
use solana_sdk::signature::{Keypair, Signer};
use spl_governance::{
error::GovernanceError, tools::bpf_loader_upgradeable::get_program_upgrade_authority,
};
#[tokio::test]
async fn test_create_program_governance() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_program_cookie = governance_test.with_governed_program().await;
// Act
let program_governance_cookie = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.unwrap();
// Assert
let program_governance_account = governance_test
.get_governance_account(&program_governance_cookie.address)
.await;
assert_eq!(
program_governance_cookie.account,
program_governance_account
);
let program_data = governance_test
.get_upgradable_loader_account(&governed_program_cookie.data_address)
.await;
let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap();
assert_eq!(Some(program_governance_cookie.address), upgrade_authority);
}
#[tokio::test]
async fn test_create_program_governance_without_transferring_upgrade_authority() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut governed_program_cookie = governance_test.with_governed_program().await;
governed_program_cookie.transfer_upgrade_authority = false;
// Act
let program_governance_cookie = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.unwrap();
// Assert
let program_governance_account = governance_test
.get_governance_account(&program_governance_cookie.address)
.await;
assert_eq!(
program_governance_cookie.account,
program_governance_account
);
let program_data = governance_test
.get_upgradable_loader_account(&governed_program_cookie.data_address)
.await;
let upgrade_authority = get_program_upgrade_authority(&program_data).unwrap();
assert_eq!(
Some(governed_program_cookie.upgrade_authority.pubkey()),
upgrade_authority
);
}
#[tokio::test]
async fn test_create_program_governance_without_transferring_upgrade_authority_with_invalid_authority_error(
) {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut governed_program_cookie = governance_test.with_governed_program().await;
governed_program_cookie.transfer_upgrade_authority = false;
governed_program_cookie.upgrade_authority = Keypair::new();
// Act
let err = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidUpgradeAuthority.into());
}
#[tokio::test]
async fn test_create_program_governance_without_transferring_upgrade_authority_with_authority_not_signed_error(
) {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut governed_program_cookie = governance_test.with_governed_program().await;
governed_program_cookie.transfer_upgrade_authority = false;
// Act
let err = governance_test
.with_program_governance_instruction(
&realm_cookie,
&governed_program_cookie,
|i| {
i.accounts[3].is_signer = false; // governed_program_upgrade_authority
},
Some(&[]),
)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::UpgradeAuthorityMustSign.into());
}
#[tokio::test]
async fn test_create_program_governance_with_incorrect_upgrade_authority_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut governed_program_cookie = governance_test.with_governed_program().await;
governed_program_cookie.upgrade_authority = Keypair::new();
// Act
let err = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, ProgramInstructionError::IncorrectAuthority.into());
}
#[tokio::test]
async fn test_create_program_governance_with_invalid_realm_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let mut realm_cookie = governance_test.with_realm().await;
let governed_program_cookie = governance_test.with_governed_program().await;
let program_governance_cookie = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.unwrap();
realm_cookie.address = program_governance_cookie.address;
// Act
let err = governance_test
.with_program_governance(&realm_cookie, &governed_program_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidAccountType.into());
}

View File

@ -1,255 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::Keypair;
use spl_governance::error::GovernanceError;
#[tokio::test]
async fn test_community_proposal_created() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(proposal_cookie.account, proposal_account);
let account_governance_account = governance_test
.get_governance_account(&account_governance_cookie.address)
.await;
assert_eq!(1, account_governance_account.proposals_count);
}
#[tokio::test]
async fn test_multiple_proposals_created() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let community_token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let council_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
let community_proposal_cookie = governance_test
.with_proposal(
&community_token_owner_record_cookie,
&mut account_governance_cookie,
)
.await
.unwrap();
let council_proposal_cookie = governance_test
.with_proposal(
&council_token_owner_record_cookie,
&mut account_governance_cookie,
)
.await
.unwrap();
// Assert
let community_proposal_account = governance_test
.get_proposal_account(&community_proposal_cookie.address)
.await;
assert_eq!(
community_proposal_cookie.account,
community_proposal_account
);
let council_proposal_account = governance_test
.get_proposal_account(&council_proposal_cookie.address)
.await;
assert_eq!(council_proposal_cookie.account, council_proposal_account);
let account_governance_account = governance_test
.get_governance_account(&account_governance_cookie.address)
.await;
assert_eq!(2, account_governance_account.proposals_count);
}
#[tokio::test]
async fn test_create_proposal_with_not_authorized_governance_authority_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.governance_authority = Some(Keypair::new());
// Act
let err = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_create_proposal_with_governance_delegate_signer() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
token_owner_record_cookie.governance_authority =
Some(token_owner_record_cookie.clone_governance_delegate());
// Act
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(proposal_cookie.account, proposal_account);
}
#[tokio::test]
async fn test_create_proposal_with_not_enough_tokens_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_amount = account_governance_cookie
.account
.config
.min_tokens_to_create_proposal as u64
- 1;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit_amount(&realm_cookie, token_amount)
.await;
// Act
let err = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::NotEnoughTokensToCreateProposal.into());
}
#[tokio::test]
async fn test_create_proposal_with_invalid_token_owner_record_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let council_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
let err = governance_test
.with_proposal_instruction(
&token_owner_record_cookie,
&mut account_governance_cookie,
|i| {
// Set token_owner_record_address for different (Council) mint
i.accounts[2] =
AccountMeta::new_readonly(council_token_owner_record_cookie.address, false);
},
)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::InvalidTokenOwnerRecordGoverningMint.into()
);
}

View File

@ -1,23 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program_test::*;
mod program_test;
use program_test::*;
#[tokio::test]
async fn test_realm_created() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
// Act
let realm_cookie = governance_test.with_realm().await;
// Assert
let realm_account = governance_test
.get_realm_account(&realm_cookie.address)
.await;
assert_eq!(realm_cookie.account, realm_account);
}

View File

@ -1,258 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::{Keypair, Signer};
use spl_governance::{error::GovernanceError, instruction::deposit_governing_tokens};
#[tokio::test]
async fn test_deposit_initial_community_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
// Act
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(token_owner_record_cookie.account, token_owner_record);
let source_account = governance_test
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
token_owner_record_cookie.token_source_amount
- token_owner_record_cookie
.account
.governing_token_deposit_amount,
source_account.amount
);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(
token_owner_record.governing_token_deposit_amount,
holding_account.amount
);
}
#[tokio::test]
async fn test_deposit_initial_council_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
// Act
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(token_owner_record_cookie.account, token_owner_record);
let source_account = governance_test
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
token_owner_record_cookie.token_source_amount
- token_owner_record_cookie
.account
.governing_token_deposit_amount,
source_account.amount
);
let holding_account = governance_test
.get_token_account(&council_token_holding_account)
.await;
assert_eq!(
token_owner_record.governing_token_deposit_amount,
holding_account.amount
);
}
#[tokio::test]
async fn test_deposit_subsequent_community_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let deposit_amount = 5;
let total_deposit_amount = token_owner_record_cookie
.account
.governing_token_deposit_amount
+ deposit_amount;
// Act
governance_test
.with_community_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
total_deposit_amount,
token_owner_record.governing_token_deposit_amount
);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(total_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_subsequent_council_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let council_token_holding_account = realm_cookie.council_token_holding_account.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
let deposit_amount = 5;
let total_deposit_amount = token_owner_record_cookie
.account
.governing_token_deposit_amount
+ deposit_amount;
// Act
governance_test
.with_council_token_deposit(&realm_cookie, &token_owner_record_cookie, deposit_amount)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
total_deposit_amount,
token_owner_record.governing_token_deposit_amount
);
let holding_account = governance_test
.get_token_account(&council_token_holding_account)
.await;
assert_eq!(total_deposit_amount, holding_account.amount);
}
#[tokio::test]
async fn test_deposit_initial_community_tokens_with_owner_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner = Keypair::new();
let transfer_authority = Keypair::new();
let token_source = Keypair::new();
governance_test
.create_token_account_with_transfer_authority(
&token_source,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
10,
&token_owner,
&transfer_authority.pubkey(),
)
.await;
let mut instruction = deposit_governing_tokens(
&realm_cookie.address,
&token_source.pubkey(),
&token_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[3] = AccountMeta::new_readonly(token_owner.pubkey(), false);
// // Act
let error = governance_test
.process_transaction(&[instruction], Some(&[&transfer_authority]))
.await
.err()
.unwrap();
// Assert
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
}
#[tokio::test]
async fn test_deposit_initial_community_tokens_with_invalid_owner_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner = Keypair::new();
let transfer_authority = Keypair::new();
let token_source = Keypair::new();
let invalid_owner = Keypair::new();
governance_test
.create_token_account_with_transfer_authority(
&token_source,
&realm_cookie.account.community_mint,
&realm_cookie.community_mint_authority,
10,
&token_owner,
&transfer_authority.pubkey(),
)
.await;
let instruction = deposit_governing_tokens(
&realm_cookie.address,
&token_source.pubkey(),
&invalid_owner.pubkey(),
&transfer_authority.pubkey(),
&governance_test.payer.pubkey(),
&realm_cookie.account.community_mint,
);
// // Act
let error = governance_test
.process_transaction(&[instruction], Some(&[&transfer_authority, &invalid_owner]))
.await
.err()
.unwrap();
// Assert
assert_eq!(error, GovernanceError::GoverningTokenOwnerMustSign.into());
}

View File

@ -1,219 +0,0 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::{error::GovernanceError, state::enums::ProposalState};
#[tokio::test]
async fn test_remove_signatory() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Act
governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(0, proposal_account.signatories_count);
assert_eq!(ProposalState::Draft, proposal_account.state);
let signatory_account = governance_test
.banks_client
.get_account(signatory_record_cookie.address)
.await
.unwrap();
assert_eq!(None, signatory_account);
}
#[tokio::test]
async fn test_remove_signatory_with_owner_or_delegate_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.token_owner = other_token_owner_record_cookie.token_owner;
// Act
let err = governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_remove_signatory_with_invalid_proposal_owner_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let other_token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
token_owner_record_cookie.address = other_token_owner_record_cookie.address;
// Act
let err = governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie,
)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::InvalidProposalOwnerAccount.into());
}
#[tokio::test]
async fn test_remove_signatory_when_all_remaining_signed() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let signatory_record_cookie1 = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
let signatory_record_cookie2 = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
governance_test
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie1)
.await
.unwrap();
// Act
governance_test
.remove_signatory(
&proposal_cookie,
&token_owner_record_cookie,
&signatory_record_cookie2,
)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(1, proposal_account.signatories_count);
assert_eq!(1, proposal_account.signatories_signed_off_count);
assert_eq!(ProposalState::Voting, proposal_account.state);
}

View File

@ -1,165 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program::instruction::AccountMeta;
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::{Keypair, Signer};
use spl_governance::{error::GovernanceError, instruction::set_governance_delegate};
#[tokio::test]
async fn test_set_community_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(token_owner_record_cookie.governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}
#[tokio::test]
async fn test_set_governance_delegate_to_none() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Act
governance_test
.set_governance_delegate(
&realm_cookie,
&token_owner_record_cookie,
&token_owner_record_cookie.token_owner,
&realm_cookie.account.community_mint,
&None,
)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(None, token_owner_record.governance_delegate);
}
#[tokio::test]
async fn test_set_council_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.with_council_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(token_owner_record_cookie.governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}
#[tokio::test]
async fn test_set_community_governance_delegate_with_owner_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let hacker_governance_delegate = Keypair::new();
let mut instruction = set_governance_delegate(
&token_owner_record_cookie.token_owner.pubkey(),
&realm_cookie.address,
&realm_cookie.account.community_mint,
&token_owner_record_cookie.token_owner.pubkey(),
&Some(hacker_governance_delegate.pubkey()),
);
instruction.accounts[0] =
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
// Act
let err = governance_test
.process_transaction(&[instruction], None)
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
);
}
#[tokio::test]
async fn test_set_community_governance_delegate_signed_by_governance_delegate() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let mut token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
governance_test
.with_community_governance_delegate(&realm_cookie, &mut token_owner_record_cookie)
.await;
let new_governance_delegate = Keypair::new();
// Act
governance_test
.set_governance_delegate(
&realm_cookie,
&token_owner_record_cookie,
&token_owner_record_cookie.governance_delegate,
&realm_cookie.account.community_mint,
&Some(new_governance_delegate.pubkey()),
)
.await;
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(
Some(new_governance_delegate.pubkey()),
token_owner_record.governance_delegate
);
}

View File

@ -1,59 +0,0 @@
#![cfg(feature = "test-bpf")]
mod program_test;
use solana_program_test::tokio;
use program_test::*;
use spl_governance::state::enums::ProposalState;
#[tokio::test]
async fn test_sign_off_proposal() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let governed_account_cookie = governance_test.with_governed_account().await;
let mut account_governance_cookie = governance_test
.with_account_governance(&realm_cookie, &governed_account_cookie)
.await
.unwrap();
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let proposal_cookie = governance_test
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
.await
.unwrap();
let signatory_record_cookie = governance_test
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Act
governance_test
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
.await
.unwrap();
// Assert
let proposal_account = governance_test
.get_proposal_account(&proposal_cookie.address)
.await;
assert_eq!(1, proposal_account.signatories_count);
assert_eq!(1, proposal_account.signatories_signed_off_count);
assert_eq!(ProposalState::Voting, proposal_account.state);
assert_eq!(Some(1), proposal_account.signing_off_at);
assert_eq!(Some(1), proposal_account.voting_at);
let signatory_record_account = governance_test
.get_signatory_record_account(&signatory_record_cookie.address)
.await;
assert_eq!(true, signatory_record_account.signed_off);
}

View File

@ -1,170 +0,0 @@
#![cfg(feature = "test-bpf")]
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
use solana_program_test::*;
mod program_test;
use program_test::*;
use solana_sdk::signature::Signer;
use spl_governance::{
error::GovernanceError, instruction::withdraw_governing_tokens,
state::token_owner_record::get_token_owner_record_address,
};
#[tokio::test]
async fn test_withdraw_community_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.withdraw_community_tokens(&realm_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.community_token_holding_account)
.await;
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
token_owner_record_cookie.token_source_amount,
source_account.amount
);
}
#[tokio::test]
async fn test_withdraw_council_tokens() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_council_token_deposit(&realm_cookie)
.await;
// Act
governance_test
.withdraw_council_tokens(&realm_cookie, &token_owner_record_cookie)
.await
.unwrap();
// Assert
let token_owner_record = governance_test
.get_token_owner_record_account(&token_owner_record_cookie.address)
.await;
assert_eq!(0, token_owner_record.governing_token_deposit_amount);
let holding_account = governance_test
.get_token_account(&realm_cookie.council_token_holding_account.unwrap())
.await;
assert_eq!(0, holding_account.amount);
let source_account = governance_test
.get_token_account(&token_owner_record_cookie.token_source)
.await;
assert_eq!(
token_owner_record_cookie.token_source_amount,
source_account.amount
);
}
#[tokio::test]
async fn test_withdraw_community_tokens_with_owner_must_sign_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let hacker_token_destination = Pubkey::new_unique();
let mut instruction = withdraw_governing_tokens(
&realm_cookie.address,
&hacker_token_destination,
&token_owner_record_cookie.token_owner.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[3] =
AccountMeta::new_readonly(token_owner_record_cookie.token_owner.pubkey(), false);
// Act
let err = governance_test
.process_transaction(&[instruction], None)
.await
.err()
.unwrap();
// Assert
assert_eq!(err, GovernanceError::GoverningTokenOwnerMustSign.into());
}
#[tokio::test]
async fn test_withdraw_community_tokens_with_token_owner_record_address_mismatch_error() {
// Arrange
let mut governance_test = GovernanceProgramTest::start_new().await;
let realm_cookie = governance_test.with_realm().await;
let token_owner_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let vote_record_address = get_token_owner_record_address(
&realm_cookie.address,
&realm_cookie.account.community_mint,
&token_owner_record_cookie.token_owner.pubkey(),
);
let hacker_record_cookie = governance_test
.with_initial_community_token_deposit(&realm_cookie)
.await;
let mut instruction = withdraw_governing_tokens(
&realm_cookie.address,
&hacker_record_cookie.token_source,
&hacker_record_cookie.token_owner.pubkey(),
&realm_cookie.account.community_mint,
);
instruction.accounts[4] = AccountMeta::new(vote_record_address, false);
// Act
let err = governance_test
.process_transaction(&[instruction], Some(&[&hacker_record_cookie.token_owner]))
.await
.err()
.unwrap();
// Assert
assert_eq!(
err,
GovernanceError::InvalidTokenOwnerRecordAccountAddress.into()
);
}

View File

@ -1,90 +0,0 @@
use solana_program::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use spl_governance::state::{
governance::Governance, realm::Realm, token_owner_record::TokenOwnerRecord,
};
use spl_governance::state::{proposal::Proposal, signatory_record::SignatoryRecord};
use super::tools::clone_keypair;
#[derive(Debug)]
pub struct RealmCookie {
pub address: Pubkey,
pub account: Realm,
pub community_mint_authority: Keypair,
pub community_token_holding_account: Pubkey,
pub council_mint_authority: Option<Keypair>,
pub council_token_holding_account: Option<Pubkey>,
}
#[derive(Debug)]
pub struct TokeOwnerRecordCookie {
pub address: Pubkey,
pub account: TokenOwnerRecord,
pub token_source: Pubkey,
pub token_source_amount: u64,
pub token_owner: Keypair,
pub governance_authority: Option<Keypair>,
pub governance_delegate: Keypair,
pub governing_token_mint: Pubkey,
}
impl TokeOwnerRecordCookie {
pub fn get_governance_authority(&self) -> &Keypair {
self.governance_authority
.as_ref()
.unwrap_or(&self.token_owner)
}
#[allow(dead_code)]
pub fn clone_governance_delegate(&self) -> Keypair {
clone_keypair(&self.governance_delegate)
}
}
#[derive(Debug)]
pub struct GovernedProgramCookie {
pub address: Pubkey,
pub upgrade_authority: Keypair,
pub data_address: Pubkey,
pub transfer_upgrade_authority: bool,
}
#[derive(Debug)]
pub struct GovernedAccountCookie {
pub address: Pubkey,
}
#[derive(Debug)]
pub struct GovernanceCookie {
pub address: Pubkey,
pub account: Governance,
pub next_proposal_index: u16,
}
#[derive(Debug)]
pub struct ProposalCookie {
pub address: Pubkey,
pub account: Proposal,
pub proposal_owner: Pubkey,
}
#[derive(Debug)]
pub struct SignatoryRecordCookie {
pub address: Pubkey,
pub account: SignatoryRecord,
pub signatory: Keypair,
}

File diff suppressed because it is too large Load Diff

View File

@ -1,45 +0,0 @@
use std::convert::TryFrom;
use solana_program::{instruction::InstructionError, program_error::ProgramError};
use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError};
/// TODO: Add to SDK
/// Instruction errors not mapped in the sdk
pub enum ProgramInstructionError {
/// Incorrect authority provided
IncorrectAuthority,
}
impl From<ProgramInstructionError> for ProgramError {
fn from(e: ProgramInstructionError) -> Self {
ProgramError::Custom(e as u32)
}
}
pub fn map_transaction_error(transport_error: TransportError) -> ProgramError {
match transport_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => ProgramError::Custom(error_index),
TransportError::TransactionError(TransactionError::InstructionError(
_,
instruction_error,
)) => ProgramError::try_from(instruction_error).unwrap_or_else(|ie| match ie {
InstructionError::IncorrectAuthority => {
ProgramInstructionError::IncorrectAuthority.into()
}
_ => panic!("TEST-INSTRUCTION-ERROR {:?}", ie),
}),
_ => panic!("TEST-TRANSPORT-ERROR: {:?}", transport_error),
}
}
pub fn clone_keypair(source: &Keypair) -> Keypair {
Keypair::from_bytes(&source.to_bytes()).unwrap()
}
/// NOP (No Operation) Override function
#[allow(non_snake_case)]
pub fn NopOverride<T>(_: &mut T) {}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

View File

@ -12,18 +12,18 @@ no-entrypoint = []
test-bpf = []
[dependencies]
borsh = "0.8"
borsh = "0.7.1"
borsh-derive = "0.8.1"
num-derive = "0.3"
num-traits = "0.2"
solana-program = "1.6.7"
solana-program = "1.6.2"
thiserror = "1.0"
uint = "0.8"
[dev-dependencies]
proptest = "0.10"
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -1,42 +1,35 @@
//! Approximation calculations
use {
num_traits::{CheckedShl, CheckedShr, PrimInt},
std::cmp::Ordering,
num_traits::{CheckedAdd, CheckedDiv, One, Zero},
std::cmp::Eq,
};
/// 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
_ => {}
const SQRT_ITERATIONS: u8 = 50;
/// 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());
}
// 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)?;
// 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;
} else {
result = result.checked_shr(1)?;
last_guess = guess;
}
bit = bit.checked_shr(2)?;
}
Some(result)
Some(guess)
}
#[cfg(test)]

View File

@ -43,16 +43,7 @@ pub enum MathInstruction {
/// The multipier
multiplier: u64,
},
/// Divide two u64 values
///
/// No accounts required for this instruction
U64Divide {
/// The dividend
dividend: u64,
/// The divisor
divisor: u64,
},
/// Multiply two float values
/// Multiply two float valies
///
/// No accounts required for this instruction
F32Multiply {
@ -61,7 +52,7 @@ pub enum MathInstruction {
/// The multipier
multiplier: f32,
},
/// Divide two float values
/// Divide two float valies
///
/// No accounts required for this instruction
F32Divide {
@ -123,17 +114,6 @@ 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 {

View File

@ -3,36 +3,9 @@
use {
crate::{approximations::sqrt, instruction::MathInstruction, precise_number::PreciseNumber},
borsh::BorshDeserialize,
solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, log::sol_log_compute_units, msg,
pubkey::Pubkey,
},
solana_program::{account_info::AccountInfo, entrypoint::ProgramResult, 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,
@ -44,25 +17,19 @@ 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(())
}
@ -71,17 +38,7 @@ pub fn process_instruction(
multiplier,
} => {
msg!("Calculating U64 Multiply");
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();
let result = multiplicand * multiplier;
msg!("{}", result);
Ok(())
}
@ -90,17 +47,13 @@ pub fn process_instruction(
multiplier,
} => {
msg!("Calculating f32 Multiply");
sol_log_compute_units();
let result = f32_multiply(multiplicand, multiplier);
sol_log_compute_units();
let result = multiplicand * multiplier;
msg!("{}", result as u64);
Ok(())
}
MathInstruction::F32Divide { dividend, divisor } => {
msg!("Calculating f32 Divide");
sol_log_compute_units();
let result = f32_divide(dividend, divisor);
sol_log_compute_units();
let result = dividend / divisor;
msg!("{}", result as u64);
Ok(())
}

View File

@ -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(4_000);
pc.set_bpf_compute_max_units(5_500);
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
@ -78,7 +78,8 @@ async fn test_sqrt_u128() {
async fn test_sqrt_u128_max() {
let mut pc = ProgramTest::new("spl_math", id(), processor!(process_instruction));
pc.set_bpf_compute_max_units(6_000);
// This is pretty big too!
pc.set_bpf_compute_max_units(90_000);
let (mut banks_client, payer, recent_blockhash) = pc.start().await;
@ -102,20 +103,6 @@ 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));

View File

@ -1,6 +1,6 @@
[package]
name = "spl-memo"
version = "3.0.1"
version = "3.0.0"
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.7"
solana-program = "1.6.2"
[dev-dependencies]
solana-program-test = "1.6.7"
solana-sdk = "1.6.7"
solana-program-test = "1.6.2"
solana-sdk = "1.6.2"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -117,7 +117,6 @@ 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;

View File

@ -1,21 +0,0 @@
# 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

View File

@ -1,11 +0,0 @@
# 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.

View File

@ -1,33 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": { "project": "./tsconfig.json" },
"env": { "es6": true },
"ignorePatterns": ["node_modules", "build", "coverage"],
"plugins": ["import", "eslint-comments"],
"extends": [
"eslint:recommended",
"plugin:eslint-comments/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/typescript",
"prettier",
"prettier/@typescript-eslint"
],
"globals": { "BigInt": true, "console": true, "WebAssembly": true },
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"eslint-comments/disable-enable-pair": [
"error",
{ "allowWholeFile": true }
],
"eslint-comments/no-unused-disable": "error",
"import/order": [
"error",
{ "newlines-between": "always", "alphabetize": { "order": "asc" } }
],
"sort-imports": [
"error",
{ "ignoreDeclarationSort": true, "ignoreCase": true }
]
}
}

View File

@ -1,22 +0,0 @@
# Name Service JavaScript bindings
[![npm](https://img.shields.io/npm/v/@solana/spl-name-service)](https://unpkg.com/@solana/spl-name-service@latest/) [![GitHub license](https://img.shields.io/badge/license-APACHE-blue.svg)](https://github.com/solana-labs/token-list/blob/b3fa86b3fdd9c817139e38641d46c5a892542a52/LICENSE)
Full documentation is available at https://spl.solana.com/name-service
JavaScript binding allow to interact with a spl program for issuing and managing
ownership of: domain names, Solana Pubkeys, URLs, twitter handles, arweave ids,
metadata, etc..
This package provides an interface that third parties can
utilize to create and use their own version of a name service of any kind.
## Installation
```bash
npm install @solana/spl-name-service
```
```bash
yarn add @solana/spl-name-service
```

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +0,0 @@
{
"name": "@solana/spl-name-service",
"version": "0.1.2",
"description": "SPL Name Service JavaScript API",
"license": "MIT",
"author": "Solana Maintainers <maintainers@solana.foundation>",
"homepage": "https://solana.com/",
"repository": {
"type": "git",
"url": "https://github.com/solana-labs/solana-program-library"
},
"bugs": {
"url": "https://github.com/solana-labs/solana-program-library/issues"
},
"publishConfig": {
"access": "public"
},
"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"
},
"prettier": {
"singleQuote": true
},
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/bs58": "^4.0.1",
"@types/node": "^14.14.20",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.8.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-functional": "^3.0.2",
"eslint-plugin-import": "^2.22.0",
"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": {
"@solana/spl-token": "0.1.4",
"@solana/web3.js": "^1.11.0",
"bip32": "^2.0.6",
"bn.js": "^5.1.3",
"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"
}
}

View File

@ -1,220 +0,0 @@
import {
Connection,
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import {
createInstruction,
deleteInstruction,
transferInstruction,
updateInstruction,
} from './instructions';
import { NameRegistryState } from './state';
import { Numberu64 } from './utils';
import {
getHashedName,
getNameAccountKey,
getNameOwner,
Numberu32,
} from './utils';
////////////////////////////////////////////////////////////
export const NAME_PROGRAM_ID = new PublicKey(
'namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX'
);
export const HASH_PREFIX = 'SPL Name Service';
export const VERIFICATION_AUTHORITY_OFFSET = 64;
////////////////////////////////////////////////////////////
/**
* 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> {
const hashed_name = await getHashedName(name);
const nameAccountKey = await getNameAccountKey(
hashed_name,
nameClass,
parentName
);
space += 96; // Accounting for the Registry State Header
const balance = lamports
? lamports
: await connection.getMinimumBalanceForRentExemption(space);
let nameParentOwner: PublicKey | undefined;
if (parentName) {
const parentAccount = await getNameOwner(connection, parentName);
nameParentOwner = parentAccount.owner;
}
const 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> {
const hashed_name = await getHashedName(name);
const nameAccountKey = await getNameAccountKey(
hashed_name,
nameClass,
nameParent
);
let signer: PublicKey;
if (nameClass) {
signer = nameClass;
} else {
signer = (await NameRegistryState.retrieve(connection, nameAccountKey))
.owner;
}
const updateInstr = updateInstruction(
NAME_PROGRAM_ID,
nameAccountKey,
new Numberu32(offset),
input_data,
signer
);
return updateInstr;
}
/**
* Change 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,
nameClass?: PublicKey,
nameParent?: PublicKey
): Promise<TransactionInstruction> {
const hashed_name = await getHashedName(name);
const nameAccountKey = await getNameAccountKey(
hashed_name,
nameClass,
nameParent
);
let curentNameOwner: PublicKey;
if (nameClass) {
curentNameOwner = nameClass;
} else {
curentNameOwner = (
await NameRegistryState.retrieve(connection, nameAccountKey)
).owner;
}
const 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> {
const hashed_name = await getHashedName(name);
const nameAccountKey = await getNameAccountKey(
hashed_name,
nameClass,
nameParent
);
let nameOwner: PublicKey;
if (nameClass) {
nameOwner = nameClass;
} else {
nameOwner = (await NameRegistryState.retrieve(connection, nameAccountKey))
.owner;
}
const changeAuthoritiesInstr = deleteInstruction(
NAME_PROGRAM_ID,
nameAccountKey,
refundTargetKey,
nameOwner
);
return changeAuthoritiesInstr;
}

View File

@ -1,5 +0,0 @@
export * from './bindings';
export * from './instructions';
export * from './state';
export * from './utils';
export * from './twitter';

View File

@ -1,198 +0,0 @@
import { PublicKey, TransactionInstruction } from '@solana/web3.js';
import { Numberu32, Numberu64 } 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 {
const buffers = [
Buffer.from(Int8Array.from([0])),
new Numberu32(hashed_name.length).toBuffer(),
hashed_name,
lamports.toBuffer(),
space.toBuffer(),
];
const data = Buffer.concat(buffers);
const 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 {
const buffers = [
Buffer.from(Int8Array.from([1])),
offset.toBuffer(),
new Numberu32(input_data.length).toBuffer(),
input_data,
];
const data = Buffer.concat(buffers);
const 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 {
const buffers = [Buffer.from(Int8Array.from([2])), newOwnerKey.toBuffer()];
const data = Buffer.concat(buffers);
const 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 {
const buffers = [Buffer.from(Int8Array.from([3]))];
const data = Buffer.concat(buffers);
const 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,
});
}

View File

@ -1,47 +0,0 @@
import { Connection, PublicKey } from '@solana/web3.js';
export class NameRegistryState {
parentName: PublicKey;
owner: PublicKey;
class: PublicKey;
data: Buffer;
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 deserialize(buffer: Buffer): NameRegistryState {
return new NameRegistryState({
parentName: buffer.slice(0, 32),
owner: buffer.slice(32, 64),
class: buffer.slice(64, 96),
data: buffer.slice(96, buffer.length),
});
}
static async retrieve(
connection: Connection,
nameAccountKey: PublicKey
): Promise<NameRegistryState> {
const nameAccount = await connection.getAccountInfo(
nameAccountKey,
'processed'
);
if (!nameAccount) {
throw new Error('Invalid name account provided');
}
const res: NameRegistryState = NameRegistryState.deserialize(
nameAccount.data
);
return res;
}
}

View File

@ -1,104 +0,0 @@
import { readFile } from 'fs/promises';
import { AccountInfo, Connection, Keypair, PublicKey } from '@solana/web3.js';
import { serialize } from 'borsh';
import { sign } from 'tweetnacl';
import {
createNameRegistry,
deleteNameRegistry,
transferNameOwnership,
updateNameRegistryData,
} from './bindings';
import { NameRegistryState } from './state';
import {
getHashedName,
getNameAccountKey,
Numberu32,
Numberu64,
signAndSendTransactionInstructions,
} from './utils';
const ENDPOINT = 'https://devnet.solana.com/';
// const ENDPOINT = 'https://solana-api.projectserum.com/';
export async function test() {
const connection = new Connection(ENDPOINT);
// let secretKey = JSON.parse(
// (await readFile('/home/lcchy-work/.config/solana/id_devnet.json')).toString()
// );
// let adminAccount = new Keypair(secretKey);
const 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]
// )
// );
const hashed_root_name = await getHashedName(root_name);
const nameAccountKey = await getNameAccountKey(hashed_root_name);
console.log(await NameRegistryState.retrieve(connection, nameAccountKey));
}
test();

View File

@ -1,343 +0,0 @@
import {
Connection,
PublicKey,
SystemProgram,
TransactionInstruction,
} from '@solana/web3.js';
import { NAME_PROGRAM_ID, VERIFICATION_AUTHORITY_OFFSET } from './bindings';
import {
createInstruction,
deleteInstruction,
transferInstruction,
updateInstruction,
} from './instructions';
import { NameRegistryState } from './state';
import {
getFilteredProgramAccounts,
getHashedName,
getNameAccountKey,
Numberu32,
Numberu64,
} from './utils';
export const TWITTER_VERIFICATION_AUTHORITY = new PublicKey(
'867BLob5b52i81SNaV9Awm5ejkZV6VGSv9SxLcwukDDJ'
);
// The address of the name registry that will be a parent to all twitter handle registries,
// it should be owned by the TWITTER_VERIFICATION_AUTHORITY and it's name is irrelevant
export const TWITTER_ROOT_PARENT_REGISTRY_KEY = new PublicKey(
'AFrGkxNmVLBn3mKhvfJJABvm8RJkTtRhHDoaF97pQZaA'
);
// Signed by the authority and the payer
export async function createVerifiedTwitterRegistry(
connection: Connection,
twitterHandle: string,
verifiedPubkey: PublicKey,
space: number, // The space that the user will have to write data into the verified registry
payerKey: PublicKey
): Promise<TransactionInstruction[]> {
const hashedTwitterHandle = await getHashedName(twitterHandle);
const twitterHandleRegistryKey = await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
const hashedVerifiedPubkey = await getHashedName(
verifiedPubkey.toString().concat(twitterHandle)
);
const reverseRegistryKey = await getNameAccountKey(
hashedVerifiedPubkey,
TWITTER_VERIFICATION_AUTHORITY,
undefined
);
space += 96; // Accounting for the Registry State Header
const instructions = [
// Create user facing registry
createInstruction(
NAME_PROGRAM_ID,
SystemProgram.programId,
twitterHandleRegistryKey,
verifiedPubkey,
payerKey,
hashedTwitterHandle,
new Numberu64(await connection.getMinimumBalanceForRentExemption(space)),
new Numberu32(space),
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY,
TWITTER_VERIFICATION_AUTHORITY // Twitter authority acts as owner of the parent for all user-facing registries
),
// Create reverse lookup registry
createInstruction(
NAME_PROGRAM_ID,
SystemProgram.programId,
reverseRegistryKey,
verifiedPubkey,
payerKey,
hashedVerifiedPubkey,
new Numberu64(
await connection.getMinimumBalanceForRentExemption(96 + 18)
),
new Numberu32(96 + 18), // maximum length of a twitter handle
TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries
undefined,
undefined
),
// Write the twitter handle into the reverse lookup registry
updateInstruction(
NAME_PROGRAM_ID,
reverseRegistryKey,
new Numberu32(0),
Buffer.from(twitterHandle),
TWITTER_VERIFICATION_AUTHORITY
),
];
return instructions;
}
// Overwrite the data that is written in the user facing registry
// Signed by the verified pubkey
export async function changeTwitterRegistryData(
twitterHandle: string,
verifiedPubkey: PublicKey,
offset: number, // The offset at which to write the input data into the NameRegistryData
input_data: Buffer
): Promise<TransactionInstruction[]> {
const hashedTwitterHandle = await getHashedName(twitterHandle);
const twitterHandleRegistryKey = await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
const instructions = [
updateInstruction(
NAME_PROGRAM_ID,
twitterHandleRegistryKey,
new Numberu32(offset),
input_data,
verifiedPubkey
),
];
return instructions;
}
// Change the verified pubkey for a given twitter handle
// Signed by the Authority, the verified pubkey and the payer
export async function changeVerifiedPubkey(
connection: Connection,
twitterHandle: string,
currentVerifiedPubkey: PublicKey,
newVerifiedPubkey: PublicKey,
payerKey: PublicKey
): Promise<TransactionInstruction[]> {
const hashedTwitterHandle = await getHashedName(twitterHandle);
const twitterHandleRegistryKey = await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
const currentHashedVerifiedPubkey = await getHashedName(
currentVerifiedPubkey.toString().concat(twitterHandle)
);
const currentReverseRegistryKey = await getNameAccountKey(
currentHashedVerifiedPubkey,
TWITTER_VERIFICATION_AUTHORITY,
undefined
);
const newHashedVerifiedPubkey = await getHashedName(
newVerifiedPubkey.toString().concat(twitterHandle)
);
const newReverseRegistryKey = await getNameAccountKey(
newHashedVerifiedPubkey,
TWITTER_VERIFICATION_AUTHORITY,
undefined
);
const instructions = [
// Transfer the user-facing registry ownership
transferInstruction(
NAME_PROGRAM_ID,
twitterHandleRegistryKey,
newVerifiedPubkey,
currentVerifiedPubkey,
undefined
),
// Delete the current reverse registry
deleteInstruction(
NAME_PROGRAM_ID,
currentReverseRegistryKey,
payerKey,
currentVerifiedPubkey
),
// Create the new reverse lookup registry
createInstruction(
NAME_PROGRAM_ID,
SystemProgram.programId,
newReverseRegistryKey,
TWITTER_VERIFICATION_AUTHORITY,
payerKey,
newHashedVerifiedPubkey,
new Numberu64(await connection.getMinimumBalanceForRentExemption(18)),
new Numberu32(18), // maximum length of a twitter handle
TWITTER_VERIFICATION_AUTHORITY, // Twitter authority acts as class for all reverse-lookup registries
undefined,
undefined
),
// Write the twitter handle into the new reverse lookup registry
updateInstruction(
NAME_PROGRAM_ID,
newReverseRegistryKey,
new Numberu32(0),
Buffer.from(twitterHandle),
TWITTER_VERIFICATION_AUTHORITY
),
];
return instructions;
}
// Delete the verified registry for a given twitter handle
// Signed by the verified pubkey
export async function deleteTwitterRegistry(
twitterHandle: string,
verifiedPubkey: PublicKey
): Promise<TransactionInstruction[]> {
const hashedTwitterHandle = await getHashedName(twitterHandle);
const twitterHandleRegistryKey = await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
const hashedVerifiedPubkey = await getHashedName(
verifiedPubkey.toString().concat(twitterHandle)
);
const reverseRegistryKey = await getNameAccountKey(
hashedVerifiedPubkey,
TWITTER_VERIFICATION_AUTHORITY,
undefined
);
const instructions = [
// Delete the user facing registry
deleteInstruction(
NAME_PROGRAM_ID,
twitterHandleRegistryKey,
verifiedPubkey,
verifiedPubkey
),
// Delete the reverse registry
deleteInstruction(
NAME_PROGRAM_ID,
reverseRegistryKey,
verifiedPubkey,
verifiedPubkey
),
];
return instructions;
}
export async function getTwitterHandle(
connection: Connection,
verifiedPubkey: PublicKey
): Promise<string> {
const filters = [
{
memcmp: {
offset: 32,
bytes: verifiedPubkey.toBase58(),
},
},
{
memcmp: {
offset: VERIFICATION_AUTHORITY_OFFSET,
bytes: TWITTER_VERIFICATION_AUTHORITY.toBase58(),
},
},
];
const filteredAccounts = await getFilteredProgramAccounts(
connection,
NAME_PROGRAM_ID,
filters
);
for (const f of filteredAccounts) {
if (f.accountInfo.data.length == 114) {
return f.accountInfo.data.slice(96, 114).toString();
}
}
throw 'Could not find the twitter handle';
}
// Returns the key of the user-facing registry
export async function getTwitterRegistryKey(
twitter_handle: string
): Promise<PublicKey> {
const hashedTwitterHandle = await getHashedName(twitter_handle);
return await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
}
export async function getTwitterRegistry(
connection: Connection,
twitter_handle: string
): Promise<NameRegistryState> {
const hashedTwitterHandle = await getHashedName(twitter_handle);
const twitterHandleRegistryKey = await getNameAccountKey(
hashedTwitterHandle,
undefined,
TWITTER_ROOT_PARENT_REGISTRY_KEY
);
const registry = NameRegistryState.retrieve(
connection,
twitterHandleRegistryKey
);
return registry;
}
export async function getTwitterRegistryData(
connection: Connection,
verifiedPubkey: PublicKey
): Promise<Buffer> {
// Does not give you the name, but is faster than getTwitterHandle + getTwitterRegistry to get the data
const filters = [
{
memcmp: {
offset: 0,
bytes: TWITTER_ROOT_PARENT_REGISTRY_KEY.toBytes(),
},
},
{
memcmp: {
offset: 32,
bytes: verifiedPubkey.toBytes(),
},
},
];
const filteredAccounts = await getFilteredProgramAccounts(
connection,
NAME_PROGRAM_ID,
filters
);
if (filteredAccounts.length > 1) {
throw 'Found more than one twitter handle';
}
return filteredAccounts[0].accountInfo.data;
}

View File

@ -1,159 +0,0 @@
import assert from 'assert';
import { createHash } from 'crypto';
import {
AccountInfo,
Connection,
Keypair,
PublicKey,
Transaction,
TransactionInstruction,
} from '@solana/web3.js';
import BN from 'bn.js';
import { HASH_PREFIX, NAME_PROGRAM_ID } from './bindings';
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): BN {
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): BN {
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<Keypair>,
feePayer: Keypair,
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> {
const input = HASH_PREFIX + name;
const buffer = createHash('sha256').update(input, 'utf8').digest();
return buffer;
}
export async function getNameAccountKey(
hashed_name: Buffer,
nameClass?: PublicKey,
nameParent?: PublicKey
): Promise<PublicKey> {
const 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));
}
const [nameAccountKey] = await PublicKey.findProgramAddress(
seeds,
NAME_PROGRAM_ID
);
return nameAccountKey;
}
export async function getNameOwner(
connection: Connection,
nameAccountKey: PublicKey
): Promise<NameRegistryState> {
const nameAccount = await connection.getAccountInfo(nameAccountKey);
if (!nameAccount) {
throw 'Unable to find the given account.';
}
return NameRegistryState.retrieve(connection, nameAccountKey);
}
//Taken from Serum
export async function getFilteredProgramAccounts(
connection: Connection,
programId: PublicKey,
filters
): Promise<{ publicKey: PublicKey; accountInfo: AccountInfo<Buffer> }[]> {
const resp = await connection.getProgramAccounts(programId, {
commitment: connection.commitment,
filters,
encoding: 'base64',
});
return resp.map(
({ pubkey, account: { data, executable, owner, lamports } }) => ({
publicKey: pubkey,
accountInfo: {
data: data,
executable,
owner: owner,
lamports,
},
})
);
}

View File

@ -1,32 +0,0 @@
{
"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

View File

@ -1,29 +0,0 @@
[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"]

Some files were not shown because too many files have changed in this diff Show More