Compare commits
280 Commits
master
...
@solana/sp
Author | SHA1 | Date |
---|---|---|
|
02a6854f68 | |
|
135d97b1ed | |
|
83c84270f7 | |
|
f2643ebb1f | |
|
8136d9795a | |
|
44f4134ebd | |
|
09239bbf71 | |
|
db4792c4fe | |
|
c2f3ed530e | |
|
e28af24443 | |
|
fe6447e9a7 | |
|
ddf9efa330 | |
|
de8433e815 | |
|
e2d018526f | |
|
e25b687d2f | |
|
f519dc7549 | |
|
3cf9f0d0f9 | |
|
34af3726fd | |
|
e20cfbee44 | |
|
4ffc2f6e05 | |
|
e313f972bd | |
|
506a1b88d5 | |
|
b14c0f7447 | |
|
462a4834a6 | |
|
ddc8f952ba | |
|
52054b254b | |
|
98b0a37b8e | |
|
07b4a56752 | |
|
ed9fbf0f9d | |
|
c9e3689a94 | |
|
329f9c0912 | |
|
ba99e44bff | |
|
454c75c711 | |
|
8919174aab | |
|
75a96d8c01 | |
|
4e2d195944 | |
|
9d7dae8bb4 | |
|
95a3013d55 | |
|
ef9684db04 | |
|
09cf39ed98 | |
|
55b3c60105 | |
|
ed75b8abf6 | |
|
b0a43c2bab | |
|
b53bcdacc4 | |
|
a2e5ce0e0f | |
|
577291b1a5 | |
|
58e78ca85c | |
|
6dada44f75 | |
|
f5fb183b2d | |
|
9d5ee88707 | |
|
5c2e4e8ca5 | |
|
450b2d511a | |
|
e5c0d64c50 | |
|
cb7f81cd69 | |
|
eb5354d129 | |
|
3e5bb7cada | |
|
921348709a | |
|
8369803041 | |
|
051e201cae | |
|
f59e43757b | |
|
b8c2968cd1 | |
|
a3dbf08998 | |
|
0c83fdae66 | |
|
7c605652a2 | |
|
b2bf8a0753 | |
|
4c97074feb | |
|
25a76217a7 | |
|
82578a22f4 | |
|
4adfcaafdf | |
|
b310c2a163 | |
|
1dcab7b605 | |
|
7eb621d769 | |
|
7df63a831d | |
|
75e772792c | |
|
2621217f2e | |
|
3af3414481 | |
|
a421eb7029 | |
|
35069f6ffb | |
|
da2c3d00ae | |
|
9df31b10c0 | |
|
42ad8b2ad4 | |
|
57cf2c20fa | |
|
09b8be5071 | |
|
1188d906ea | |
|
94350d0e8e | |
|
ecc25f7d26 | |
|
c3657e5568 | |
|
813aa33040 | |
|
929dd59e50 | |
|
5f692a0b61 | |
|
6b3fbb8ff5 | |
|
c548e32119 | |
|
d31bec8339 | |
|
3fe5291570 | |
|
9837c62a42 | |
|
44c12ff8ab | |
|
5b6439fca8 | |
|
b0591808ec | |
|
9a857654c7 | |
|
73c3ea2bd5 | |
|
7992d85530 | |
|
8a9202c204 | |
|
40dec6e1fe | |
|
696abe1844 | |
|
98aa6000dc | |
|
c6cd23fa2e | |
|
3377a0d39d | |
|
50665fd549 | |
|
62c59b0453 | |
|
69ec863802 | |
|
7b150f841a | |
|
bf0fd7aaa0 | |
|
b488aee067 | |
|
ed0389f708 | |
|
d4dd97b3b1 | |
|
ea98e6bf73 | |
|
addc6bf4b4 | |
|
79f31e320f | |
|
4915c0910a | |
|
3426b714aa | |
|
06630018c2 | |
|
26560daae2 | |
|
8555f2d222 | |
|
b1629a18f6 | |
|
cef8b7363f | |
|
5afe5c7175 | |
|
0eadd43890 | |
|
e8c97e3cb2 | |
|
d8c062e522 | |
|
e5af52d6e7 | |
|
1beeb9fd21 | |
|
9da22609af | |
|
9342fa39f3 | |
|
df466175ae | |
|
6b82600d05 | |
|
0f9859410f | |
|
c71671cf2a | |
|
a1071cd8c6 | |
|
7b897cf0de | |
|
dc730085de | |
|
5e58890397 | |
|
6903f32221 | |
|
893ab9f2f5 | |
|
f2d4ad4500 | |
|
ba389f9581 | |
|
2625fb006b | |
|
ee4a135e20 | |
|
c0af8bdd0a | |
|
7fe1abeb6b | |
|
59c6bb7072 | |
|
561053730e | |
|
56572e32cb | |
|
2492734068 | |
|
0f82fa7d9c | |
|
1a6df09e89 | |
|
58221fc9ae | |
|
d4bbdf8224 | |
|
41568014eb | |
|
731bb6f57a | |
|
cebb3176b2 | |
|
f3a8fae2f5 | |
|
3709ac60db | |
|
5a9eebd692 | |
|
d3d527e26b | |
|
88eb28625b | |
|
e7360196ae | |
|
0007114121 | |
|
848c580889 | |
|
7f3ccec305 | |
|
1f48b5bc5e | |
|
8a02ab5650 | |
|
7336a7641e | |
|
e402aed8dd | |
|
96100f751b | |
|
6ac82f9caf | |
|
25b51bc2a0 | |
|
f0ad7b3d5f | |
|
2522d937d2 | |
|
d68608c440 | |
|
c6a9446b17 | |
|
0ac20fcddf | |
|
9ca1c6e8f2 | |
|
5a5f129694 | |
|
13689ac2dd | |
|
18d96ffce6 | |
|
f0b1cec4d8 | |
|
cfc6c582ff | |
|
090ecefa46 | |
|
9482f8d4e3 | |
|
1526301d0d | |
|
3045d7b1df | |
|
ea4b7e62fc | |
|
51cc3ed6fe | |
|
9aa238e0b4 | |
|
00eb817073 | |
|
4e1892adba | |
|
3714c70d12 | |
|
a05fd7185d | |
|
cf2fcde121 | |
|
3d04aa0109 | |
|
7320cf404b | |
|
975b524487 | |
|
a539ef021f | |
|
1efc90c5c9 | |
|
0ce5b41b9f | |
|
1f36ca301b | |
|
4068b77f53 | |
|
512497aa39 | |
|
d9fd11a8f3 | |
|
9e4764faf7 | |
|
0ed080a50f | |
|
3dd6767297 | |
|
b99c9b375c | |
|
9c63bc0b06 | |
|
5a357a50df | |
|
0f4f2b8de9 | |
|
60ef11e26b | |
|
5e320ba976 | |
|
636407d7a9 | |
|
11e207cc85 | |
|
14bdbdc3ac | |
|
7d666b86ce | |
|
81ab529311 | |
|
c9a5289aa0 | |
|
ca6d57991a | |
|
99aaab0993 | |
|
1e28a427a4 | |
|
61a53abf6f | |
|
1e47030549 | |
|
9ad4168253 | |
|
dfc5cc5a23 | |
|
c149b0a46e | |
|
804a61e558 | |
|
3613ffe3b0 | |
|
08c4cb530a | |
|
fdba05714d | |
|
53c86493e6 | |
|
40ebfc6917 | |
|
30671aa5b3 | |
|
cf8eeb0720 | |
|
df994bf426 | |
|
6fee08be2f | |
|
a5c4b1e071 | |
|
589da55e29 | |
|
ebc16782bb | |
|
18468b513f | |
|
7a0e5aa14e | |
|
9dd807c893 | |
|
71e5e556c4 | |
|
d3e26d089b | |
|
48a0f81ab6 | |
|
9281b6e828 | |
|
96901b1299 | |
|
b8a773fddd | |
|
4374d8dee4 | |
|
6a52ba7d92 | |
|
24ea32aa48 | |
|
1ff9f789e7 | |
|
529b070e2b | |
|
13f8430b77 | |
|
b581ab3319 | |
|
31d5640706 | |
|
34571b624f | |
|
2b3f71ead5 | |
|
2ef336fd0a | |
|
78ab468781 | |
|
0d5146e30c | |
|
34583bf748 | |
|
fa5c34dc84 | |
|
4d1bb013fb | |
|
3ab19ba514 | |
|
e8f59e42ba | |
|
7f89183c0d | |
|
17dd53d5e8 | |
|
8f325dcd2d | |
|
0e2b08066b | |
|
c01665832a | |
|
ee52f1d499 | |
|
092432f1e1 | |
|
f309df4f35 |
|
@ -0,0 +1,59 @@
|
||||||
|
name: Binary Oracle Pair Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'binary-oracle-pair/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'binary-oracle-pair/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh binary-oracle-pair
|
|
@ -10,7 +10,34 @@ on:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
all_github_action_checks:
|
check_non_docs:
|
||||||
|
outputs:
|
||||||
|
run_all_github_action_checks: ${{ steps.check_files.outputs.run_all_github_action_checks }}
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- run: echo "Done"
|
- run: echo "Done"
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Examples Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'examples/rust/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'examples/rust/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh examples/rust
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Feature Proposal Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'feature-proposal/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'feature-proposal/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh feature-proposal
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Governance Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'governance/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'governance/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh governance
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Libraries Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'libraries/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'libraries/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh libraries
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Memo Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'memo/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'memo/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh memo
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Name Service Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'name-service/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'name-service/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh name-service
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Record Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'record/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'record/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh record
|
|
@ -0,0 +1,57 @@
|
||||||
|
name: Shared Memory Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'shared-memory/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'shared-memory/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh shared-memory
|
|
@ -0,0 +1,59 @@
|
||||||
|
name: Stake Pool Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'stake-pool/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'stake-pool/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh stake-pool
|
|
@ -0,0 +1,90 @@
|
||||||
|
name: Token Lending Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'token-lending/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'token-lending/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh token-lending
|
||||||
|
|
||||||
|
- name: Upload programs
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-lending-programs
|
||||||
|
path: "target/deploy/*.so"
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
js-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 12.x
|
||||||
|
needs: cargo-test-bpf
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: node-${{ hashFiles('token-lending/js/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-
|
||||||
|
- name: Download programs
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-lending-programs
|
||||||
|
path: target/deploy
|
||||||
|
- run: ./ci/js-test-token-lending.sh
|
|
@ -0,0 +1,154 @@
|
||||||
|
name: Token Swap Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'token-swap/**'
|
||||||
|
- 'token/**'
|
||||||
|
- 'libraries/math/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'token-swap/**'
|
||||||
|
- 'token/**'
|
||||||
|
- 'libraries/math/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh token-swap
|
||||||
|
|
||||||
|
- name: Build production version
|
||||||
|
run: |
|
||||||
|
cargo +"$RUST_STABLE" build-bpf \
|
||||||
|
--manifest-path=token-swap/program/Cargo.toml \
|
||||||
|
--features production \
|
||||||
|
--bpf-out-dir target/deploy-production
|
||||||
|
env:
|
||||||
|
SWAP_PROGRAM_OWNER_FEE_ADDRESS: HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN
|
||||||
|
|
||||||
|
- name: Move production version for upload
|
||||||
|
run: |
|
||||||
|
mv target/deploy-production/spl_token_swap.so target/deploy/spl_token_swap_production.so
|
||||||
|
|
||||||
|
- name: Upload programs
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-swap-programs
|
||||||
|
path: "target/deploy/*.so"
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
js-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 12.x
|
||||||
|
needs: cargo-test-bpf
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: node-${{ hashFiles('token-swap/js/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-
|
||||||
|
- name: Download programs
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-swap-programs
|
||||||
|
path: target/deploy
|
||||||
|
- run: ./ci/js-test-token-swap.sh
|
||||||
|
|
||||||
|
fuzz:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: token-swap-fuzz-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/cargo-hfuzz
|
||||||
|
~/.cargo/bin/cargo-honggfuzz
|
||||||
|
key: cargo-fuzz-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
restore-keys: |
|
||||||
|
solana-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Run fuzz target
|
||||||
|
run: ./ci/fuzz.sh token-swap-instructions 30 # 30 seconds, just to check everything is ok
|
|
@ -0,0 +1,93 @@
|
||||||
|
name: Token Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- 'associated-token-account/**'
|
||||||
|
- 'token/**'
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- 'associated-token-account/**'
|
||||||
|
- 'token/**'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo-test-bpf:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set env vars
|
||||||
|
run: |
|
||||||
|
source ci/rust-version.sh
|
||||||
|
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
||||||
|
source ci/solana-version.sh
|
||||||
|
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ env.RUST_STABLE }}
|
||||||
|
override: true
|
||||||
|
profile: minimal
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
key: cargo-build-${{ hashFiles('**/Cargo.lock') }}-${{ env.RUST_STABLE}}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/bin/rustfilt
|
||||||
|
key: cargo-bpf-bins-${{ runner.os }}
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
key: solana-${{ env.SOLANA_VERSION }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
./ci/install-build-deps.sh
|
||||||
|
./ci/install-program-deps.sh
|
||||||
|
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh token
|
||||||
|
|
||||||
|
- name: Build and test
|
||||||
|
run: ./ci/cargo-test-bpf.sh associated-token-account
|
||||||
|
|
||||||
|
- name: Upload programs
|
||||||
|
uses: actions/upload-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-programs
|
||||||
|
path: "target/deploy/*.so"
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
js-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
NODE_VERSION: 12.x
|
||||||
|
needs: cargo-test-bpf
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Use Node.js ${{ env.NODE_VERSION }}
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: node-${{ hashFiles('token/js/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
node-
|
||||||
|
- name: Download programs
|
||||||
|
uses: actions/download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: token-programs
|
||||||
|
path: target/deploy
|
||||||
|
- run: ./ci/js-test-token.sh
|
|
@ -16,10 +16,6 @@ jobs:
|
||||||
- rustfmt
|
- rustfmt
|
||||||
- clippy
|
- clippy
|
||||||
- cargo-build-test
|
- cargo-build-test
|
||||||
- js-test-token
|
|
||||||
- js-test-token-swap
|
|
||||||
- js-test-token-lending
|
|
||||||
- fuzz
|
|
||||||
steps:
|
steps:
|
||||||
- run: echo "Done"
|
- run: echo "Done"
|
||||||
|
|
||||||
|
@ -128,135 +124,3 @@ jobs:
|
||||||
|
|
||||||
- name: Build and test
|
- name: Build and test
|
||||||
run: ./ci/cargo-build-test.sh
|
run: ./ci/cargo-build-test.sh
|
||||||
|
|
||||||
- name: Upload programs
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: programs
|
|
||||||
path: "target/deploy/*.so"
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
js-test-token:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
NODE_VERSION: 12.x
|
|
||||||
needs: cargo-build-test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: node-token-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-token-
|
|
||||||
- name: Download programs
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: programs
|
|
||||||
path: target/bpfel-unknown-unknown/release
|
|
||||||
- run: ./ci/js-test-token.sh
|
|
||||||
|
|
||||||
js-test-token-swap:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
NODE_VERSION: 12.x
|
|
||||||
needs: cargo-build-test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: node-token-swap-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-token-swap-
|
|
||||||
- name: Download programs
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: programs
|
|
||||||
path: target/deploy
|
|
||||||
- run: ./ci/js-test-token-swap.sh
|
|
||||||
|
|
||||||
js-test-token-lending:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
NODE_VERSION: 12.x
|
|
||||||
needs: cargo-build-test
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Use Node.js ${{ env.NODE_VERSION }}
|
|
||||||
uses: actions/setup-node@v1
|
|
||||||
with:
|
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: node-token-lending-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
node-token-lending-
|
|
||||||
- name: Download programs
|
|
||||||
uses: actions/download-artifact@v2
|
|
||||||
with:
|
|
||||||
name: programs
|
|
||||||
path: target/deploy
|
|
||||||
- run: ./ci/js-test-token-lending.sh
|
|
||||||
|
|
||||||
fuzz:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
fuzz_target: [token-swap-instructions]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Set env vars
|
|
||||||
run: |
|
|
||||||
source ci/rust-version.sh
|
|
||||||
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
|
|
||||||
source ci/solana-version.sh
|
|
||||||
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ env.RUST_STABLE }}
|
|
||||||
override: true
|
|
||||||
profile: minimal
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/registry
|
|
||||||
~/.cargo/git
|
|
||||||
target
|
|
||||||
key: cargo-fuzz-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cargo/bin/cargo-hfuzz
|
|
||||||
~/.cargo/bin/cargo-honggfuzz
|
|
||||||
key: cargo-fuzz-bins-${{ runner.os }}
|
|
||||||
|
|
||||||
- uses: actions/cache@v2
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache
|
|
||||||
key: solana-${{ env.SOLANA_VERSION }}
|
|
||||||
restore-keys: |
|
|
||||||
solana-
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
./ci/install-build-deps.sh
|
|
||||||
./ci/install-program-deps.sh
|
|
||||||
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
|
|
||||||
|
|
||||||
- name: Run fuzz target
|
|
||||||
run: ./ci/fuzz.sh ${{ matrix.fuzz_target }} 30 # 30 seconds, just to check everything is ok
|
|
||||||
|
|
|
@ -8,3 +8,5 @@ node_modules
|
||||||
hfuzz_target
|
hfuzz_target
|
||||||
hfuzz_workspace
|
hfuzz_workspace
|
||||||
**/*.so
|
**/*.so
|
||||||
|
**/.DS_Store
|
||||||
|
test-ledger
|
||||||
|
|
|
@ -4,6 +4,15 @@
|
||||||
#
|
#
|
||||||
# https://doc.mergify.io/
|
# https://doc.mergify.io/
|
||||||
pull_request_rules:
|
pull_request_rules:
|
||||||
|
- name: label changes from community
|
||||||
|
conditions:
|
||||||
|
- author≠@core-contributors
|
||||||
|
- author≠mergify[bot]
|
||||||
|
- author≠dependabot[bot]
|
||||||
|
actions:
|
||||||
|
label:
|
||||||
|
add:
|
||||||
|
- community
|
||||||
- name: automatic merge (squash) on CI success
|
- name: automatic merge (squash) on CI success
|
||||||
conditions:
|
conditions:
|
||||||
- check-success=Travis CI - Pull Request
|
- check-success=Travis CI - Pull Request
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -9,14 +9,15 @@ members = [
|
||||||
"examples/rust/transfer-lamports",
|
"examples/rust/transfer-lamports",
|
||||||
"feature-proposal/program",
|
"feature-proposal/program",
|
||||||
"feature-proposal/cli",
|
"feature-proposal/cli",
|
||||||
|
"governance/program",
|
||||||
"libraries/math",
|
"libraries/math",
|
||||||
"memo/program",
|
"memo/program",
|
||||||
|
"name-service/program",
|
||||||
"record/program",
|
"record/program",
|
||||||
"shared-memory/program",
|
"shared-memory/program",
|
||||||
"stake-pool/cli",
|
"stake-pool/cli",
|
||||||
"stake-pool/program",
|
"stake-pool/program",
|
||||||
"token-lending/program",
|
"token-lending/program",
|
||||||
"token-lending/client",
|
|
||||||
"token-swap/program",
|
"token-swap/program",
|
||||||
"token-swap/program/fuzz",
|
"token-swap/program/fuzz",
|
||||||
"token/cli",
|
"token/cli",
|
||||||
|
@ -29,3 +30,6 @@ exclude = [
|
||||||
"themis/program_ristretto",
|
"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
|
"token/perf-monitor", # TODO: Rework perf-monitor to use solana-program-test, avoiding the need to link directly with the BPF VM
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
split-debuginfo = "unpacked"
|
||||||
|
|
|
@ -12,12 +12,12 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -13,16 +13,16 @@ test-bpf = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] }
|
spl-token = { version = "3.1", path = "../../token/program", features = [ "no-entrypoint" ] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
uint = "0.8"
|
uint = "0.8"
|
||||||
arbitrary = { version = "0.4", features = ["derive"], optional = true }
|
arbitrary = { version = "0.4", features = ["derive"], optional = true }
|
||||||
borsh = "0.8.2"
|
borsh = "0.8.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -14,10 +14,6 @@ set -x
|
||||||
# Build all C examples
|
# Build all C examples
|
||||||
make -C examples/c
|
make -C examples/c
|
||||||
|
|
||||||
# Build/test all BPF programs
|
|
||||||
cargo +"$rust_stable" test-bpf -- --nocapture
|
|
||||||
rm -rf target/debug # Prevents running out of space on github action runners
|
|
||||||
|
|
||||||
# Build/test all host crates
|
# Build/test all host crates
|
||||||
cargo +"$rust_stable" build
|
cargo +"$rust_stable" build
|
||||||
cargo +"$rust_stable" test -- --nocapture
|
cargo +"$rust_stable" test -- --nocapture
|
||||||
|
@ -29,13 +25,6 @@ cargo +"$rust_stable" run --manifest-path=utils/test-client/Cargo.toml
|
||||||
# client_ristretto disabled because it requires RpcBanksService, which is no longer supported.
|
# client_ristretto disabled because it requires RpcBanksService, which is no longer supported.
|
||||||
#cargo +"$rust_stable" test --manifest-path=themis/client_ristretto/Cargo.toml -- --nocapture
|
#cargo +"$rust_stable" test --manifest-path=themis/client_ristretto/Cargo.toml -- --nocapture
|
||||||
|
|
||||||
SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" \
|
|
||||||
cargo +"$rust_stable" build-bpf \
|
|
||||||
--manifest-path=token-swap/program/Cargo.toml \
|
|
||||||
--features production \
|
|
||||||
--bpf-out-dir target/deploy-production
|
|
||||||
mv target/deploy-production/spl_token_swap.so target/deploy/spl_token_swap_production.so
|
|
||||||
|
|
||||||
# # Check generated C headers
|
# # Check generated C headers
|
||||||
# cargo run --manifest-path=utils/cgen/Cargo.toml
|
# cargo run --manifest-path=utils/cgen/Cargo.toml
|
||||||
#
|
#
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
source ./ci/rust-version.sh stable
|
||||||
|
source ./ci/solana-version.sh
|
||||||
|
|
||||||
|
export RUSTFLAGS="-D warnings"
|
||||||
|
export RUSTBACKTRACE=1
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
exitcode=0
|
||||||
|
if [[ -n "$1" ]]; then
|
||||||
|
exitcode=1
|
||||||
|
echo "Error: $*"
|
||||||
|
fi
|
||||||
|
echo "Usage: $0 [program-directory]"
|
||||||
|
exit $exitcode
|
||||||
|
}
|
||||||
|
|
||||||
|
program_directory=$1
|
||||||
|
if [[ -z $program_directory ]]; then
|
||||||
|
usage "No program directory provided"
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
cd $program_directory
|
||||||
|
run_dir=$(pwd)
|
||||||
|
|
||||||
|
if [[ -d $run_dir/program ]]; then
|
||||||
|
# Build/test just one BPF program
|
||||||
|
cd $run_dir/program
|
||||||
|
cargo +"$rust_stable" test-bpf -- --nocapture
|
||||||
|
else
|
||||||
|
# Build/test all BPF programs
|
||||||
|
for directory in $(ls -d $run_dir/*/); do
|
||||||
|
cd $directory
|
||||||
|
cargo +"$rust_stable" test-bpf -- --nocapture
|
||||||
|
done
|
||||||
|
fi
|
|
@ -4,13 +4,10 @@ set -ex
|
||||||
cd "$(dirname "$0")/.."
|
cd "$(dirname "$0")/.."
|
||||||
source ./ci/solana-version.sh install
|
source ./ci/solana-version.sh install
|
||||||
|
|
||||||
(cd token/js && npm install)
|
|
||||||
|
|
||||||
cd token-swap/js
|
cd token-swap/js
|
||||||
npm install
|
npm install
|
||||||
npm run lint
|
npm run lint
|
||||||
npm run flow
|
npm run build
|
||||||
npx tsc module.d.ts
|
|
||||||
npm run start-with-test-validator
|
npm run start-with-test-validator
|
||||||
(cd ../../target/deploy && mv spl_token_swap_production.so spl_token_swap.so)
|
(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
|
SWAP_PROGRAM_OWNER_FEE_ADDRESS="HfoTxFR1Tm6kGmWgYWD6J7YHVy1UwqSULUGVLXkJqaKN" npm run start-with-test-validator
|
||||||
|
|
|
@ -18,13 +18,13 @@
|
||||||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||||
stable_version="$RUST_STABLE_VERSION"
|
stable_version="$RUST_STABLE_VERSION"
|
||||||
else
|
else
|
||||||
stable_version=1.50.0
|
stable_version=1.52.1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||||
else
|
else
|
||||||
nightly_version=2021-02-18
|
nightly_version=2021-04-18
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,10 @@
|
||||||
if [[ -n $SOLANA_VERSION ]]; then
|
if [[ -n $SOLANA_VERSION ]]; then
|
||||||
solana_version="$SOLANA_VERSION"
|
solana_version="$SOLANA_VERSION"
|
||||||
else
|
else
|
||||||
solana_version=v1.5.15
|
solana_version=v1.6.11
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export solana_version="$solana_version"
|
export solana_version="$solana_version"
|
||||||
export solana_docker_image=solanalabs/solana:"$solana_version"
|
|
||||||
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
|
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
|
||||||
|
|
||||||
if [[ -n $1 ]]; then
|
if [[ -n $1 ]]; then
|
||||||
|
|
|
@ -8,6 +8,7 @@ module.exports = {
|
||||||
"token-lending",
|
"token-lending",
|
||||||
"associated-token-account",
|
"associated-token-account",
|
||||||
"memo",
|
"memo",
|
||||||
|
"name-service",
|
||||||
"shared-memory",
|
"shared-memory",
|
||||||
"stake-pool",
|
"stake-pool",
|
||||||
"feature-proposal",
|
"feature-proposal",
|
||||||
|
|
|
@ -55,7 +55,7 @@ The [get_associated_token_address](https://docs.rs/spl-associated-token-account/
|
||||||
Rust function may be used by clients to derive the wallet's associated token address.
|
Rust function may be used by clients to derive the wallet's associated token address.
|
||||||
|
|
||||||
|
|
||||||
The associated account address can be derived in Javascript with:
|
The associated account address can be derived in TypeScript with:
|
||||||
```ts
|
```ts
|
||||||
import { PublicKey } from '@solana/web3.js';
|
import { PublicKey } from '@solana/web3.js';
|
||||||
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
|
||||||
|
|
|
@ -62,7 +62,7 @@ Logging ends with the status of the instruction, one of:
|
||||||
|
|
||||||
For more information about exposing program logs on a node, head to the
|
For more information about exposing program logs on a node, head to the
|
||||||
[developer
|
[developer
|
||||||
docs](https://docs.solana.com/developing/deployed-programs/debugging#logging)
|
docs](https://docs.solana.com/developing/on-chain-programs/debugging#logging)
|
||||||
|
|
||||||
### Compute Limits
|
### Compute Limits
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
---
|
||||||
|
title: Name Service
|
||||||
|
---
|
||||||
|
|
||||||
|
A SPL program for issuing and managing ownership of: domain names, Solana Pubkeys, URLs, Twitter handles, ipfs cid's etc..
|
||||||
|
|
||||||
|
This program could be used for dns, pubkey etc lookups via a browser extension
|
||||||
|
for example, the goal is to create an easy way to identify Solana public keys
|
||||||
|
with various links.
|
||||||
|
|
||||||
|
Broader use cases are also imaginable.
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
- A Name is a string that maps to a record (program derived account) which can hold data.
|
||||||
|
- Each name is of a certain class and has a certain owner, both are identified
|
||||||
|
by their pubkeys. The class of a name needs to sign the issuance of it.
|
||||||
|
- A name can have a parent name that is identified by the address of its record.
|
||||||
|
The owner of the parent name (when it exists) needs to sign the issuance of
|
||||||
|
the child name.
|
||||||
|
- The data of a name registry is controlled 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.
|
|
@ -3,7 +3,7 @@ title: Stake Pool Program
|
||||||
---
|
---
|
||||||
|
|
||||||
A program for pooling together SOL to be staked by an off-chain agent running
|
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.
|
to maximize censorship resistance and rewards.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
@ -14,7 +14,7 @@ inflation rate, total number of SOL staked on the network, and an individual
|
||||||
validator’s uptime and commission (fee).
|
validator’s uptime and commission (fee).
|
||||||
|
|
||||||
Stake pools are an alternative method of earning staking rewards. This on-chain
|
Stake pools are an alternative method of earning staking rewards. This on-chain
|
||||||
program pools together SOL to be staked by a manager, allowing SOL holders to
|
program pools together SOL to be staked by a staker, allowing SOL holders to
|
||||||
stake and earn rewards without managing stakes.
|
stake and earn rewards without managing stakes.
|
||||||
|
|
||||||
Additional information regarding staking and stake programming is available at:
|
Additional information regarding staking and stake programming is available at:
|
||||||
|
@ -24,16 +24,18 @@ Additional information regarding staking and stake programming is available at:
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
This document is intended for stake pool managers who want to create or manage
|
This document is intended for the main actors of the stake pool system:
|
||||||
stake pools, and users who want to provide staked SOL into an existing stake
|
|
||||||
pool.
|
* manager: creates and manages the stake pool, earns fees, can update the fee, staker, and manager
|
||||||
|
* staker: adds and removes validators to the pool, rebalances stake among validators
|
||||||
|
* user: provides staked SOL into an existing stake pool
|
||||||
|
|
||||||
In its current iteration, the stake pool only processes totally active stakes.
|
In its current iteration, the stake pool only processes totally active stakes.
|
||||||
Deposits must come from fully active stakes, and withdrawals return a fully
|
Deposits must come from fully active stakes, and withdrawals return a fully
|
||||||
active stake account.
|
active stake account.
|
||||||
|
|
||||||
This means that stake pool managers and users must be comfortable with creating
|
This means that stake pool managers, stakers, and users must be comfortable with
|
||||||
and delegating stakes, which are more advanced operations than sending and
|
creating and delegating stakes, which are more advanced operations than sending and
|
||||||
receiving SPL tokens and SOL. Additional information on stake operations are
|
receiving SPL tokens and SOL. Additional information on stake operations are
|
||||||
available at:
|
available at:
|
||||||
|
|
||||||
|
@ -46,27 +48,28 @@ like [Token Swap](token-swap.md).
|
||||||
|
|
||||||
## Operation
|
## Operation
|
||||||
|
|
||||||
A stake pool manager creates a stake pool and includes validators that will
|
A stake pool manager creates a stake pool, and the staker includes validators that will
|
||||||
receive delegations from the pool by creating "validator stake accounts" and
|
receive delegations from the pool by creating "validator stake accounts" and
|
||||||
activating a delegation on them. Once a validator stake account's delegation is
|
activating a delegation on them. Once a validator stake account's delegation is
|
||||||
active, the stake pool manager adds it to the stake pool.
|
active, the staker adds it to the stake pool.
|
||||||
|
|
||||||
At this point, users can participate with deposits. They must delegate a stake
|
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
|
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
|
user can deposit their stake into the pool in exchange for SPL staking derivatives
|
||||||
representing their fractional ownership in pool. A percentage of the user's
|
representing their fractional ownership in pool. A percentage of the rewards
|
||||||
deposit goes to the pool manager as a fee.
|
earned by the pool goes to the pool manager as a fee.
|
||||||
|
|
||||||
Over time, as the stake pool accrues staking rewards, the user's fractional
|
Over time, as the stakes in the stake pool accrue staking rewards, the user's fractional
|
||||||
ownership will be worth more than their initial deposit. Whenever the user chooses,
|
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.
|
they can withdraw their SPL staking derivatives in exchange for an activated stake.
|
||||||
|
|
||||||
The stake pool manager can add and remove validators, or rebalance the pool by
|
The stake pool staker can add and remove validators, or rebalance the pool by
|
||||||
withdrawing stakes from the pool, deactivating them, reactivating them on another
|
decreasing the stake on a validator, waiting an epoch to move it into the stake
|
||||||
validator, then depositing back into the pool.
|
pool's reserve account, then increasing the stake on another validator.
|
||||||
|
|
||||||
These manager operations require SPL staking derivatives and staked SOL, so the
|
The staker operation to add a new validator requires roughly 1.003 SOL to create
|
||||||
stake pool manager will need liquidity on hand to properly manage the pool.
|
the stake account on a validator, so the stake pool staker will need liquidity
|
||||||
|
on hand to fully manage the pool stakes.
|
||||||
|
|
||||||
## Background
|
## Background
|
||||||
|
|
||||||
|
@ -112,7 +115,7 @@ Keypair Path: ${HOME}/.config/solana/id.json
|
||||||
|
|
||||||
See [Solana clusters](https://docs.solana.com/clusters) for cluster-specific RPC URLs
|
See [Solana clusters](https://docs.solana.com/clusters) for cluster-specific RPC URLs
|
||||||
```sh
|
```sh
|
||||||
solana config set --url https://devnet.solana.com
|
solana config set --url https://api.devnet.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Default Keypair
|
#### Default Keypair
|
||||||
|
@ -130,32 +133,105 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
||||||
solana config set --keypair usb://ledger/
|
solana config set --keypair usb://ledger/
|
||||||
```
|
```
|
||||||
|
|
||||||
### Stake Pool Administrator Examples
|
#### Run Locally
|
||||||
|
|
||||||
|
If you would like to test a stake pool locally without having to wait for stakes
|
||||||
|
to activate and deactivate, you can run the stake pool locally using the
|
||||||
|
`solana-test-validator` tool with shorter epochs, and pulling the current program
|
||||||
|
from devnet, testnet, or mainnet.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ solana-test-validator -c poo1B9L9nR3CrcaziKVYVpRX6A9Y1LAXYasjjfCbApj --url devnet --slots-per-epoch 32
|
||||||
|
$ solana config set --url http://127.0.0.1:8899
|
||||||
|
```
|
||||||
|
|
||||||
|
### Stake Pool Manager Examples
|
||||||
|
|
||||||
#### Create a stake pool
|
#### Create a stake pool
|
||||||
|
|
||||||
The pool administrator manages the stake accounts in a stake pool, and in exchange
|
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 administrator
|
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:
|
sets the fee on creation. Let's create a pool with a 3% fee and a maximum of 1000
|
||||||
|
validator stake accounts:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100
|
$ spl-stake-pool create-pool --fee-numerator 3 --fee-denominator 100 --max-validators 1000
|
||||||
Creating mint Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
Creating reserve stake 33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J
|
||||||
Creating pool fee collection account 3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5
|
Creating mint D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ
|
||||||
Creating stake pool 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
Creating pool fee collection account 5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE
|
||||||
Signature: 5HdDoPssqwyLjt2QvhRbnSATZqFLGKha92zMuJiBUpKeKYKGURRV41N5ydCQxqnFjCud3xv85Z6ghErppNJzaYM8
|
Signature: 2dvCtHMcqxibckhvVgFQeFCRb7VcHbuFLRf71Aqd9PtzFzdbG3gAkNpxYznfpKDx2vTRrVtwW81sZAx5U3Frb5Uu
|
||||||
|
Creating stake pool EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
|
Signature: 2kYDVyJp8FVrLmEZyW9ivMYcXEsgWm4hFyhp5omxVtonjhYG6WS1S85sPTCdsQWe3idof6ZqsY8F3oaMXwrEkAYK
|
||||||
```
|
```
|
||||||
|
|
||||||
The unique stake pool identifier is `3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC`.
|
The unique stake pool identifier is `EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1`.
|
||||||
|
|
||||||
The identifier for the SPL token for staking derivatives is
|
The identifier for the SPL token for staking derivatives is
|
||||||
`Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS`. The stake pool has full control
|
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ`. The stake pool has full control
|
||||||
over the mint.
|
over the mint.
|
||||||
|
|
||||||
The pool creator's fee account identifier is
|
The pool creator's fee account identifier is
|
||||||
`3xvXPfQi2SaTkqPV9A7BQwh4GyTe2ZPasfoaCBCnTAJ5`. When users deposit warmed up
|
`5gpuSdutGY98KKbgmR5CfLK7toFcQD69JzKDwseegzXE`. Every epoch, as stake accounts
|
||||||
stake accounts into the stake pool, the program will transfer 3% of their
|
in the stake pool earn rewards, the program will mint SPL token staking derivatives
|
||||||
contribution into this account in the form of SPL token staking derivatives.
|
equal to 3% of the gains on that epoch into this account. If no gains were observed,
|
||||||
|
nothing will be deposited.
|
||||||
|
|
||||||
|
The reserve stake account identifier is `33Hg3bvYrAwfqCzTMjAWZNAWC6H96qJNEdzGamfFjG4J`.
|
||||||
|
This account holds onto additional stake used when rebalancing between validators.
|
||||||
|
|
||||||
|
For a stake pool with 1000 validators, the cost to create a stake pool is less
|
||||||
|
than 0.5 SOL.
|
||||||
|
|
||||||
|
#### Set manager
|
||||||
|
|
||||||
|
The stake pool manager may pass their administrator privileges to another account.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-manager 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
|
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||||
|
```
|
||||||
|
|
||||||
|
At the same time, they may also change the SPL token account that receives fees
|
||||||
|
every epoch. The mint for the provided token account must be the SPL token mint,
|
||||||
|
`D5yiK1tE1yAXBnrV9ZrSUJCw8WiQctZ8ekbv1U6ATVZ` in our example.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool set-manager EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --new-fee-receiver HoCsh97wRxRXVjtG7dyfsXSwH9VxdDzC7GvAsBE1eqJz
|
||||||
|
Signature: 4aK8yzYvPBkP4PyuXTcCm529kjEH6tTt4ixc5D5ZyCrHwc4pvxAHj6wcr4cpAE1e3LddE87J1GLD466aiifcXoAY
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set fee
|
||||||
|
|
||||||
|
The stake pool manager may update the fee assessed every epoch, passing the
|
||||||
|
numerator and denominator for the fraction that make up the fee. For a fee of
|
||||||
|
10%, they could run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool set-fee EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 10 100
|
||||||
|
Signature: 5yPXfVj5cbKBfZiEVi2UR5bXzVDuc2c3ruBwSjkAqpvxPHigwGHiS1mXQVE4qwok5moMWT5RNYAMvkE9bnfQ1i93
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Set staker
|
||||||
|
|
||||||
|
In order to manage the stake accounts, the stake pool manager or
|
||||||
|
staker can set the staker authority of the stake pool's managed accounts.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool set-staker EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
|
Signature: 39N5gkaqXuWm6JPEUWfenKXeG4nSa71p7iHb9zurvdZcsWmbjdmSXwLVYfhAVHWucTY77sJ8SkUNpVpVAhe4eZ53
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, the new staker can perform any normal stake pool operations, including
|
||||||
|
adding and removing validators and rebalancing stake.
|
||||||
|
|
||||||
|
Important security note: the stake pool program only gives staking authority to
|
||||||
|
the pool staker and always retains withdraw authority. Therefore, a malicious
|
||||||
|
stake pool staker cannot steal funds from the stake pool.
|
||||||
|
|
||||||
|
Note: to avoid "disturbing the manager", the staker can also reassign their stake
|
||||||
|
authority.
|
||||||
|
|
||||||
|
### Stake Pool Staker Examples
|
||||||
|
|
||||||
#### Create a validator stake account
|
#### Create a validator stake account
|
||||||
|
|
||||||
|
@ -170,7 +246,7 @@ lists, we choose some validators at random and start with identity
|
||||||
delegated to that vote account.
|
delegated to that vote account.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||||
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
Creating stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||||
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
|
Signature: 4pA2WKT6d2wkXEtSpiQswv22WyoFad2KX6FdPEzwBiEquvaUBEtzenys5Jh1ABPCh7yc4w8kzqMRRCwDj6ZSUV1K
|
||||||
```
|
```
|
||||||
|
@ -179,13 +255,13 @@ In order to maximize censorship resistance, we want to distribute our SOL to as
|
||||||
many validators as possible, so let's add a few more.
|
many validators as possible, so let's add a few more.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz
|
||||||
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
|
Creating stake account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie
|
||||||
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
|
Signature: 4pyRZzjsWG7jP3GRZeZCo2Eb2TPjHM4kAYRFMivimme6HAee1nhzoNJBe3VSt2sv7acp5fwT7J8omBM8o3niY8gu
|
||||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||||
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
Creating stake account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||||
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
|
Signature: 4ZUdZzUARgUCPuY8nVsJbN6vRDbVX8sYAQGYYXj2YVvjoJ2oevq2H8uzrhYApe419uoP7QYukqNstiti5p5DDukN
|
||||||
$ spl-stake-pool create-validator-stake 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
$ spl-stake-pool create-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm
|
||||||
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
Creating stake account FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13
|
||||||
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
|
Signature: yQqXCbuA66wQsHtkziNg3XadfZF5aCmvjfentwbZJnSPeEjJwPka3M1QY5GmR1efprptqaePn71BTMSLscX8DLr
|
||||||
```
|
```
|
||||||
|
@ -235,22 +311,21 @@ We created new validator stake accounts in the last step and staked them. Once
|
||||||
the stake activates, we can add them to the stake pool.
|
the stake activates, we can add them to the stake pool.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool add-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
$ spl-stake-pool add-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||||
Creating account to receive tokens Gu8xqzYFg2sPHWHhUivKNBeF9uikiauihLs9hLzziKu7
|
|
||||||
Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV
|
Signature: 3N1K89rGV9gWueTTrPGTDBwKAp8BikQhKHMFoREw98Q1piXFeZSSxqfnRQexrfAZQfrpYH9qwsaPWRruwkVeBivV
|
||||||
```
|
```
|
||||||
|
|
||||||
Users can start depositing their activated stakes into the stake pool, as
|
Users can start depositing their activated stakes into the stake pool, as
|
||||||
long as they are delegated to the same vote account, which was
|
long as they are delegated to the same vote account, which was
|
||||||
`FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` in this example. You can also
|
`FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN` in this example. You can also
|
||||||
double-check that at any time using the Solana command-line utility.
|
double-check that at any time using the Solana command-line utility.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
$ solana stake-account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||||
Balance: 0.002282881 SOL
|
Balance: 0.002282881 SOL
|
||||||
Rent Exempt Reserve: 0.00228288 SOL
|
Rent Exempt Reserve: 0.00228288 SOL
|
||||||
Delegated Stake: 0.000000001 SOL
|
Delegated Stake: 1.000000000 SOL
|
||||||
Active Stake: 0.000000001 SOL
|
Active Stake: 1.000000000 SOL
|
||||||
Activating Stake: 0 SOL
|
Activating Stake: 0 SOL
|
||||||
Stake activates starting from epoch: 161
|
Stake activates starting from epoch: 161
|
||||||
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
Delegated Vote Account Address: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3
|
||||||
|
@ -260,26 +335,31 @@ Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
|
|
||||||
#### Remove validator stake account
|
#### Remove validator stake account
|
||||||
|
|
||||||
If the stake pool manager wants to stop delegating to a vote account, they can
|
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 by providing
|
totally remove the validator stake account from the stake pool.
|
||||||
staking derivatives, just like `withdraw`.
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool remove-validator 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||||
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||||
```
|
```
|
||||||
|
|
||||||
The difference with `withdraw` is that the validator stake account is totally
|
The difference with `withdraw` is that the validator stake account is totally
|
||||||
removed from the stake pool and now belongs to the administrator.
|
removed from the stake pool and now belongs to the administrator. The authority
|
||||||
|
for the withdrawn stake account can also be specified using the `--new-authority` flag:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool remove-validator EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G --new-authority 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
|
Signature: 5rrQ3xhDWyiPkUTAQkNAeq31n6sMf1xsg2x9hVY8Vj1NonwBnhxuTv87nADLkwC8Xzc4CGTNCTX2Vph9esWnXk2d
|
||||||
|
```
|
||||||
|
|
||||||
We can check the removed stake account:
|
We can check the removed stake account:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
$ solana stake-account CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E
|
||||||
Balance: 1.002282881 SOL
|
Balance: 1.002282880 SOL
|
||||||
Rent Exempt Reserve: 0.00228288 SOL
|
Rent Exempt Reserve: 0.00228288 SOL
|
||||||
Delegated Stake: 1.000000001 SOL
|
Delegated Stake: 1.000000000 SOL
|
||||||
Active Stake: 1.000000001 SOL
|
Active Stake: 1.000000000 SOL
|
||||||
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
Delegated Vote Account Address: AUCzCaGAGjL3uyjFBtJs7KuJcgQWvNZu1Z2S9G3pw77G
|
||||||
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
|
@ -291,7 +371,7 @@ removal of staked SOL from the pool.
|
||||||
We can also double-check that the stake pool no longer shows the stake account:
|
We can also double-check that the stake pool no longer shows the stake account:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||||
|
@ -300,14 +380,14 @@ Total: ◎15.849959206
|
||||||
|
|
||||||
#### Rebalance the stake pool
|
#### Rebalance the stake pool
|
||||||
|
|
||||||
As time goes on, deposits and withdrawals will happen to all of the stake accounts
|
As time goes on, users will deposit to and withdraw from all of the stake accounts
|
||||||
managed by the pool, and the stake pool manager may want to rebalance the stakes.
|
managed by the pool, and the stake pool staker may want to rebalance the stakes.
|
||||||
|
|
||||||
For example, let's say the manager wants the same delegation to every validator
|
For example, let's say the staker wants the same delegation to every validator
|
||||||
in the pool. When they look at the state of the pool, they see:
|
in the pool. When they look at the state of the pool, they see:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎1.002282881
|
||||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎3.410872673
|
||||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎11.436803652
|
||||||
|
@ -315,75 +395,63 @@ Total: ◎15.849959206
|
||||||
```
|
```
|
||||||
|
|
||||||
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
|
This isn't great! The last stake account, `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`
|
||||||
has too much allocated. For their strategy, the manager wants the `15.849959206`
|
has too much allocated. For their strategy, the staker wants the `15.849959206`
|
||||||
SOL to be distributed evenly, meaning around `5.283319735` in each account. They need
|
SOL to be distributed evenly, meaning around `5.283319735` in each account. They need
|
||||||
to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and
|
to move `4.281036854` to `FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13` and
|
||||||
`1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`.
|
`1.872447062` to `FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN`.
|
||||||
|
|
||||||
First, they need to withdraw a total of `6.153483916` from
|
##### Decrease validator stake
|
||||||
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`. Using the `spl-token` utility,
|
|
||||||
let's check the total supply of pool tokens:
|
First, they need to decrease the amount on stake account
|
||||||
|
`E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie`, delegated to
|
||||||
|
`HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz`, by total of `6.153483916` SOL.
|
||||||
|
|
||||||
|
They decrease that amount of SOL:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-token supply Gmk71cM7j2RMorRsQrsyysM4HsByQx5PuDGtDdqGLWCS
|
$ spl-stake-pool decrease-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz 6.153483916
|
||||||
0.034692168
|
Signature: ZpQGwT85rJ8Y9afdkXhKo3TVv4xgTz741mmZj2vW7mihYseAkFsazWxza2y8eNGY4HDJm15c1cStwyiQzaM3RpH
|
||||||
```
|
```
|
||||||
|
|
||||||
Given a total pool token supply of `0.034692168` and total staked SOL amount of
|
Internally, this instruction splits and deactivates 6.153483916 SOL from the
|
||||||
`15.849959206`, let's calculate how many pool tokens to withdraw from the pool:
|
validator stake account `E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie` into a
|
||||||
|
transient stake account, owned and managed entirely by the stake pool.
|
||||||
|
|
||||||
```
|
Once the stake is deactivated during the next epoch, the `update` command will
|
||||||
sol_to_withdraw * total_pool_tokens / total_sol_staked = pool_tokens_to_withdraw
|
automatically merge the transient stake account into a reserve stake account,
|
||||||
6.153483916 * 0.034692168 / 15.849959206 ~ 0.013468659
|
also entirely owned and managed by the stake pool.
|
||||||
```
|
|
||||||
|
|
||||||
They withdraw that amount of pool tokens:
|
##### Increase validator stake
|
||||||
|
|
||||||
|
Now that the reserve stake account has enough to perform the rebalance, the staker
|
||||||
|
can increase the stake on the two other validators,
|
||||||
|
`8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm` and
|
||||||
|
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`.
|
||||||
|
|
||||||
|
They add 4.281036854 SOL to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.013468659 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm 4.281036854
|
||||||
Withdrawing from account E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie, amount ◎6.153483855, 0.013468659 pool tokens
|
Signature: 3GJACzjUGLPjcd9RLUW86AfBLWKapZRkxnEMc2yHT6erYtcKBgCapzyrVH6VN8Utxj7e2mtvzcigwLm6ZafXyTMw
|
||||||
Creating account to receive stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
|
||||||
Signature: z8a5ZRfWdj8Fcsr3ttCJ731wFKyhZNcqoKEdV1RBCkzr3tHGQNCC56qvRVJ6oxyCVDqWZ3KL1Bkyn3sDpjYPDku
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Because of rounding in the calculation a few lines above, it looks like we receive
|
And they add 1.872447062 SOL to `2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3`:
|
||||||
less than we should. If we play that back the other way, we'll see that all is well:
|
|
||||||
|
|
||||||
```
|
|
||||||
pool_tokens_to_withdraw * total_sol_staked / total_pool_tokens = sol_to_withdraw
|
|
||||||
0.013468659 * 15.849959206 / 0.034692168 ~ 6.153483855
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, they deactivate the new received stake:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ solana deactivate-stake 8ykyY7maA9HUfUphZHBkhsnydY5gFfyHFSfxCA7imqrk
|
$ spl-stake-pool increase-validator-stake EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 1.872447062
|
||||||
Signature: 4SuwZK5JvYkYVkM5yfu2x8x6iou6558teMwzphGECLmstMVoWbSvngUH48Ra24PrxtgUDyVDA8SXYS1qMyx3fjMj
|
Signature: 4zaKYu3MQ3as8reLbuHKaXN8FNaHvpHuiZtsJeARo67UKMo6wUUoWE88Fy8N4EYQYicuwULTNffcUD3a9jY88PoU
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the stake is deactivated during the next epoch, they split the stake
|
Internally, this instruction also uses transient stake accounts. This time, the
|
||||||
and activate it on the other two validator vote accounts. For brevity, those
|
stake pool splits from the reserve stake, into the transient stake account,
|
||||||
commands are omitted.
|
then activates it to the appropriate validator.
|
||||||
|
|
||||||
Eventually, we are left with stake account `4zppED2kFodUS2hBf8Fzeepu6yZ6QuyeNPBXCT9VU6fK`
|
One to two epochs later, once the transient stakes activate, the `update` command
|
||||||
with `4.281036854` delegated to `8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm`
|
automatically merges the transient stakes into the validator stake account, leaving
|
||||||
and stake account `GCJnuFGCDzaToPwJtG5GiK4g3DJBfuhQy6388NyGcfwf` with `1.872447062`
|
a fully rebalanced stake pool:
|
||||||
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
|
```sh
|
||||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
|
Pubkey: FhFft7ArhZZkh6q4ir1JZMYFgXdH6wkT5M5nmDDb1Q13 Vote: 8r1f8mwrUiYdg2Rx9sxTh4M3UAUcCBBrmRA3nxk3Z6Lm ◎5.283340235
|
||||||
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
|
Pubkey: FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN Vote: 2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3 ◎5.283612231
|
||||||
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
|
Pubkey: E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie Vote: HJiC8iJ4Sj846SswQuauFJK93UvV6zp3c2T6jzGqzhhz ◎5.284317422
|
||||||
|
@ -391,33 +459,7 @@ Total: ◎15.851269888
|
||||||
```
|
```
|
||||||
|
|
||||||
Due to staking rewards that accrued during the rebalancing process, the pool is
|
Due to staking rewards that accrued during the rebalancing process, the pool is
|
||||||
not prefectly balanced. This is completely normal.
|
not perfectly 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
|
### User Examples
|
||||||
|
|
||||||
|
@ -429,7 +471,7 @@ command-line utility has a special instruction for finding out which vote
|
||||||
accounts are already associated with the stake pool.
|
accounts are already associated with the stake pool.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
|
CrStLEWfme37kDc3nubK9HsmWR5dsuVUuqEKqTR4Mc5E 1.002282880 SOL
|
||||||
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
|
E5KBATUd21Dnjnh5sGFw5ngp9kdVXCcAAYMRe2WsVXie 1.002282880 SOL
|
||||||
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
|
FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN 1.002282880 SOL
|
||||||
|
@ -441,13 +483,13 @@ If the manager has recently created the stake pool, and there are no stake
|
||||||
accounts present yet, the command-line utility will inform us.
|
accounts present yet, the command-line utility will inform us.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool list 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool list EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
No accounts found.
|
No accounts found.
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Deposit stake
|
#### Deposit stake
|
||||||
|
|
||||||
Stake pools only accept deposits from fully staked accounts, so we must first
|
Stake pools only accept deposits from active accounts, so we must first
|
||||||
create stake accounts and delegate them to one of the validators managed by the
|
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
|
stake pool. Using the `list` command from the previous section, we see that
|
||||||
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's
|
`2HUKQz7W2nXZSwrdX5RkfS2rLU4j1QZLjdGCHcoUKFh3` is a valid vote account, so let's
|
||||||
|
@ -473,17 +515,19 @@ Two epochs later, when the stake is fully active and has received one epoch of
|
||||||
rewards, we can deposit the stake into the stake pool.
|
rewards, we can deposit the stake into the stake pool.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa
|
||||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||||
Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
Creating account to receive tokens 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
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
|
Alternatively, you can create an SPL token account yourself and pass it as the
|
||||||
`token-receiver` for the command.
|
`token-receiver` for the command.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool deposit 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
$ spl-stake-pool deposit EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 4F4AYKZbNtDnu7uQey2Vkz9VgkVtLE6XWLezYjc9yxZa --token-receiver 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
||||||
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
Depositing into stake account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN
|
||||||
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
Signature: 4AESGZzqBVfj5xQnMiPWAwzJnAtQDRFK1Ha6jqKKTs46Zm5fw3LqgU1mRAT6CKTywVfFMHZCLm1hcQNScSMwVvjQ
|
||||||
```
|
```
|
||||||
|
@ -505,7 +549,8 @@ In order to calculate the proper value of these stake pool tokens, we must updat
|
||||||
the total value managed by the stake pool every epoch.
|
the total value managed by the stake pool every epoch.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
|
Updating stake pool...
|
||||||
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -513,13 +558,33 @@ If another user already updated the stake pool balance for the current epoch, we
|
||||||
see a different output.
|
see a different output.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool update 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC
|
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1
|
||||||
Stake pool balances are up to date, no update required.
|
Update not required
|
||||||
```
|
```
|
||||||
|
|
||||||
If no one updates the stake pool in the current epoch, the deposit and withdraw
|
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
|
instructions will fail. The update instruction is permissionless, so any user
|
||||||
can run it before depositing or withdrawing.
|
can run it before depositing or withdrawing. As a convenience, the CLI attempts
|
||||||
|
to update before running any instruction on the stake pool.
|
||||||
|
|
||||||
|
If the stake pool transient stakes are in an unexpected state, and merges are
|
||||||
|
not possible, there is the option to only update the stake pool balances without
|
||||||
|
performing merges using the `--no-merge` flag.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --no-merge
|
||||||
|
Updating stake pool...
|
||||||
|
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||||
|
```
|
||||||
|
|
||||||
|
Later on, whenever the transient stakes are ready to be merged, it is possible to
|
||||||
|
force another update in the same epoch using the `--force` flag.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ spl-stake-pool update EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --force
|
||||||
|
Updating stake pool...
|
||||||
|
Signature: 3Yx1RH3Afqj5ckX8YvPCRt1DudVP4HuRPkh1dBPvTM9GqGxcB9ZXHGZPADVSZiaqKi166fevMG232EWxrRWswPtt
|
||||||
|
```
|
||||||
|
|
||||||
#### Withdraw stake
|
#### Withdraw stake
|
||||||
|
|
||||||
|
@ -529,7 +594,7 @@ staking derivative SPL tokens in exchange for an activated stake account.
|
||||||
Let's withdraw 0.02 staking derivative tokens from the stake pool.
|
Let's withdraw 0.02 staking derivative tokens from the stake pool.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF
|
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 0.02
|
||||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||||
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
Creating account to receive stake CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
||||||
|
@ -550,15 +615,58 @@ Stake Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
Withdraw Authority: 4SnSuUtJGKvk2GYpBwmEsWG53zTurVM8yXGsoiZQyMJn
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, the user can specify an existing stake account to receive their
|
Alternatively, the user can specify an existing uninitialized stake account to
|
||||||
stake using the `stake-receiver` parameter.
|
receive their stake using the `--stake-receiver` parameter.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ spl-stake-pool withdraw 3CLwo9CntMi4D1enHEFBe3pRJQzGJBCAYe66xFuEbmhC --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
$ spl-stake-pool withdraw EjspffVUi2Tivszzs2JVj4GiSiMNYKyqZpgP3NeefBU1 --amount 0.02 --withdraw-from 34XMHa3JUPv46ftU4dGHvemZ9oKVjnciRePYMcX3rjEF --stake-receiver CZF2z3JJoDmJRcVjtsrz1BKUUGNL3VPW5FPFqge1bzmQ
|
||||||
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
Withdrawing from account FYQB64aEzSmECvnG8RVvdAXBxRnzrLvcA3R22aGH2hUN, amount 8.867176377 SOL, 0.02 pool tokens
|
||||||
Signature: 2xBPVPJ749AE4hHNCNYdjuHv1EdMvxm9uvvraWfTA7Urrvecwh9w64URCyLLroLQ2RKDGE2QELM2ZHd8qRkjavJM
|
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
|
## Appendix
|
||||||
|
|
||||||
### Activated stakes
|
### Activated stakes
|
||||||
|
@ -569,6 +677,22 @@ are not equivalent to inactive, activating, or deactivating stakes due to the
|
||||||
time cost of staking. Otherwise, malicious actors can deposit stake in one state
|
time cost of staking. Otherwise, malicious actors can deposit stake in one state
|
||||||
and withdraw it in another state without waiting.
|
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
|
### Staking Credits Observed on Deposit
|
||||||
|
|
||||||
A deposited stake account's "credits observed" must match the destination
|
A deposited stake account's "credits observed" must match the destination
|
||||||
|
|
|
@ -65,7 +65,7 @@ Keypair Path: ${HOME}/.config/solana/id.json
|
||||||
|
|
||||||
See [Solana clusters](https://docs.solana.com/clusters) for cluster-specific RPC URLs
|
See [Solana clusters](https://docs.solana.com/clusters) for cluster-specific RPC URLs
|
||||||
```
|
```
|
||||||
solana config set --url https://devnet.solana.com
|
solana config set --url https://api.devnet.solana.com
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Default Keypair
|
#### Default Keypair
|
||||||
|
@ -83,6 +83,15 @@ Hardware Wallet URL (See [URL spec](https://docs.solana.com/wallet-guide/hardwar
|
||||||
solana config set --keypair usb://ledger/
|
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
|
### Example: Creating your own fungible token
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
@ -109,7 +118,7 @@ Signature: 42Sa5eK9dMEQyvD9GMHuKxXf55WLZ7tfjabUKDhNoZRAxj9MsnN7omriWMEHXLea3aYpj
|
||||||
|
|
||||||
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
|
`7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi` is now an empty account:
|
||||||
```sh
|
```sh
|
||||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||||
0
|
0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -126,7 +135,7 @@ The token `supply` and account `balance` now reflect the result of minting:
|
||||||
```sh
|
```sh
|
||||||
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
$ spl-token supply AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||||
100
|
100
|
||||||
$ spl-token balance 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
$ spl-token balance AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
||||||
100
|
100
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -166,7 +175,7 @@ address by running `solana address` and provides it to the sender.
|
||||||
|
|
||||||
The sender then runs:
|
The sender then runs:
|
||||||
```
|
```
|
||||||
$ spl-token transfer 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
$ spl-token transfer AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
Transfer 50 tokens
|
Transfer 50 tokens
|
||||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
@ -184,7 +193,7 @@ The receiver obtains their wallet address by running `solana address` and provid
|
||||||
The sender then runs to fund the receiver's associated token account, at the
|
The sender then runs to fund the receiver's associated token account, at the
|
||||||
sender's expense, and then transfers 50 tokens into it:
|
sender's expense, and then transfers 50 tokens into it:
|
||||||
```
|
```
|
||||||
$ spl-token transfer --fund-recipient 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
$ spl-token transfer --fund-recipient AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM 50 vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
Transfer 50 tokens
|
Transfer 50 tokens
|
||||||
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
Sender: 7UX2i7SucgLMQcfZ75s3VXmZZY4YRUyJN9X1RgfMoDUi
|
||||||
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
Recipient: vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg
|
||||||
|
@ -199,7 +208,7 @@ Tokens may be transferred to a specific recipient token account. The recipient
|
||||||
token account must already exist and be of the same Token type.
|
token account must already exist and be of the same Token type.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ spl-token create-account AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM
|
$ spl-token create-account AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe1TTJC9wajM /path/to/auxiliary_keypair.json
|
||||||
Creating account CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
Creating account CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ
|
||||||
Signature: 4yPWj22mbyLu5mhfZ5WATNfYzTt5EQ7LGzryxM7Ufu7QCVjTE7czZdEBqdKR7vjKsfAqsBdjU58NJvXrTqCXvfWW
|
Signature: 4yPWj22mbyLu5mhfZ5WATNfYzTt5EQ7LGzryxM7Ufu7QCVjTE7czZdEBqdKR7vjKsfAqsBdjU58NJvXrTqCXvfWW
|
||||||
```
|
```
|
||||||
|
@ -228,9 +237,9 @@ CqAxDdBRnawzx9q4PYM3wrybLHBhDZ4P6BTV13WsRJYJ AQoKYV7tYpTrFZN6P5oUufbQKAUr9mNYGe
|
||||||
|
|
||||||
### Example: Create a non-fungible token
|
### Example: Create a non-fungible token
|
||||||
|
|
||||||
Create the token type,
|
Create the token type with nine decimal places,
|
||||||
```
|
```
|
||||||
$ spl-token create-token
|
$ spl-token create-token --decimals 9
|
||||||
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
Creating token 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
||||||
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
|
Signature: 4kz82JUey1B9ki1McPW7NYv1NqPKCod6WNptSkYqtuiEsQb9exHaktSAHJJsm4YxuGNW4NugPJMFX9ee6WA2dXts
|
||||||
```
|
```
|
||||||
|
@ -264,7 +273,7 @@ Now the `7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM` account holds the
|
||||||
one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token:
|
one and only `559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z` token:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ spl-token account-info 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
|
$ spl-token account-info 559u4Tdr9umKwft3yHMsnAxohhzkFnUBPAFtibwuZD9z
|
||||||
|
|
||||||
Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
|
Address: 7KqpRwzkkeweW5jQoETyLzhvs9rcCj9dVQ1MnzudirsM
|
||||||
Balance: 1
|
Balance: 1
|
||||||
|
@ -520,7 +529,7 @@ There is a rich set of JSON RPC methods available for use with SPL Token:
|
||||||
|
|
||||||
See https://docs.solana.com/apps/jsonrpc-api for more details.
|
See https://docs.solana.com/apps/jsonrpc-api for more details.
|
||||||
|
|
||||||
Additionally the versatile `getProgramAcccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
Additionally the versatile `getProgramAccounts` JSON RPC method can be employed in various ways to fetch SPL Token accounts of interest.
|
||||||
|
|
||||||
### Finding all token accounts for a specific mint
|
### Finding all token accounts for a specific mint
|
||||||
|
|
||||||
|
@ -553,7 +562,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
|
||||||
```
|
```
|
||||||
|
|
||||||
The `"dataSize": 165` filter selects all [Token
|
The `"dataSize": 165` filter selects all [Token
|
||||||
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
Account](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
|
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)
|
[mint](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L88)
|
||||||
address within each token account.
|
address within each token account.
|
||||||
|
@ -588,7 +597,7 @@ curl http://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/js
|
||||||
```
|
```
|
||||||
|
|
||||||
The `"dataSize": 165` filter selects all [Token
|
The `"dataSize": 165` filter selects all [Token
|
||||||
Acccount](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L86-L106)s,
|
Account](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
|
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)
|
[owner](https://github.com/solana-labs/solana-program-library/blob/08d9999f997a8bf38719679be9d572f119d0d960/token/program/src/state.rs#L90)
|
||||||
address within each token account.
|
address within each token account.
|
||||||
|
@ -825,7 +834,13 @@ The sender's wallet must not require that the recipient's main wallet address
|
||||||
hold a balance before allowing the transfer.
|
hold a balance before allowing the transfer.
|
||||||
|
|
||||||
### Registry for token details
|
### Registry for token details
|
||||||
At the moment Token Mint addresses need to be hard coded by each wallet. **Improving this situation is a work in progress.**
|
At the moment there exist two solutions for Token Mint registries:
|
||||||
|
|
||||||
|
* hard coded addresses in the wallet or dapp
|
||||||
|
* [spl-token-registry](https://www.npmjs.com/package/@solana/spl-token-registry)
|
||||||
|
package, maintained at [https://github.com/solana-labs/token-list](https://github.com/solana-labs/token-list)
|
||||||
|
|
||||||
|
**A decentralized solution is in progress.**
|
||||||
|
|
||||||
### Garbage Collecting Ancillary Token Accounts
|
### Garbage Collecting Ancillary Token Accounts
|
||||||
Wallets should empty ancillary token accounts as quickly as practical by
|
Wallets should empty ancillary token accounts as quickly as practical by
|
||||||
|
@ -852,3 +867,13 @@ the maximum allowed transaction size, remove those extra clean up instructions.
|
||||||
They can be cleaned up during the next send operation.
|
They can be cleaned up during the next send operation.
|
||||||
|
|
||||||
The `spl-token gc` command provides an example implementation of this cleanup process.
|
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)
|
||||||
|
|
|
@ -22,7 +22,7 @@ extern uint64_t do_invoke(SolParameters *params) {
|
||||||
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
|
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
|
||||||
|
|
||||||
SolPubkey expected_allocated_key;
|
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,
|
params->program_id,
|
||||||
&expected_allocated_key)) {
|
&expected_allocated_key)) {
|
||||||
return ERROR_INVALID_INSTRUCTION_DATA;
|
return ERROR_INVALID_INSTRUCTION_DATA;
|
||||||
|
@ -31,8 +31,7 @@ extern uint64_t do_invoke(SolParameters *params) {
|
||||||
return ERROR_INVALID_ARGUMENT;
|
return ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
SolAccountMeta arguments[] = {{system_program_info->key, false, false},
|
SolAccountMeta arguments[] = {{allocated_info->key, true, true}};
|
||||||
{allocated_info->key, true, true}};
|
|
||||||
uint8_t data[4 + 8]; // Enough room for the Allocate instruction
|
uint8_t data[4 + 8]; // Enough room for the Allocate instruction
|
||||||
*(uint16_t *)data = 8; // Allocate instruction enum value
|
*(uint16_t *)data = 8; // Allocate instruction enum value
|
||||||
*(uint64_t *)(data + 4) = SIZE; // Size to allocate
|
*(uint64_t *)(data + 4) = SIZE; // Size to allocate
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
# Program examples written in Rust
|
# Program examples written in Rust
|
||||||
|
|
||||||
The examples in this directory demonstrate various Solana program mechanisms.
|
The examples in this directory demonstrate various Solana program mechanisms.
|
||||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -15,11 +15,11 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -13,11 +13,11 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -17,24 +17,28 @@ pub fn process_instruction(
|
||||||
// Create in iterator to safety reference accounts in the slice
|
// Create in iterator to safety reference accounts in the slice
|
||||||
let account_info_iter = &mut accounts.iter();
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
// The first account is the clock sysvar
|
// Get the clock sysvar via syscall
|
||||||
|
let clock_via_sysvar = Clock::get()?;
|
||||||
|
// Or deserialize the account into a clock struct
|
||||||
let clock_sysvar_info = next_account_info(account_info_iter)?;
|
let clock_sysvar_info = next_account_info(account_info_iter)?;
|
||||||
// The second account is the rent sysvar
|
let clock_via_account = Clock::from_account_info(&clock_sysvar_info)?;
|
||||||
let rent_sysvar_info = next_account_info(account_info_iter)?;
|
// Both produce the same sysvar
|
||||||
|
assert_eq!(clock_via_sysvar, clock_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
|
// Note: `format!` can be very expensive, use cautiously
|
||||||
msg!("{:?}", clock);
|
msg!("{:?}", clock_via_sysvar);
|
||||||
|
|
||||||
|
// Get the rent sysvar via syscall
|
||||||
|
let rent_via_sysvar = Rent::get()?;
|
||||||
|
// Or deserialize the account into a rent struct
|
||||||
|
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);
|
||||||
// Can't print `exemption_threshold` because BPF does not support printing floats
|
// Can't print `exemption_threshold` because BPF does not support printing floats
|
||||||
msg!(
|
msg!(
|
||||||
"Rent: lamports_per_byte_year: {:?}, burn_percent: {:?}",
|
"Rent: lamports_per_byte_year: {:?}, burn_percent: {:?}",
|
||||||
rent.lamports_per_byte_year,
|
rent_via_sysvar.lamports_per_byte_year,
|
||||||
rent.burn_percent
|
rent_via_sysvar.burn_percent
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -12,11 +12,11 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
solana-program-test = "1.6.2"
|
solana-program-test = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -10,11 +10,11 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
chrono = "0.4.19"
|
chrono = "0.4.19"
|
||||||
clap = "2.33.3"
|
clap = "2.33.3"
|
||||||
solana-clap-utils = "1.6.2"
|
solana-clap-utils = "1.6.11"
|
||||||
solana-cli-config = "1.6.2"
|
solana-cli-config = "1.6.11"
|
||||||
solana-client = "1.6.2"
|
solana-client = "1.6.11"
|
||||||
solana-logger = "1.6.2"
|
solana-logger = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
|
spl-feature-proposal = { version = "1.0", path = "../program", features = ["no-entrypoint"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
@ -12,15 +12,14 @@ no-entrypoint = []
|
||||||
test-bpf = []
|
test-bpf = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
borsh = "0.7.1"
|
borsh = "0.8"
|
||||||
borsh-derive = "0.8.1"
|
borsh-derive = "0.8.1"
|
||||||
solana-program = "1.6.2"
|
solana-program = "1.6.11"
|
||||||
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
spl-token = { version = "3.1", path = "../../token/program", features = ["no-entrypoint"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
futures = "0.3"
|
solana-program-test = "1.6.11"
|
||||||
solana-program-test = "1.6.2"
|
solana-sdk = "1.6.11"
|
||||||
solana-sdk = "1.6.2"
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "lib"]
|
crate-type = ["cdylib", "lib"]
|
||||||
|
|
|
@ -155,13 +155,12 @@ pub fn tally(feature_proposal_address: &Pubkey) -> Instruction {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::borsh_utils;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_packed_len() {
|
fn test_get_packed_len() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FeatureProposalInstruction::get_packed_len(),
|
FeatureProposalInstruction::get_packed_len(),
|
||||||
borsh_utils::get_packed_len::<FeatureProposalInstruction>()
|
solana_program::borsh::get_packed_len::<FeatureProposalInstruction>()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
#![deny(missing_docs)]
|
#![deny(missing_docs)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
pub mod borsh_utils;
|
|
||||||
mod entrypoint;
|
mod entrypoint;
|
||||||
pub mod instruction;
|
pub mod instruction;
|
||||||
pub mod processor;
|
pub mod processor;
|
||||||
|
|
|
@ -59,13 +59,12 @@ impl Pack for FeatureProposal {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::borsh_utils;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_get_packed_len() {
|
fn test_get_packed_len() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
FeatureProposal::get_packed_len(),
|
FeatureProposal::get_packed_len(),
|
||||||
borsh_utils::get_packed_len::<FeatureProposal>()
|
solana_program::borsh::get_packed_len::<FeatureProposal>()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program
|
||||||
#![cfg(feature = "test-bpf")]
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
use futures::{Future, FutureExt};
|
use {
|
||||||
use solana_program::{
|
solana_program::{
|
||||||
feature::{self, Feature},
|
feature::{self, Feature},
|
||||||
program_option::COption,
|
program_option::COption,
|
||||||
program_pack::Pack,
|
pubkey::Pubkey,
|
||||||
pubkey::Pubkey,
|
system_program,
|
||||||
system_program,
|
},
|
||||||
|
solana_program_test::*,
|
||||||
|
solana_sdk::{
|
||||||
|
signature::{Keypair, Signer},
|
||||||
|
transaction::Transaction,
|
||||||
|
},
|
||||||
|
spl_feature_proposal::{instruction::*, state::*, *},
|
||||||
};
|
};
|
||||||
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 {
|
fn program_test() -> ProgramTest {
|
||||||
ProgramTest::new(
|
ProgramTest::new(
|
||||||
|
@ -25,21 +24,6 @@ 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]
|
#[tokio::test]
|
||||||
async fn test_basic() {
|
async fn test_basic() {
|
||||||
let feature_proposal = Keypair::new();
|
let feature_proposal = Keypair::new();
|
||||||
|
@ -68,16 +52,17 @@ async fn test_basic() {
|
||||||
banks_client.process_transaction(transaction).await.unwrap();
|
banks_client.process_transaction(transaction).await.unwrap();
|
||||||
|
|
||||||
// Confirm feature id account is now funded and allocated, but not assigned
|
// Confirm feature id account is now funded and allocated, but not assigned
|
||||||
let feature_id_acccount = banks_client
|
let feature_id_account = banks_client
|
||||||
.get_account(feature_id_address)
|
.get_account(feature_id_address)
|
||||||
.await
|
.await
|
||||||
.expect("success")
|
.expect("success")
|
||||||
.expect("some account");
|
.expect("some account");
|
||||||
assert_eq!(feature_id_acccount.owner, system_program::id());
|
assert_eq!(feature_id_account.owner, system_program::id());
|
||||||
assert_eq!(feature_id_acccount.data.len(), Feature::size_of());
|
assert_eq!(feature_id_account.data.len(), Feature::size_of());
|
||||||
|
|
||||||
// Confirm mint account state
|
// Confirm mint account state
|
||||||
let mint = get_account_data::<spl_token::state::Mint>(&mut banks_client, mint_address)
|
let mint = banks_client
|
||||||
|
.get_packed_account_data::<spl_token::state::Mint>(mint_address)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(mint.supply, 42);
|
assert_eq!(mint.supply, 42);
|
||||||
|
@ -86,20 +71,20 @@ async fn test_basic() {
|
||||||
assert_eq!(mint.mint_authority, COption::Some(mint_address));
|
assert_eq!(mint.mint_authority, COption::Some(mint_address));
|
||||||
|
|
||||||
// Confirm distributor token account state
|
// Confirm distributor token account state
|
||||||
let distributor_token =
|
let distributor_token = banks_client
|
||||||
get_account_data::<spl_token::state::Account>(&mut banks_client, distributor_token_address)
|
.get_packed_account_data::<spl_token::state::Account>(distributor_token_address)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(distributor_token.amount, 42);
|
assert_eq!(distributor_token.amount, 42);
|
||||||
assert_eq!(distributor_token.mint, mint_address);
|
assert_eq!(distributor_token.mint, mint_address);
|
||||||
assert_eq!(distributor_token.owner, feature_proposal.pubkey());
|
assert_eq!(distributor_token.owner, feature_proposal.pubkey());
|
||||||
assert!(distributor_token.close_authority.is_none());
|
assert!(distributor_token.close_authority.is_none());
|
||||||
|
|
||||||
// Confirm acceptance token account state
|
// Confirm acceptance token account state
|
||||||
let acceptance_token =
|
let acceptance_token = banks_client
|
||||||
get_account_data::<spl_token::state::Account>(&mut banks_client, acceptance_token_address)
|
.get_packed_account_data::<spl_token::state::Account>(acceptance_token_address)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(acceptance_token.amount, 0);
|
assert_eq!(acceptance_token.amount, 0);
|
||||||
assert_eq!(acceptance_token.mint, mint_address);
|
assert_eq!(acceptance_token.mint, mint_address);
|
||||||
assert_eq!(acceptance_token.owner, id());
|
assert_eq!(acceptance_token.owner, id());
|
||||||
|
@ -115,15 +100,17 @@ async fn test_basic() {
|
||||||
banks_client.process_transaction(transaction).await.unwrap();
|
banks_client.process_transaction(transaction).await.unwrap();
|
||||||
|
|
||||||
// Confirm feature id account is not yet assigned
|
// Confirm feature id account is not yet assigned
|
||||||
let feature_id_acccount = banks_client
|
let feature_id_account = banks_client
|
||||||
.get_account(feature_id_address)
|
.get_account(feature_id_address)
|
||||||
.await
|
.await
|
||||||
.expect("success")
|
.expect("success")
|
||||||
.expect("some account");
|
.expect("some account");
|
||||||
assert_eq!(feature_id_acccount.owner, system_program::id());
|
assert_eq!(feature_id_account.owner, system_program::id());
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
banks_client
|
||||||
|
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||||
|
.await,
|
||||||
Ok(FeatureProposal::Pending(_))
|
Ok(FeatureProposal::Pending(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -158,16 +145,18 @@ async fn test_basic() {
|
||||||
banks_client.process_transaction(transaction).await.unwrap();
|
banks_client.process_transaction(transaction).await.unwrap();
|
||||||
|
|
||||||
// Confirm feature id account is now assigned
|
// Confirm feature id account is now assigned
|
||||||
let feature_id_acccount = banks_client
|
let feature_id_account = banks_client
|
||||||
.get_account(feature_id_address)
|
.get_account(feature_id_address)
|
||||||
.await
|
.await
|
||||||
.expect("success")
|
.expect("success")
|
||||||
.expect("some account");
|
.expect("some account");
|
||||||
assert_eq!(feature_id_acccount.owner, feature::id());
|
assert_eq!(feature_id_account.owner, feature::id());
|
||||||
|
|
||||||
// Confirm feature proposal account state
|
// Confirm feature proposal account state
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
banks_client
|
||||||
|
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||||
|
.await,
|
||||||
Ok(FeatureProposal::Accepted {
|
Ok(FeatureProposal::Accepted {
|
||||||
tokens_upon_acceptance: 42
|
tokens_upon_acceptance: 42
|
||||||
})
|
})
|
||||||
|
@ -197,7 +186,9 @@ async fn test_expired() {
|
||||||
banks_client.process_transaction(transaction).await.unwrap();
|
banks_client.process_transaction(transaction).await.unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
banks_client
|
||||||
|
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||||
|
.await,
|
||||||
Ok(FeatureProposal::Pending(_))
|
Ok(FeatureProposal::Pending(_))
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -208,7 +199,9 @@ async fn test_expired() {
|
||||||
banks_client.process_transaction(transaction).await.unwrap();
|
banks_client.process_transaction(transaction).await.unwrap();
|
||||||
|
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
get_account_data::<FeatureProposal>(&mut banks_client, feature_proposal.pubkey()).await,
|
banks_client
|
||||||
|
.get_packed_account_data::<FeatureProposal>(feature_proposal.pubkey())
|
||||||
|
.await,
|
||||||
Ok(FeatureProposal::Expired)
|
Ok(FeatureProposal::Expired)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
# 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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Realm account
|
||||||
|
|
||||||
|
Realm account 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.
|
||||||
|
|
||||||
|
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. Optionally `CreateProgramGovernance` instruction can also transfer `upgrade_authority`
|
||||||
|
of the governed program to the Governance PDA at the creation time of the Governance account.
|
||||||
|
|
||||||
|
### How does the 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 instructions 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 Owner) and tied to a given Governance account
|
||||||
|
and has a set of executable instructions 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.
|
||||||
|
Its 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 Owner and receives permission to edit the Proposal.
|
||||||
|
With this power the Owner can edit the Proposal, add/remove Signatories to the Proposal and also cancel it.
|
||||||
|
These Signatories can then show their approval of the Proposal by signing it off.
|
||||||
|
Once all Signatories have signed off 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 Succeeded state. If the vote can't be tipped automatically
|
||||||
|
during the voting period but still reaches the required Yes vote threshold it can be manually transitioned to Succeeded state
|
||||||
|
using FinalizeVote instruction.
|
||||||
|
Once all Proposal instructions are executed the Proposal enters Completed state.
|
||||||
|
|
||||||
|
In the Executing state an instruction can be run by any one at any time after the `instruction_hold_up_time` period has
|
||||||
|
transpired.
|
||||||
|
|
||||||
|
### ProposalInstruction
|
||||||
|
|
||||||
|
A Proposal can have multiple Proposal Instructions, and they run independently of each other.
|
||||||
|
These contain the actual data for an instruction, 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 decided 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
|
||||||
|
|
||||||
|

|
|
@ -0,0 +1,33 @@
|
||||||
|
[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.11"
|
||||||
|
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.11"
|
||||||
|
solana-sdk = "1.6.11"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "lib"]
|
|
@ -0,0 +1,2 @@
|
||||||
|
[target.bpfel-unknown-unknown.dependencies.std]
|
||||||
|
features = []
|
|
@ -0,0 +1,22 @@
|
||||||
|
//! 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(())
|
||||||
|
}
|
|
@ -0,0 +1,248 @@
|
||||||
|
//! 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 votes must be relinquished to withdraw governing tokens
|
||||||
|
#[error("All votes must be relinquished to withdraw governing tokens")]
|
||||||
|
AllVotesMustBeRelinquishedToWithdrawGoverningTokens,
|
||||||
|
|
||||||
|
/// Invalid Token Owner Record account address
|
||||||
|
#[error("Invalid Token Owner Record account address")]
|
||||||
|
InvalidTokenOwnerRecordAccountAddress,
|
||||||
|
|
||||||
|
/// Invalid GoverningMint for TokenOwnerRecord
|
||||||
|
#[error("Invalid GoverningMint for TokenOwnerRecord")]
|
||||||
|
InvalidGoverningMintForTokenOwnerRecord,
|
||||||
|
|
||||||
|
/// Invalid Realm for TokenOwnerRecord
|
||||||
|
#[error("Invalid Realm for TokenOwnerRecord")]
|
||||||
|
InvalidRealmForTokenOwnerRecord,
|
||||||
|
|
||||||
|
/// Invalid Proposal for ProposalInstruction
|
||||||
|
#[error("Invalid Proposal for ProposalInstruction")]
|
||||||
|
InvalidProposalForProposalInstruction,
|
||||||
|
|
||||||
|
/// 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 Proposal for VoterRecord
|
||||||
|
#[error("Invalid Proposal for VoterRecord")]
|
||||||
|
InvalidProposalForVoterRecord,
|
||||||
|
|
||||||
|
/// Invalid GoverningTokenOwner for VoteRecord
|
||||||
|
#[error("Invalid GoverningTokenOwner for VoteRecord")]
|
||||||
|
InvalidGoverningTokenOwnerForVoteRecord,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Token Owner already voted on the Proposal
|
||||||
|
#[error("Token Owner already voted on the Proposal")]
|
||||||
|
VoteAlreadyExists,
|
||||||
|
|
||||||
|
/// 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 Proposal state
|
||||||
|
#[error("Invalid Proposal state")]
|
||||||
|
InvalidProposalState,
|
||||||
|
/// Invalid State: Can't edit instructions
|
||||||
|
#[error("Invalid State: Can't edit instructions")]
|
||||||
|
InvalidStateCannotEditInstructions,
|
||||||
|
|
||||||
|
/// Invalid State: Can't execute instruction
|
||||||
|
#[error("Invalid State: Can't execute instruction")]
|
||||||
|
InvalidStateCannotExecuteInstruction,
|
||||||
|
|
||||||
|
/// Can't execute instruction within its hold up time
|
||||||
|
#[error("Can't execute instruction within its hold up time")]
|
||||||
|
CannotExecuteInstructionWithinHoldUpTime,
|
||||||
|
|
||||||
|
/// Instruction already executed
|
||||||
|
#[error("Instruction already executed")]
|
||||||
|
InstructionAlreadyExecuted,
|
||||||
|
|
||||||
|
/// Invalid Instruction index
|
||||||
|
#[error("Invalid Instruction index")]
|
||||||
|
InvalidInstructionIndex,
|
||||||
|
|
||||||
|
/// Instruction hold up time is below the min specified by Governance
|
||||||
|
#[error("Instruction hold up time is below the min specified by Governance")]
|
||||||
|
InstructionHoldUpTimeBelowRequiredMin,
|
||||||
|
|
||||||
|
/// Instruction at the given index for the Proposal already exists
|
||||||
|
#[error("Instruction at the given index for the Proposal already exists")]
|
||||||
|
InstructionAlreadyExists,
|
||||||
|
|
||||||
|
/// Invalid State: Can't sign off
|
||||||
|
#[error("Invalid State: Can't sign off")]
|
||||||
|
InvalidStateCannotSignOff,
|
||||||
|
|
||||||
|
/// Invalid State: Can't vote
|
||||||
|
#[error("Invalid State: Can't vote")]
|
||||||
|
InvalidStateCannotVote,
|
||||||
|
|
||||||
|
/// Invalid State: Can't finalize vote
|
||||||
|
#[error("Invalid State: Can't finalize vote")]
|
||||||
|
InvalidStateCannotFinalize,
|
||||||
|
|
||||||
|
/// Invalid State: Can't cancel Proposal
|
||||||
|
#[error("Invalid State: Can't cancel Proposal")]
|
||||||
|
InvalidStateCannotCancelProposal,
|
||||||
|
|
||||||
|
/// Vote already relinquished
|
||||||
|
#[error("Vote already relinquished")]
|
||||||
|
VoteAlreadyRelinquished,
|
||||||
|
|
||||||
|
/// Can't finalize vote. Voting still in progress
|
||||||
|
#[error("Can't finalize vote. Voting still in progress")]
|
||||||
|
CannotFinalizeVotingInProgress,
|
||||||
|
|
||||||
|
/// Proposal voting time expired
|
||||||
|
#[error("Proposal voting time expired")]
|
||||||
|
ProposalVotingTimeExpired,
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// Proposal does not belong to the given Governance
|
||||||
|
#[error("Proposal does not belong to the given Governance")]
|
||||||
|
InvalidGovernanceForProposal,
|
||||||
|
|
||||||
|
/// Proposal does not belong to given Governing Mint"
|
||||||
|
#[error("Proposal does not belong to given Governing Mint")]
|
||||||
|
InvalidGoverningMintForProposal,
|
||||||
|
|
||||||
|
/// ---- SPL Token Tools Errors ----
|
||||||
|
|
||||||
|
/// Invalid Token account owner
|
||||||
|
#[error("Invalid Token account owner")]
|
||||||
|
SplTokenAccountWithInvalidOwner,
|
||||||
|
|
||||||
|
/// Invalid Mint account owner
|
||||||
|
#[error("Invalid Mint account owner")]
|
||||||
|
SplTokenMintWithInvalidOwner,
|
||||||
|
|
||||||
|
/// Token Account is not initialized
|
||||||
|
#[error("Token Account is not initialized")]
|
||||||
|
SplTokenAccountNotInitialized,
|
||||||
|
|
||||||
|
/// Token account data is invalid
|
||||||
|
#[error("Token account data is invalid")]
|
||||||
|
SplTokenInvalidTokenAccountData,
|
||||||
|
|
||||||
|
/// Token mint account data is invalid
|
||||||
|
#[error("Token mint account data is invalid")]
|
||||||
|
SplTokenInvalidMintAccountData,
|
||||||
|
|
||||||
|
/// Token Mint is not initialized
|
||||||
|
#[error("Token Mint account is not initialized")]
|
||||||
|
SplTokenMintNotInitialized,
|
||||||
|
|
||||||
|
/// ---- 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,843 @@
|
||||||
|
//! Program instructions
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
id,
|
||||||
|
state::{
|
||||||
|
governance::{
|
||||||
|
get_account_governance_address, get_program_governance_address, GovernanceConfig,
|
||||||
|
},
|
||||||
|
proposal::get_proposal_address,
|
||||||
|
proposal_instruction::{get_proposal_instruction_address, InstructionData},
|
||||||
|
realm::{get_governing_token_holding_address, get_realm_address},
|
||||||
|
signatory_record::get_signatory_record_address,
|
||||||
|
token_owner_record::get_token_owner_record_address,
|
||||||
|
vote_record::get_vote_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. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 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. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 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. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 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
|
||||||
|
RemoveSignatory {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Signatory to remove from the Proposal
|
||||||
|
signatory: Pubkey,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Inserts an instruction for the Proposal at the given index position
|
||||||
|
/// New Instructions must be inserted at the end of the range indicated by Proposal instructions_next_index
|
||||||
|
/// If an Instruction replaces an existing Instruction at a given index then the old one must be removed using RemoveInstruction first
|
||||||
|
|
||||||
|
/// 0. `[]` Governance account
|
||||||
|
/// 1. `[writable]` Proposal account
|
||||||
|
/// 2. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||||
|
/// 4. `[writable]` ProposalInstruction account. PDA seeds: ['governance',proposal,index]
|
||||||
|
/// 5. `[signer]` Payer
|
||||||
|
/// 6. `[]` System program
|
||||||
|
/// 7. `[]` Clock sysvar
|
||||||
|
InsertInstruction {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Instruction index to be inserted at.
|
||||||
|
index: u16,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Slot waiting time between vote period ending and this being eligible for execution
|
||||||
|
hold_up_time: u64,
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Instruction Data
|
||||||
|
instruction: InstructionData,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Removes instruction from the Proposal
|
||||||
|
///
|
||||||
|
/// 0. `[writable]` Proposal account
|
||||||
|
/// 1. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 2. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||||
|
/// 3. `[writable]` ProposalInstruction account
|
||||||
|
/// 4. `[writable]` Beneficiary Account which would receive lamports from the disposed ProposalInstruction account
|
||||||
|
RemoveInstruction,
|
||||||
|
|
||||||
|
/// Cancels Proposal by changing its state to Canceled
|
||||||
|
///
|
||||||
|
/// 0. `[writable]` Proposal account
|
||||||
|
/// 1. `[]` TokenOwnerRecord account for Proposal owner
|
||||||
|
/// 2 `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||||
|
/// 3. `[]` Clock sysvar
|
||||||
|
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. `[]` Governance account
|
||||||
|
/// 1. `[writable]` Proposal account
|
||||||
|
/// 2. `[writable]` Token Owner Record account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||||
|
/// 3. `[signer]` Governance Authority (Token Owner or Governance Delegate)
|
||||||
|
/// 4. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record]
|
||||||
|
/// 5. `[]` Governing Token Mint
|
||||||
|
/// 6. `[signer]` Payer
|
||||||
|
/// 7. `[]` System program
|
||||||
|
/// 8. `[]` Rent sysvar
|
||||||
|
/// 9. `[]` Clock sysvar
|
||||||
|
CastVote {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
/// Yes/No vote
|
||||||
|
vote: Vote,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Finalizes vote in case the Vote was not automatically tipped within max_voting_time period
|
||||||
|
///
|
||||||
|
/// 0. `[]` Governance account
|
||||||
|
/// 1. `[writable]` Proposal account
|
||||||
|
/// 2. `[]` Governing Token Mint
|
||||||
|
/// 3. `[]` Clock sysvar
|
||||||
|
FinalizeVote {},
|
||||||
|
|
||||||
|
/// 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. `[]` Governance account
|
||||||
|
/// 1. `[writable]` Proposal account
|
||||||
|
/// 2. `[writable]` TokenOwnerRecord account. PDA seeds: ['governance',realm, governing_token_mint, governing_token_owner]
|
||||||
|
/// 3. `[writable]` Proposal VoteRecord account. PDA seeds: ['governance',proposal,governing_token_owner_record]
|
||||||
|
/// 4. `[]` Governing Token Mint
|
||||||
|
|
||||||
|
/// 5. `[signer]` Optional Governance Authority (Token Owner or Governance Delegate)
|
||||||
|
/// It's required only when Proposal is still being voted on
|
||||||
|
/// 6. `[writable]` Optional Beneficiary account which would receive lamports when VoteRecord Account is disposed
|
||||||
|
/// It's required only when Proposal is still being voted on
|
||||||
|
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 the Proposal belongs to
|
||||||
|
/// For example to execute Program upgrade the ProgramGovernance PDA would be used as the singer
|
||||||
|
///
|
||||||
|
/// 0. `[writable]` Proposal account
|
||||||
|
/// 1. `[writable]` ProposalInstruction account you wish to execute
|
||||||
|
/// 2. `[]` Clock sysvar
|
||||||
|
/// 3+ Any extra accounts that are part of the instruction, in order
|
||||||
|
ExecuteInstruction,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 token_owner_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(token_owner_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 token_owner_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(token_owner_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: u32,
|
||||||
|
) -> 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),
|
||||||
|
];
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates CastVote instruction
|
||||||
|
pub fn cast_vote(
|
||||||
|
// Accounts
|
||||||
|
governance: &Pubkey,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
token_owner_record: &Pubkey,
|
||||||
|
governance_authority: &Pubkey,
|
||||||
|
governing_token_mint: &Pubkey,
|
||||||
|
payer: &Pubkey,
|
||||||
|
// Args
|
||||||
|
vote: Vote,
|
||||||
|
) -> Instruction {
|
||||||
|
let vote_record_address = get_vote_record_address(&proposal, &token_owner_record);
|
||||||
|
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*governance, false),
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new(*token_owner_record, false),
|
||||||
|
AccountMeta::new_readonly(*governance_authority, true),
|
||||||
|
AccountMeta::new(vote_record_address, false),
|
||||||
|
AccountMeta::new_readonly(*governing_token_mint, false),
|
||||||
|
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::CastVote { vote };
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates FinalizeVote instruction
|
||||||
|
pub fn finalize_vote(
|
||||||
|
// Accounts
|
||||||
|
governance: &Pubkey,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
governing_token_mint: &Pubkey,
|
||||||
|
) -> Instruction {
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*governance, false),
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new_readonly(*governing_token_mint, false),
|
||||||
|
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::FinalizeVote {};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates RelinquishVote instruction
|
||||||
|
pub fn relinquish_vote(
|
||||||
|
// Accounts
|
||||||
|
governance: &Pubkey,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
token_owner_record: &Pubkey,
|
||||||
|
governing_token_mint: &Pubkey,
|
||||||
|
governance_authority: Option<Pubkey>,
|
||||||
|
beneficiary: Option<Pubkey>,
|
||||||
|
) -> Instruction {
|
||||||
|
let vote_record_address = get_vote_record_address(&proposal, &token_owner_record);
|
||||||
|
|
||||||
|
let mut accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*governance, false),
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new(*token_owner_record, false),
|
||||||
|
AccountMeta::new(vote_record_address, false),
|
||||||
|
AccountMeta::new_readonly(*governing_token_mint, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
if let Some(governance_authority) = governance_authority {
|
||||||
|
accounts.push(AccountMeta::new_readonly(governance_authority, true));
|
||||||
|
accounts.push(AccountMeta::new(beneficiary.unwrap(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::RelinquishVote {};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates CancelProposal instruction
|
||||||
|
pub fn cancel_proposal(
|
||||||
|
// Accounts
|
||||||
|
proposal: &Pubkey,
|
||||||
|
token_owner_record: &Pubkey,
|
||||||
|
governance_authority: &Pubkey,
|
||||||
|
) -> Instruction {
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new_readonly(*token_owner_record, false),
|
||||||
|
AccountMeta::new_readonly(*governance_authority, true),
|
||||||
|
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::CancelProposal {};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates InsertInstruction instruction
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn insert_instruction(
|
||||||
|
// Accounts
|
||||||
|
governance: &Pubkey,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
token_owner_record: &Pubkey,
|
||||||
|
governance_authority: &Pubkey,
|
||||||
|
payer: &Pubkey,
|
||||||
|
// Args
|
||||||
|
index: u16,
|
||||||
|
hold_up_time: u64,
|
||||||
|
instruction: InstructionData,
|
||||||
|
) -> Instruction {
|
||||||
|
let proposal_instruction_address =
|
||||||
|
get_proposal_instruction_address(&proposal, &index.to_le_bytes());
|
||||||
|
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*governance, false),
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new_readonly(*token_owner_record, false),
|
||||||
|
AccountMeta::new_readonly(*governance_authority, true),
|
||||||
|
AccountMeta::new(proposal_instruction_address, false),
|
||||||
|
AccountMeta::new_readonly(*payer, true),
|
||||||
|
AccountMeta::new_readonly(system_program::id(), false),
|
||||||
|
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::InsertInstruction {
|
||||||
|
index,
|
||||||
|
hold_up_time,
|
||||||
|
instruction,
|
||||||
|
};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates RemoveInstruction instruction
|
||||||
|
pub fn remove_instruction(
|
||||||
|
// Accounts
|
||||||
|
proposal: &Pubkey,
|
||||||
|
token_owner_record: &Pubkey,
|
||||||
|
governance_authority: &Pubkey,
|
||||||
|
proposal_instruction: &Pubkey,
|
||||||
|
beneficiary: &Pubkey,
|
||||||
|
) -> Instruction {
|
||||||
|
let accounts = vec![
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new_readonly(*token_owner_record, false),
|
||||||
|
AccountMeta::new_readonly(*governance_authority, true),
|
||||||
|
AccountMeta::new(*proposal_instruction, false),
|
||||||
|
AccountMeta::new(*beneficiary, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::RemoveInstruction {};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates ExecuteInstruction instruction
|
||||||
|
pub fn execute_instruction(
|
||||||
|
// Accounts
|
||||||
|
governance: &Pubkey,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
proposal_instruction: &Pubkey,
|
||||||
|
instruction_program_id: &Pubkey,
|
||||||
|
instruction_accounts: &[AccountMeta],
|
||||||
|
) -> Instruction {
|
||||||
|
let mut accounts = vec![
|
||||||
|
AccountMeta::new_readonly(*governance, false),
|
||||||
|
AccountMeta::new(*proposal, false),
|
||||||
|
AccountMeta::new(*proposal_instruction, false),
|
||||||
|
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||||
|
AccountMeta::new_readonly(*instruction_program_id, false),
|
||||||
|
];
|
||||||
|
|
||||||
|
accounts.extend_from_slice(instruction_accounts);
|
||||||
|
|
||||||
|
let instruction = GovernanceInstruction::ExecuteInstruction {};
|
||||||
|
|
||||||
|
Instruction {
|
||||||
|
program_id: id(),
|
||||||
|
accounts,
|
||||||
|
data: instruction.try_to_vec().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
#![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";
|
|
@ -0,0 +1,144 @@
|
||||||
|
//! Program processor
|
||||||
|
|
||||||
|
mod process_add_signatory;
|
||||||
|
mod process_cancel_proposal;
|
||||||
|
mod process_cast_vote;
|
||||||
|
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_execute_instruction;
|
||||||
|
mod process_finalize_vote;
|
||||||
|
mod process_insert_instruction;
|
||||||
|
mod process_relinquish_vote;
|
||||||
|
mod process_remove_instruction;
|
||||||
|
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_cancel_proposal::*;
|
||||||
|
use process_cast_vote::*;
|
||||||
|
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_execute_instruction::*;
|
||||||
|
use process_finalize_vote::*;
|
||||||
|
use process_insert_instruction::*;
|
||||||
|
use process_relinquish_vote::*;
|
||||||
|
use process_remove_instruction::*;
|
||||||
|
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)?;
|
||||||
|
|
||||||
|
if let GovernanceInstruction::InsertInstruction {
|
||||||
|
index,
|
||||||
|
hold_up_time,
|
||||||
|
instruction: _,
|
||||||
|
} = instruction
|
||||||
|
{
|
||||||
|
// Do not dump instruction data into logs
|
||||||
|
msg!(
|
||||||
|
"GOVERNANCE-INSTRUCTION: InsertInstruction {{ index: {:?}, hold_up_time: {:?} }}",
|
||||||
|
index,
|
||||||
|
hold_up_time
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
GovernanceInstruction::CastVote { vote } => process_cast_vote(program_id, accounts, vote),
|
||||||
|
|
||||||
|
GovernanceInstruction::FinalizeVote {} => process_finalize_vote(program_id, accounts),
|
||||||
|
|
||||||
|
GovernanceInstruction::RelinquishVote {} => process_relinquish_vote(program_id, accounts),
|
||||||
|
|
||||||
|
GovernanceInstruction::CancelProposal {} => process_cancel_proposal(program_id, accounts),
|
||||||
|
|
||||||
|
GovernanceInstruction::InsertInstruction {
|
||||||
|
index,
|
||||||
|
hold_up_time,
|
||||||
|
instruction,
|
||||||
|
} => process_insert_instruction(program_id, accounts, index, hold_up_time, instruction),
|
||||||
|
|
||||||
|
GovernanceInstruction::RemoveInstruction {} => {
|
||||||
|
process_remove_instruction(program_id, accounts)
|
||||||
|
}
|
||||||
|
GovernanceInstruction::ExecuteInstruction {} => {
|
||||||
|
process_execute_instruction(program_id, accounts)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
//! 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::get_proposal_data,
|
||||||
|
signatory_record::{get_signatory_record_address_seeds, SignatoryRecord},
|
||||||
|
token_owner_record::get_token_owner_record_data_for_proposal_owner,
|
||||||
|
},
|
||||||
|
tools::account::create_and_serialize_account_signed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 = get_proposal_data(proposal_info)?;
|
||||||
|
proposal_data.assert_can_edit_signatories()?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
|
||||||
|
token_owner_record_info,
|
||||||
|
&proposal_data.token_owner_record,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(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(())
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
//! 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::get_proposal_data,
|
||||||
|
token_owner_record::get_token_owner_record_data_for_proposal_owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Processes CancelProposal instruction
|
||||||
|
pub fn process_cancel_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 token_owner_record_info = next_account_info(account_info_iter)?; // 1
|
||||||
|
let governance_authority_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 = get_proposal_data(proposal_info)?;
|
||||||
|
proposal_data.assert_can_cancel()?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
|
||||||
|
token_owner_record_info,
|
||||||
|
&proposal_data.token_owner_record,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
proposal_data.state = ProposalState::Cancelled;
|
||||||
|
proposal_data.closed_at = Some(clock.slot);
|
||||||
|
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
clock::Clock,
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
rent::Rent,
|
||||||
|
sysvar::Sysvar,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::GovernanceError,
|
||||||
|
instruction::Vote,
|
||||||
|
state::{
|
||||||
|
enums::{GovernanceAccountType, VoteWeight},
|
||||||
|
governance::get_governance_data,
|
||||||
|
proposal::get_proposal_data_for_governance_and_governing_mint,
|
||||||
|
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
|
||||||
|
vote_record::{get_vote_record_address_seeds, VoteRecord},
|
||||||
|
},
|
||||||
|
tools::{account::create_and_serialize_account_signed, spl_token::get_spl_token_mint_supply},
|
||||||
|
};
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
|
||||||
|
/// Processes CastVote instruction
|
||||||
|
pub fn process_cast_vote(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
vote: Vote,
|
||||||
|
) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let governance_info = next_account_info(account_info_iter)?; // 0
|
||||||
|
let proposal_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 vote_record_info = next_account_info(account_info_iter)?; // 4
|
||||||
|
let governing_token_mint_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 rent_sysvar_info = next_account_info(account_info_iter)?; // 8
|
||||||
|
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||||
|
|
||||||
|
let clock_info = next_account_info(account_info_iter)?; // 9
|
||||||
|
let clock = Clock::from_account_info(clock_info)?;
|
||||||
|
|
||||||
|
if !vote_record_info.data_is_empty() {
|
||||||
|
return Err(GovernanceError::VoteAlreadyExists.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let governance_data = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
|
||||||
|
&proposal_info,
|
||||||
|
governance_info.key,
|
||||||
|
governing_token_mint_info.key,
|
||||||
|
)?;
|
||||||
|
proposal_data.assert_can_cast_vote(&governance_data.config, clock.slot)?;
|
||||||
|
|
||||||
|
let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
|
||||||
|
&token_owner_record_info,
|
||||||
|
&governance_data.config.realm,
|
||||||
|
governing_token_mint_info.key,
|
||||||
|
)?;
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
// Update TokenOwnerRecord vote counts
|
||||||
|
token_owner_record_data.unrelinquished_votes_count = token_owner_record_data
|
||||||
|
.unrelinquished_votes_count
|
||||||
|
.checked_add(1)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
token_owner_record_data.total_votes_count = token_owner_record_data
|
||||||
|
.total_votes_count
|
||||||
|
.checked_add(1)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
let vote_amount = token_owner_record_data.governing_token_deposit_amount;
|
||||||
|
|
||||||
|
// Calculate Proposal voting weights
|
||||||
|
let vote_weight = match vote {
|
||||||
|
Vote::Yes => {
|
||||||
|
proposal_data.yes_votes_count = proposal_data
|
||||||
|
.yes_votes_count
|
||||||
|
.checked_add(vote_amount)
|
||||||
|
.unwrap();
|
||||||
|
VoteWeight::Yes(vote_amount)
|
||||||
|
}
|
||||||
|
Vote::No => {
|
||||||
|
proposal_data.no_votes_count = proposal_data
|
||||||
|
.no_votes_count
|
||||||
|
.checked_add(vote_amount)
|
||||||
|
.unwrap();
|
||||||
|
VoteWeight::No(vote_amount)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let governing_token_supply = get_spl_token_mint_supply(&governing_token_mint_info)?;
|
||||||
|
proposal_data.try_tip_vote(governing_token_supply, &governance_data.config, clock.slot);
|
||||||
|
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
// Create and serialize VoteRecord
|
||||||
|
let vote_record_data = VoteRecord {
|
||||||
|
account_type: GovernanceAccountType::VoteRecord,
|
||||||
|
proposal: *proposal_info.key,
|
||||||
|
governing_token_owner: token_owner_record_data.governing_token_owner,
|
||||||
|
vote_weight,
|
||||||
|
is_relinquished: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
create_and_serialize_account_signed::<VoteRecord>(
|
||||||
|
payer_info,
|
||||||
|
vote_record_info,
|
||||||
|
&vote_record_data,
|
||||||
|
&get_vote_record_address_seeds(proposal_info.key, token_owner_record_info.key),
|
||||||
|
program_id,
|
||||||
|
system_info,
|
||||||
|
rent,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
//! 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(())
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
//! 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(())
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
//! 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::get_governance_data,
|
||||||
|
proposal::{get_proposal_address_seeds, Proposal},
|
||||||
|
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
|
||||||
|
},
|
||||||
|
tools::account::create_and_serialize_account_signed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_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
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(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,
|
||||||
|
|
||||||
|
instructions_executed_count: 0,
|
||||||
|
instructions_count: 0,
|
||||||
|
instructions_next_index: 0,
|
||||||
|
|
||||||
|
yes_votes_count: 0,
|
||||||
|
no_votes_count: 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(())
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
//! 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, spl_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(())
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
//! 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::get_realm_data,
|
||||||
|
token_owner_record::{
|
||||||
|
get_token_owner_record_address_seeds, get_token_owner_record_data_for_seeds,
|
||||||
|
TokenOwnerRecord,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools::{
|
||||||
|
account::create_and_serialize_account_signed,
|
||||||
|
spl_token::{
|
||||||
|
get_spl_token_amount, get_spl_token_mint, get_spl_token_owner, 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 = get_realm_data(realm_info)?;
|
||||||
|
let governing_token_mint = get_spl_token_mint(governing_token_holding_info)?;
|
||||||
|
|
||||||
|
realm_data.assert_is_valid_governing_token_mint(&governing_token_mint)?;
|
||||||
|
|
||||||
|
let amount = get_spl_token_amount(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_spl_token_owner(&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,
|
||||||
|
unrelinquished_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 = get_token_owner_record_data_for_seeds(
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
clock::Clock,
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
instruction::Instruction,
|
||||||
|
program::invoke_signed,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
sysvar::Sysvar,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::{
|
||||||
|
enums::ProposalState, governance::get_governance_data,
|
||||||
|
proposal::get_proposal_data_for_governance,
|
||||||
|
proposal_instruction::get_proposal_instruction_data_for_proposal,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Processes ExecuteInstruction instruction
|
||||||
|
pub fn process_execute_instruction(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let governance_info = next_account_info(account_info_iter)?; // 0
|
||||||
|
let proposal_info = next_account_info(account_info_iter)?; // 1
|
||||||
|
let proposal_instruction_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 governance_data = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data_for_governance(proposal_info, governance_info.key)?;
|
||||||
|
|
||||||
|
let mut proposal_instruction_data =
|
||||||
|
get_proposal_instruction_data_for_proposal(proposal_instruction_info, proposal_info.key)?;
|
||||||
|
|
||||||
|
proposal_data.assert_can_execute_instruction(&proposal_instruction_data, clock.slot)?;
|
||||||
|
|
||||||
|
// Execute instruction with Governance PDA as signer
|
||||||
|
let instruction = Instruction::from(&proposal_instruction_data.instruction);
|
||||||
|
|
||||||
|
let instruction_account_infos = account_info_iter.as_slice();
|
||||||
|
|
||||||
|
let mut governance_seeds = governance_data.get_governance_address_seeds()?.to_vec();
|
||||||
|
let (_, bump_seed) = Pubkey::find_program_address(&governance_seeds, program_id);
|
||||||
|
let bump = &[bump_seed];
|
||||||
|
governance_seeds.push(bump);
|
||||||
|
|
||||||
|
invoke_signed(
|
||||||
|
&instruction,
|
||||||
|
&instruction_account_infos,
|
||||||
|
&[&governance_seeds[..]],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Update proposal and instruction accounts
|
||||||
|
if proposal_data.state == ProposalState::Succeeded {
|
||||||
|
proposal_data.executing_at = Some(clock.slot);
|
||||||
|
proposal_data.state = ProposalState::Executing;
|
||||||
|
}
|
||||||
|
|
||||||
|
proposal_data.instructions_executed_count = proposal_data
|
||||||
|
.instructions_executed_count
|
||||||
|
.checked_add(1)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if proposal_data.state == ProposalState::Executing
|
||||||
|
&& proposal_data.instructions_executed_count == proposal_data.instructions_count
|
||||||
|
{
|
||||||
|
proposal_data.closed_at = Some(clock.slot);
|
||||||
|
proposal_data.state = ProposalState::Completed;
|
||||||
|
}
|
||||||
|
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
proposal_instruction_data.executed_at = Some(clock.slot);
|
||||||
|
proposal_instruction_data.serialize(&mut *proposal_instruction_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
clock::Clock,
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
sysvar::Sysvar,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
state::{
|
||||||
|
governance::get_governance_data,
|
||||||
|
proposal::get_proposal_data_for_governance_and_governing_mint,
|
||||||
|
},
|
||||||
|
tools::spl_token::get_spl_token_mint_supply,
|
||||||
|
};
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
|
||||||
|
/// Processes FinalizeVote instruction
|
||||||
|
pub fn process_finalize_vote(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let governance_info = next_account_info(account_info_iter)?; // 0
|
||||||
|
let proposal_info = next_account_info(account_info_iter)?; // 1
|
||||||
|
|
||||||
|
let governing_token_mint_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 governance_data = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
|
||||||
|
&proposal_info,
|
||||||
|
governance_info.key,
|
||||||
|
governing_token_mint_info.key,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let governing_token_supply = get_spl_token_mint_supply(&governing_token_mint_info)?;
|
||||||
|
|
||||||
|
proposal_data.finalize_vote(governing_token_supply, &governance_data.config, clock.slot)?;
|
||||||
|
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
|
||||||
|
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,
|
||||||
|
governance::get_governance_data,
|
||||||
|
proposal::get_proposal_data_for_governance,
|
||||||
|
proposal_instruction::{
|
||||||
|
get_proposal_instruction_address_seeds, InstructionData, ProposalInstruction,
|
||||||
|
},
|
||||||
|
token_owner_record::get_token_owner_record_data_for_proposal_owner,
|
||||||
|
},
|
||||||
|
tools::account::create_and_serialize_account_signed,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Processes InsertInstruction instruction
|
||||||
|
pub fn process_insert_instruction(
|
||||||
|
program_id: &Pubkey,
|
||||||
|
accounts: &[AccountInfo],
|
||||||
|
index: u16,
|
||||||
|
hold_up_time: u64,
|
||||||
|
instruction: InstructionData,
|
||||||
|
) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let governance_info = next_account_info(account_info_iter)?; // 0
|
||||||
|
let proposal_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 proposal_instruction_info = next_account_info(account_info_iter)?; // 4
|
||||||
|
|
||||||
|
let payer_info = next_account_info(account_info_iter)?; // 5
|
||||||
|
let system_info = next_account_info(account_info_iter)?; // 6
|
||||||
|
|
||||||
|
let rent_sysvar_info = next_account_info(account_info_iter)?; // 7
|
||||||
|
let rent = &Rent::from_account_info(rent_sysvar_info)?;
|
||||||
|
|
||||||
|
if !proposal_instruction_info.data_is_empty() {
|
||||||
|
return Err(GovernanceError::InstructionAlreadyExists.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let governance_data = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
if hold_up_time < governance_data.config.min_instruction_hold_up_time {
|
||||||
|
return Err(GovernanceError::InstructionHoldUpTimeBelowRequiredMin.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data_for_governance(&proposal_info, governance_info.key)?;
|
||||||
|
proposal_data.assert_can_edit_instructions()?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
|
||||||
|
token_owner_record_info,
|
||||||
|
&proposal_data.token_owner_record,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
match index.cmp(&proposal_data.instructions_next_index) {
|
||||||
|
Ordering::Greater => return Err(GovernanceError::InvalidInstructionIndex.into()),
|
||||||
|
// If the index is the same as instructions_next_index then we are adding a new instruction
|
||||||
|
// If the index is below instructions_next_index then we are inserting into an existing empty slot
|
||||||
|
Ordering::Equal => {
|
||||||
|
proposal_data.instructions_next_index = proposal_data
|
||||||
|
.instructions_next_index
|
||||||
|
.checked_add(1)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
Ordering::Less => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
proposal_data.instructions_count = proposal_data.instructions_count.checked_add(1).unwrap();
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
let proposal_instruction_data = ProposalInstruction {
|
||||||
|
account_type: GovernanceAccountType::ProposalInstruction,
|
||||||
|
hold_up_time,
|
||||||
|
instruction,
|
||||||
|
executed_at: None,
|
||||||
|
proposal: *proposal_info.key,
|
||||||
|
};
|
||||||
|
|
||||||
|
create_and_serialize_account_signed::<ProposalInstruction>(
|
||||||
|
payer_info,
|
||||||
|
proposal_instruction_info,
|
||||||
|
&proposal_instruction_data,
|
||||||
|
&get_proposal_instruction_address_seeds(proposal_info.key, &index.to_le_bytes()),
|
||||||
|
program_id,
|
||||||
|
system_info,
|
||||||
|
rent,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
state::{
|
||||||
|
enums::{ProposalState, VoteWeight},
|
||||||
|
governance::get_governance_data,
|
||||||
|
proposal::get_proposal_data_for_governance_and_governing_mint,
|
||||||
|
token_owner_record::get_token_owner_record_data_for_realm_and_governing_mint,
|
||||||
|
vote_record::get_vote_record_data_for_proposal_and_token_owner,
|
||||||
|
},
|
||||||
|
tools::account::dispose_account,
|
||||||
|
};
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
|
||||||
|
/// Processes RelinquishVote instruction
|
||||||
|
pub fn process_relinquish_vote(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
|
||||||
|
let account_info_iter = &mut accounts.iter();
|
||||||
|
|
||||||
|
let governance_info = next_account_info(account_info_iter)?; // 0
|
||||||
|
let proposal_info = next_account_info(account_info_iter)?; // 1
|
||||||
|
let token_owner_record_info = next_account_info(account_info_iter)?; // 2
|
||||||
|
|
||||||
|
let vote_record_info = next_account_info(account_info_iter)?; // 3
|
||||||
|
let governing_token_mint_info = next_account_info(account_info_iter)?; // 4
|
||||||
|
|
||||||
|
let governance_data = get_governance_data(governance_info)?;
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data_for_governance_and_governing_mint(
|
||||||
|
&proposal_info,
|
||||||
|
governance_info.key,
|
||||||
|
governing_token_mint_info.key,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut token_owner_record_data = get_token_owner_record_data_for_realm_and_governing_mint(
|
||||||
|
&token_owner_record_info,
|
||||||
|
&governance_data.config.realm,
|
||||||
|
governing_token_mint_info.key,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut vote_record_data = get_vote_record_data_for_proposal_and_token_owner(
|
||||||
|
vote_record_info,
|
||||||
|
proposal_info.key,
|
||||||
|
&token_owner_record_data.governing_token_owner,
|
||||||
|
)?;
|
||||||
|
vote_record_data.assert_can_relinquish_vote()?;
|
||||||
|
|
||||||
|
// If the Proposal is still being voted on then the token owner vote won't count towards the outcome
|
||||||
|
if proposal_data.state == ProposalState::Voting {
|
||||||
|
let governance_authority_info = next_account_info(account_info_iter)?; // 5
|
||||||
|
let beneficiary_info = next_account_info(account_info_iter)?; // 6
|
||||||
|
|
||||||
|
// Note: It's only required to sign by governing_authority if relinquishing the vote results in vote change
|
||||||
|
// If the Proposal is already decided then anybody can prune active votes for token owner
|
||||||
|
token_owner_record_data
|
||||||
|
.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
match vote_record_data.vote_weight {
|
||||||
|
VoteWeight::Yes(vote_amount) => {
|
||||||
|
proposal_data.yes_votes_count = proposal_data
|
||||||
|
.yes_votes_count
|
||||||
|
.checked_sub(vote_amount)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
VoteWeight::No(vote_amount) => {
|
||||||
|
proposal_data.no_votes_count = proposal_data
|
||||||
|
.no_votes_count
|
||||||
|
.checked_sub(vote_amount)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
dispose_account(vote_record_info, beneficiary_info);
|
||||||
|
|
||||||
|
token_owner_record_data.total_votes_count = token_owner_record_data
|
||||||
|
.total_votes_count
|
||||||
|
.checked_sub(1)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
vote_record_data.is_relinquished = true;
|
||||||
|
vote_record_data.serialize(&mut *vote_record_info.data.borrow_mut())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the Proposal has been already voted on then we only have to decrease unrelinquished_votes_count
|
||||||
|
token_owner_record_data.unrelinquished_votes_count = token_owner_record_data
|
||||||
|
.unrelinquished_votes_count
|
||||||
|
.checked_sub(1)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
token_owner_record_data.serialize(&mut *token_owner_record_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
state::{
|
||||||
|
proposal::get_proposal_data,
|
||||||
|
proposal_instruction::assert_proposal_instruction_for_proposal,
|
||||||
|
token_owner_record::get_token_owner_record_data_for_proposal_owner,
|
||||||
|
},
|
||||||
|
tools::account::dispose_account,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Processes RemoveInstruction instruction
|
||||||
|
pub fn process_remove_instruction(_program_id: &Pubkey, accounts: &[AccountInfo]) -> 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 proposal_instruction_info = next_account_info(account_info_iter)?; // 3
|
||||||
|
|
||||||
|
let beneficiary_info = next_account_info(account_info_iter)?; // 4
|
||||||
|
|
||||||
|
let mut proposal_data = get_proposal_data(&proposal_info)?;
|
||||||
|
proposal_data.assert_can_edit_instructions()?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
|
||||||
|
token_owner_record_info,
|
||||||
|
&proposal_data.token_owner_record,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
assert_proposal_instruction_for_proposal(proposal_instruction_info, proposal_info.key)?;
|
||||||
|
|
||||||
|
dispose_account(proposal_instruction_info, beneficiary_info);
|
||||||
|
|
||||||
|
proposal_data.instructions_count = proposal_data.instructions_count.checked_sub(1).unwrap();
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
//! Program state processor
|
||||||
|
|
||||||
|
use borsh::BorshSerialize;
|
||||||
|
use solana_program::{
|
||||||
|
account_info::{next_account_info, AccountInfo},
|
||||||
|
entrypoint::ProgramResult,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
state::{
|
||||||
|
proposal::get_proposal_data, signatory_record::get_signatory_record_data_for_seeds,
|
||||||
|
token_owner_record::get_token_owner_record_data_for_proposal_owner,
|
||||||
|
},
|
||||||
|
tools::account::dispose_account,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 mut proposal_data = get_proposal_data(proposal_info)?;
|
||||||
|
proposal_data.assert_can_edit_signatories()?;
|
||||||
|
|
||||||
|
let token_owner_record_data = get_token_owner_record_data_for_proposal_owner(
|
||||||
|
token_owner_record_info,
|
||||||
|
&proposal_data.token_owner_record,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(governance_authority_info)?;
|
||||||
|
|
||||||
|
let signatory_record_data =
|
||||||
|
get_signatory_record_data_for_seeds(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();
|
||||||
|
|
||||||
|
proposal_data.serialize(&mut *proposal_info.data.borrow_mut())?;
|
||||||
|
|
||||||
|
dispose_account(signatory_record_info, beneficiary_info);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
//! 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::get_token_owner_record_data;
|
||||||
|
|
||||||
|
/// 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 = get_token_owner_record_data(token_owner_record_info)?;
|
||||||
|
|
||||||
|
token_owner_record_data.assert_token_owner_or_delegate_is_signer(&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(())
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
//! 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::get_proposal_data,
|
||||||
|
signatory_record::get_signatory_record_data_for_seeds,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 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 = get_proposal_data(proposal_info)?;
|
||||||
|
proposal_data.assert_can_sign_off()?;
|
||||||
|
|
||||||
|
let mut signatory_record_data = get_signatory_record_data_for_seeds(
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
//! 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::{get_realm_address_seeds, get_realm_data},
|
||||||
|
token_owner_record::{
|
||||||
|
get_token_owner_record_address_seeds, get_token_owner_record_data_for_seeds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tools::spl_token::{get_spl_token_mint, 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 = get_realm_data(realm_info)?;
|
||||||
|
let governing_token_mint = get_spl_token_mint(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 = get_token_owner_record_data_for_seeds(
|
||||||
|
token_owner_record_info,
|
||||||
|
&token_owner_record_address_seeds,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if token_owner_record_data.unrelinquished_votes_count > 0 {
|
||||||
|
return Err(GovernanceError::AllVotesMustBeRelinquishedToWithdrawGoverningTokens.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(())
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
//! 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
|
||||||
|
VoteRecord,
|
||||||
|
|
||||||
|
/// ProposalInstruction account which holds an instruction to execute for Proposal
|
||||||
|
ProposalInstruction,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
//! Governance Account
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::GovernanceError, id, state::enums::GovernanceAccountType,
|
||||||
|
tools::account::get_account_data, tools::account::AccountMaxSize,
|
||||||
|
};
|
||||||
|
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||||
|
use solana_program::{
|
||||||
|
account_info::AccountInfo, program_error::ProgramError, program_pack::IsInitialized,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::state::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 of Yes votes in % required to tip the vote
|
||||||
|
/// It's the percentage of tokens out of the entire pool of governance tokens eligible to vote
|
||||||
|
// Note: If the threshold is below or equal to 50% then an even split of votes ex: 50:50 or 40:40 is always resolved as Defeated
|
||||||
|
// In other words +1 vote tie breaker is required to have successful vote
|
||||||
|
pub yes_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: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountMaxSize for Governance {}
|
||||||
|
|
||||||
|
impl IsInitialized for Governance {
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.account_type == GovernanceAccountType::AccountGovernance
|
||||||
|
|| self.account_type == GovernanceAccountType::ProgramGovernance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Governance {
|
||||||
|
/// Returns Governance PDA seeds
|
||||||
|
pub fn get_governance_address_seeds(&self) -> Result<[&[u8]; 3], ProgramError> {
|
||||||
|
let seeds = match self.account_type {
|
||||||
|
GovernanceAccountType::AccountGovernance => get_account_governance_address_seeds(
|
||||||
|
&self.config.realm,
|
||||||
|
&self.config.governed_account,
|
||||||
|
),
|
||||||
|
GovernanceAccountType::ProgramGovernance => get_program_governance_address_seeds(
|
||||||
|
&self.config.realm,
|
||||||
|
&self.config.governed_account,
|
||||||
|
),
|
||||||
|
_ => return Err(GovernanceError::InvalidAccountType.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(seeds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes account and checks owner program
|
||||||
|
pub fn get_governance_data(governance_info: &AccountInfo) -> Result<Governance, ProgramError> {
|
||||||
|
get_account_data::<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.yes_vote_threshold_percentage < 1
|
||||||
|
|| governance_config.yes_vote_threshold_percentage > 100
|
||||||
|
{
|
||||||
|
return Err(GovernanceError::InvalidGovernanceConfig.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
//! Program accounts
|
||||||
|
|
||||||
|
pub mod enums;
|
||||||
|
pub mod governance;
|
||||||
|
pub mod proposal;
|
||||||
|
pub mod proposal_instruction;
|
||||||
|
pub mod realm;
|
||||||
|
pub mod signatory_record;
|
||||||
|
pub mod token_owner_record;
|
||||||
|
pub mod vote_record;
|
|
@ -0,0 +1,999 @@
|
||||||
|
//! Proposal Account
|
||||||
|
|
||||||
|
use solana_program::{
|
||||||
|
account_info::AccountInfo, epoch_schedule::Slot, program_error::ProgramError,
|
||||||
|
program_pack::IsInitialized, pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::tools::account::get_account_data;
|
||||||
|
use crate::{error::GovernanceError, id, tools::account::AccountMaxSize, PROGRAM_AUTHORITY_SEED};
|
||||||
|
|
||||||
|
use crate::state::enums::{GovernanceAccountType, ProposalState};
|
||||||
|
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||||
|
|
||||||
|
use crate::state::governance::GovernanceConfig;
|
||||||
|
|
||||||
|
use crate::state::proposal_instruction::ProposalInstruction;
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
|
||||||
|
/// The number of Yes votes
|
||||||
|
pub yes_votes_count: u64,
|
||||||
|
|
||||||
|
/// The number of No votes
|
||||||
|
pub no_votes_count: u64,
|
||||||
|
|
||||||
|
/// 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 instructions_executed_count: u16,
|
||||||
|
|
||||||
|
/// The number of instructions included in the proposal
|
||||||
|
pub instructions_count: u16,
|
||||||
|
|
||||||
|
/// The index of the the next instruction to be added
|
||||||
|
pub instructions_next_index: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountMaxSize for Proposal {
|
||||||
|
fn get_max_size(&self) -> Option<usize> {
|
||||||
|
Some(self.name.len() + self.description_link.len() + 183)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
self.assert_is_draft_state()
|
||||||
|
.map_err(|_| GovernanceError::InvalidStateCannotEditSignatories.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Proposal can be singed off
|
||||||
|
pub fn assert_can_sign_off(&self) -> Result<(), ProgramError> {
|
||||||
|
match self.state {
|
||||||
|
ProposalState::Draft | ProposalState::SigningOff => Ok(()),
|
||||||
|
ProposalState::Executing
|
||||||
|
| ProposalState::Completed
|
||||||
|
| ProposalState::Cancelled
|
||||||
|
| ProposalState::Voting
|
||||||
|
| ProposalState::Succeeded
|
||||||
|
| ProposalState::Defeated => Err(GovernanceError::InvalidStateCannotSignOff.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the Proposal is in Voting state
|
||||||
|
fn assert_is_voting_state(&self) -> Result<(), ProgramError> {
|
||||||
|
if self.state != ProposalState::Voting {
|
||||||
|
return Err(GovernanceError::InvalidProposalState.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks the Proposal is in Draft state
|
||||||
|
fn assert_is_draft_state(&self) -> Result<(), ProgramError> {
|
||||||
|
if self.state != ProposalState::Draft {
|
||||||
|
return Err(GovernanceError::InvalidProposalState.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Proposal can be voted on
|
||||||
|
pub fn assert_can_cast_vote(
|
||||||
|
&self,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
current_slot: Slot,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
self.assert_is_voting_state()
|
||||||
|
.map_err(|_| GovernanceError::InvalidStateCannotVote)?;
|
||||||
|
|
||||||
|
// Check if we are still within the configured max_voting_time period
|
||||||
|
if self
|
||||||
|
.voting_at
|
||||||
|
.unwrap()
|
||||||
|
.checked_add(config.max_voting_time)
|
||||||
|
.unwrap()
|
||||||
|
< current_slot
|
||||||
|
{
|
||||||
|
return Err(GovernanceError::ProposalVotingTimeExpired.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Proposal can be finalized
|
||||||
|
pub fn assert_can_finalize_vote(
|
||||||
|
&self,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
current_slot: Slot,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
self.assert_is_voting_state()
|
||||||
|
.map_err(|_| GovernanceError::InvalidStateCannotFinalize)?;
|
||||||
|
|
||||||
|
// Check if we passed the configured max_voting_time period yet
|
||||||
|
if self
|
||||||
|
.voting_at
|
||||||
|
.unwrap()
|
||||||
|
.checked_add(config.max_voting_time)
|
||||||
|
.unwrap()
|
||||||
|
>= current_slot
|
||||||
|
{
|
||||||
|
return Err(GovernanceError::CannotFinalizeVotingInProgress.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalizes vote by moving it to final state Succeeded or Defeated if max_voting_time has passed
|
||||||
|
/// If Proposal is still within max_voting_time period then error is returned
|
||||||
|
pub fn finalize_vote(
|
||||||
|
&mut self,
|
||||||
|
governing_token_supply: u64,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
current_slot: Slot,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
self.assert_can_finalize_vote(config, current_slot)?;
|
||||||
|
|
||||||
|
self.state = self.get_final_vote_state(governing_token_supply, config);
|
||||||
|
self.voting_completed_at = Some(current_slot);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_final_vote_state(
|
||||||
|
&mut self,
|
||||||
|
governing_token_supply: u64,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
) -> ProposalState {
|
||||||
|
let yes_vote_threshold_count =
|
||||||
|
get_vote_threshold_count(config.yes_vote_threshold_percentage, governing_token_supply);
|
||||||
|
|
||||||
|
// Yes vote must be equal or above the required yes_vote_threshold_percentage and higher than No vote
|
||||||
|
// The same number of Yes and No votes is a tie and resolved as Defeated
|
||||||
|
// In other words +1 vote as a tie breaker is required to Succeed
|
||||||
|
if self.yes_votes_count >= yes_vote_threshold_count
|
||||||
|
&& self.yes_votes_count > self.no_votes_count
|
||||||
|
{
|
||||||
|
ProposalState::Succeeded
|
||||||
|
} else {
|
||||||
|
ProposalState::Defeated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state
|
||||||
|
/// If the conditions are met the state is updated accordingly
|
||||||
|
pub fn try_tip_vote(
|
||||||
|
&mut self,
|
||||||
|
governing_token_supply: u64,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
current_slot: Slot,
|
||||||
|
) {
|
||||||
|
if let Some(tipped_state) = self.try_get_tipped_vote_state(governing_token_supply, config) {
|
||||||
|
self.state = tipped_state;
|
||||||
|
self.voting_completed_at = Some(current_slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if vote can be tipped and automatically transitioned to Succeeded or Defeated state
|
||||||
|
/// If yes then Some(ProposalState) is returned and None otherwise
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
|
pub fn try_get_tipped_vote_state(
|
||||||
|
&self,
|
||||||
|
governing_token_supply: u64,
|
||||||
|
config: &GovernanceConfig,
|
||||||
|
) -> Option<ProposalState> {
|
||||||
|
if self.yes_votes_count == governing_token_supply {
|
||||||
|
return Some(ProposalState::Succeeded);
|
||||||
|
}
|
||||||
|
if self.no_votes_count == governing_token_supply {
|
||||||
|
return Some(ProposalState::Defeated);
|
||||||
|
}
|
||||||
|
|
||||||
|
let yes_vote_threshold_count =
|
||||||
|
get_vote_threshold_count(config.yes_vote_threshold_percentage, governing_token_supply);
|
||||||
|
|
||||||
|
if self.yes_votes_count >= yes_vote_threshold_count
|
||||||
|
&& self.yes_votes_count > (governing_token_supply - self.yes_votes_count)
|
||||||
|
{
|
||||||
|
return Some(ProposalState::Succeeded);
|
||||||
|
} else if self.no_votes_count > (governing_token_supply - yes_vote_threshold_count)
|
||||||
|
|| self.no_votes_count >= (governing_token_supply - self.no_votes_count)
|
||||||
|
{
|
||||||
|
return Some(ProposalState::Defeated);
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Proposal can be canceled in the given state
|
||||||
|
pub fn assert_can_cancel(&self) -> Result<(), ProgramError> {
|
||||||
|
match self.state {
|
||||||
|
ProposalState::Draft | ProposalState::SigningOff | ProposalState::Voting => Ok(()),
|
||||||
|
ProposalState::Executing
|
||||||
|
| ProposalState::Completed
|
||||||
|
| ProposalState::Cancelled
|
||||||
|
| ProposalState::Succeeded
|
||||||
|
| ProposalState::Defeated => {
|
||||||
|
Err(GovernanceError::InvalidStateCannotCancelProposal.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Instructions can be edited (inserted or removed) for the Proposal in the given state
|
||||||
|
pub fn assert_can_edit_instructions(&self) -> Result<(), ProgramError> {
|
||||||
|
self.assert_is_draft_state()
|
||||||
|
.map_err(|_| GovernanceError::InvalidStateCannotEditInstructions.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if Instructions can be executed for the Proposal in the given state
|
||||||
|
pub fn assert_can_execute_instruction(
|
||||||
|
&self,
|
||||||
|
proposal_instruction_data: &ProposalInstruction,
|
||||||
|
current_slot: Slot,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
match self.state {
|
||||||
|
ProposalState::Succeeded | ProposalState::Executing => {}
|
||||||
|
ProposalState::Draft
|
||||||
|
| ProposalState::SigningOff
|
||||||
|
| ProposalState::Completed
|
||||||
|
| ProposalState::Voting
|
||||||
|
| ProposalState::Cancelled
|
||||||
|
| ProposalState::Defeated => {
|
||||||
|
return Err(GovernanceError::InvalidStateCannotExecuteInstruction.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self
|
||||||
|
.voting_completed_at
|
||||||
|
.unwrap()
|
||||||
|
.checked_add(proposal_instruction_data.hold_up_time)
|
||||||
|
.unwrap()
|
||||||
|
>= current_slot
|
||||||
|
{
|
||||||
|
return Err(GovernanceError::CannotExecuteInstructionWithinHoldUpTime.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if proposal_instruction_data.executed_at.is_some() {
|
||||||
|
return Err(GovernanceError::InstructionAlreadyExecuted.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts threshold in percentages to actual vote count
|
||||||
|
fn get_vote_threshold_count(threshold_percentage: u8, total_supply: u64) -> u64 {
|
||||||
|
let numerator = (threshold_percentage as u128)
|
||||||
|
.checked_mul(total_supply as u128)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut threshold = numerator.checked_div(100).unwrap();
|
||||||
|
|
||||||
|
if threshold * 100 < numerator {
|
||||||
|
threshold += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
threshold as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes Proposal account and checks owner program
|
||||||
|
pub fn get_proposal_data(proposal_info: &AccountInfo) -> Result<Proposal, ProgramError> {
|
||||||
|
get_account_data::<Proposal>(proposal_info, &id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes Proposal and validates it belongs to the given Governance and Governing Mint
|
||||||
|
pub fn get_proposal_data_for_governance_and_governing_mint(
|
||||||
|
proposal_info: &AccountInfo,
|
||||||
|
governance: &Pubkey,
|
||||||
|
governing_token_mint: &Pubkey,
|
||||||
|
) -> Result<Proposal, ProgramError> {
|
||||||
|
let proposal_data = get_proposal_data_for_governance(proposal_info, governance)?;
|
||||||
|
|
||||||
|
if proposal_data.governing_token_mint != *governing_token_mint {
|
||||||
|
return Err(GovernanceError::InvalidGoverningMintForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proposal_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes Proposal and validates it belongs to the given Governance
|
||||||
|
pub fn get_proposal_data_for_governance(
|
||||||
|
proposal_info: &AccountInfo,
|
||||||
|
governance: &Pubkey,
|
||||||
|
) -> Result<Proposal, ProgramError> {
|
||||||
|
let proposal_data = get_proposal_data(proposal_info)?;
|
||||||
|
|
||||||
|
if proposal_data.governance != *governance {
|
||||||
|
return Err(GovernanceError::InvalidGovernanceForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proposal_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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_le_bytes: &'a [u8],
|
||||||
|
) -> Pubkey {
|
||||||
|
Pubkey::find_program_address(
|
||||||
|
&get_proposal_address_seeds(governance, governing_token_mint, &proposal_index_le_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),
|
||||||
|
|
||||||
|
yes_votes_count: 0,
|
||||||
|
no_votes_count: 0,
|
||||||
|
|
||||||
|
instructions_executed_count: 10,
|
||||||
|
instructions_count: 10,
|
||||||
|
instructions_next_index: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_governance_config() -> GovernanceConfig {
|
||||||
|
GovernanceConfig {
|
||||||
|
realm: Pubkey::new_unique(),
|
||||||
|
governed_account: Pubkey::new_unique(),
|
||||||
|
yes_vote_threshold_percentage: 60,
|
||||||
|
min_tokens_to_create_proposal: 5,
|
||||||
|
min_instruction_hold_up_time: 10,
|
||||||
|
max_voting_time: 5,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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));
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_compose! {
|
||||||
|
fn vote_results()(governing_token_supply in 1..=u64::MAX)(
|
||||||
|
governing_token_supply in Just(governing_token_supply),
|
||||||
|
vote_count in 0..=governing_token_supply,
|
||||||
|
) -> (u64, u64) {
|
||||||
|
(vote_count as u64, governing_token_supply as u64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn editable_signatory_states() -> impl Strategy<Value = ProposalState> {
|
||||||
|
prop_oneof![Just(ProposalState::Draft)]
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
Just(ProposalState::SigningOff),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancellable_states() -> impl Strategy<Value = ProposalState> {
|
||||||
|
prop_oneof![
|
||||||
|
Just(ProposalState::Draft),
|
||||||
|
Just(ProposalState::SigningOff),
|
||||||
|
Just(ProposalState::Voting),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn test_assert_can_cancel(state in cancellable_states()) {
|
||||||
|
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = state;
|
||||||
|
proposal.assert_can_cancel().unwrap();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fn none_cancellable_states() -> impl Strategy<Value = ProposalState> {
|
||||||
|
prop_oneof![
|
||||||
|
Just(ProposalState::Succeeded),
|
||||||
|
Just(ProposalState::Executing),
|
||||||
|
Just(ProposalState::Completed),
|
||||||
|
Just(ProposalState::Cancelled),
|
||||||
|
Just(ProposalState::Defeated),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn test_assert_can_cancel_with_invalid_state_error(state in none_cancellable_states()) {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = state;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = proposal.assert_can_cancel().err().unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::InvalidStateCannotCancelProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct VoteCastTestCase {
|
||||||
|
name: &'static str,
|
||||||
|
governing_token_supply: u64,
|
||||||
|
vote_threshold_percentage: u8,
|
||||||
|
yes_votes_count: u64,
|
||||||
|
no_votes_count: u64,
|
||||||
|
expected_tipped_state: ProposalState,
|
||||||
|
expected_finalized_state: ProposalState,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vote_casting_test_cases() -> impl Strategy<Value = VoteCastTestCase> {
|
||||||
|
prop_oneof![
|
||||||
|
// threshold < 50%
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "45:10 @40 -- Nays can still outvote Yeahs",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 45,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "49:50 @40 -- In best case scenario it can be 50:50 tie and hence Defeated",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 49,
|
||||||
|
no_votes_count: 50,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "40:40 @40 -- Still can go either way",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 40,
|
||||||
|
no_votes_count: 40,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "45:45 @40 -- Still can go either way",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 45,
|
||||||
|
no_votes_count: 45,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "50:10 @40 -- Nay sayers can still tie up",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 50,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "50:50 @40 -- It's a tie and hence Defeated",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 50,
|
||||||
|
no_votes_count: 50,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "45:51 @ 40 -- Nays won",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 45,
|
||||||
|
no_votes_count: 51,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "40:55 @ 40 -- Nays won",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 40,
|
||||||
|
yes_votes_count: 40,
|
||||||
|
no_votes_count: 55,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
// threshold == 50%
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "50:10 @50 -- +1 tie breaker required to tip",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 50,
|
||||||
|
yes_votes_count: 50,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "10:50 @50 -- +1 tie breaker vote not possible any longer",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 50,
|
||||||
|
yes_votes_count: 10,
|
||||||
|
no_votes_count: 50,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "50:50 @50 -- +1 tie breaker vote not possible any longer",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 50,
|
||||||
|
yes_votes_count: 50,
|
||||||
|
no_votes_count: 50,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "51:10 @ 50 -- Nay sayers can't outvote any longer",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 50,
|
||||||
|
yes_votes_count: 51,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Succeeded,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "10:51 @ 50 -- Nays won",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 50,
|
||||||
|
yes_votes_count: 10,
|
||||||
|
no_votes_count: 51,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
// threshold > 50%
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "10:10 @ 60 -- Can still go either way",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 10,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "55:10 @ 60 -- Can still go either way",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 55,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "60:10 @ 60 -- Yeah reached the required threshold",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 60,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Succeeded,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "61:10 @ 60 -- Yeah won",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 61,
|
||||||
|
no_votes_count: 10,
|
||||||
|
expected_tipped_state: ProposalState::Succeeded,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "10:40 @ 60 -- Yeah can still outvote Nay",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 10,
|
||||||
|
no_votes_count: 40,
|
||||||
|
expected_tipped_state: ProposalState::Voting,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "60:40 @ 60 -- Yeah won",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 60,
|
||||||
|
no_votes_count: 40,
|
||||||
|
expected_tipped_state: ProposalState::Succeeded,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "10:41 @ 60 -- Aye can't outvote Nay any longer",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 60,
|
||||||
|
yes_votes_count: 10,
|
||||||
|
no_votes_count: 41,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "100:0",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 100,
|
||||||
|
yes_votes_count: 100,
|
||||||
|
no_votes_count: 0,
|
||||||
|
expected_tipped_state: ProposalState::Succeeded,
|
||||||
|
expected_finalized_state: ProposalState::Succeeded,
|
||||||
|
}),
|
||||||
|
Just(VoteCastTestCase {
|
||||||
|
name: "0:100",
|
||||||
|
governing_token_supply: 100,
|
||||||
|
vote_threshold_percentage: 100,
|
||||||
|
yes_votes_count: 0,
|
||||||
|
no_votes_count: 100,
|
||||||
|
expected_tipped_state: ProposalState::Defeated,
|
||||||
|
expected_finalized_state: ProposalState::Defeated,
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn test_try_tip_vote(test_case in vote_casting_test_cases()) {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.yes_votes_count = test_case.yes_votes_count;
|
||||||
|
proposal.no_votes_count = test_case.no_votes_count;
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
|
||||||
|
let mut governance_config = create_test_governance_config();
|
||||||
|
governance_config.yes_vote_threshold_percentage = test_case.vote_threshold_percentage;
|
||||||
|
|
||||||
|
let current_slot = 15_u64;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
proposal.try_tip_vote(test_case.governing_token_supply, &governance_config,current_slot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(proposal.state,test_case.expected_tipped_state,"CASE: {:?}",test_case);
|
||||||
|
|
||||||
|
if test_case.expected_tipped_state != ProposalState::Voting {
|
||||||
|
assert_eq!(Some(current_slot),proposal.voting_completed_at)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_finalize_vote(test_case in vote_casting_test_cases()) {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.yes_votes_count = test_case.yes_votes_count;
|
||||||
|
proposal.no_votes_count = test_case.no_votes_count;
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
|
||||||
|
let mut governance_config = create_test_governance_config();
|
||||||
|
governance_config.yes_vote_threshold_percentage = test_case.vote_threshold_percentage;
|
||||||
|
|
||||||
|
let current_slot = 16_u64;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
proposal.finalize_vote(test_case.governing_token_supply, &governance_config,current_slot).unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(proposal.state,test_case.expected_finalized_state,"CASE: {:?}",test_case);
|
||||||
|
assert_eq!(Some(current_slot),proposal.voting_completed_at);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_compose! {
|
||||||
|
fn full_vote_results()(governing_token_supply in 1..=u64::MAX, yes_vote_threshold in 1..100)(
|
||||||
|
governing_token_supply in Just(governing_token_supply),
|
||||||
|
yes_vote_threshold in Just(yes_vote_threshold),
|
||||||
|
|
||||||
|
yes_votes_count in 0..=governing_token_supply,
|
||||||
|
no_votes_count in 0..=governing_token_supply,
|
||||||
|
|
||||||
|
) -> (u64, u64, u64, u8) {
|
||||||
|
(yes_votes_count as u64, no_votes_count as u64, governing_token_supply as u64,yes_vote_threshold as u8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn test_try_tip_vote_with_full_vote_results(
|
||||||
|
(yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
|
||||||
|
|
||||||
|
) {
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.yes_votes_count = yes_votes_count;
|
||||||
|
proposal.no_votes_count =no_votes_count.min(governing_token_supply-yes_votes_count);
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
|
||||||
|
|
||||||
|
let mut governance_config = create_test_governance_config();
|
||||||
|
governance_config.yes_vote_threshold_percentage = yes_vote_threshold_percentage;
|
||||||
|
|
||||||
|
let current_slot = 15_u64;
|
||||||
|
|
||||||
|
|
||||||
|
// Act
|
||||||
|
proposal.try_tip_vote(governing_token_supply, &governance_config,current_slot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let yes_vote_threshold_count = get_vote_threshold_count(yes_vote_threshold_percentage,governing_token_supply);
|
||||||
|
|
||||||
|
if yes_votes_count >= yes_vote_threshold_count && yes_votes_count > (governing_token_supply - yes_votes_count)
|
||||||
|
{
|
||||||
|
assert_eq!(proposal.state,ProposalState::Succeeded);
|
||||||
|
} else if proposal.no_votes_count > (governing_token_supply - yes_vote_threshold_count)
|
||||||
|
|| proposal.no_votes_count >= (governing_token_supply - proposal.no_votes_count ) {
|
||||||
|
assert_eq!(proposal.state,ProposalState::Defeated);
|
||||||
|
} else {
|
||||||
|
assert_eq!(proposal.state,ProposalState::Voting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
#[test]
|
||||||
|
fn test_finalize_vote_with_full_vote_results(
|
||||||
|
(yes_votes_count, no_votes_count, governing_token_supply, yes_vote_threshold_percentage) in full_vote_results(),
|
||||||
|
|
||||||
|
) {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.yes_votes_count = yes_votes_count;
|
||||||
|
proposal.no_votes_count = no_votes_count.min(governing_token_supply-yes_votes_count);
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
|
||||||
|
|
||||||
|
let mut governance_config = create_test_governance_config();
|
||||||
|
governance_config.yes_vote_threshold_percentage = yes_vote_threshold_percentage;
|
||||||
|
|
||||||
|
let current_slot = 16_u64;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
proposal.finalize_vote(governing_token_supply, &governance_config,current_slot).unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let yes_vote_threshold_count = get_vote_threshold_count(yes_vote_threshold_percentage,governing_token_supply);
|
||||||
|
|
||||||
|
if yes_votes_count >= yes_vote_threshold_count && yes_votes_count > proposal.no_votes_count
|
||||||
|
{
|
||||||
|
assert_eq!(proposal.state,ProposalState::Succeeded);
|
||||||
|
} else {
|
||||||
|
assert_eq!(proposal.state,ProposalState::Defeated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_finalize_vote_with_expired_voting_time_error() {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
let governance_config = create_test_governance_config();
|
||||||
|
|
||||||
|
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = proposal
|
||||||
|
.finalize_vote(100, &governance_config, current_slot)
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::CannotFinalizeVotingInProgress.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_finalize_vote_after_voting_time() {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
let governance_config = create_test_governance_config();
|
||||||
|
|
||||||
|
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time + 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = proposal.finalize_vote(100, &governance_config, current_slot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_assert_can_vote_with_expired_voting_time_error() {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
let governance_config = create_test_governance_config();
|
||||||
|
|
||||||
|
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time + 1;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = proposal
|
||||||
|
.assert_can_cast_vote(&governance_config, current_slot)
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_assert_can_vote_within_voting_time() {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal = create_test_proposal();
|
||||||
|
proposal.state = ProposalState::Voting;
|
||||||
|
let governance_config = create_test_governance_config();
|
||||||
|
|
||||||
|
let current_slot = proposal.voting_at.unwrap() + governance_config.max_voting_time;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let result = proposal.assert_can_cast_vote(&governance_config, current_slot);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(result, Ok(()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
//! ProposalInstruction Account
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::GovernanceError,
|
||||||
|
id,
|
||||||
|
state::enums::GovernanceAccountType,
|
||||||
|
tools::account::{get_account_data, AccountMaxSize},
|
||||||
|
PROGRAM_AUTHORITY_SEED,
|
||||||
|
};
|
||||||
|
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||||
|
use solana_program::{
|
||||||
|
account_info::AccountInfo,
|
||||||
|
epoch_schedule::Slot,
|
||||||
|
instruction::{AccountMeta, Instruction},
|
||||||
|
program_error::ProgramError,
|
||||||
|
program_pack::IsInitialized,
|
||||||
|
pubkey::Pubkey,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// InstructionData wrapper. It can be removed once Borsh serialization for Instruction is supported in the SDK
|
||||||
|
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct InstructionData {
|
||||||
|
/// Pubkey of the instruction processor that executes this instruction
|
||||||
|
pub program_id: Pubkey,
|
||||||
|
/// Metadata for what accounts should be passed to the instruction processor
|
||||||
|
pub accounts: Vec<AccountMetaData>,
|
||||||
|
/// Opaque data passed to the instruction processor
|
||||||
|
pub data: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account metadata used to define Instructions
|
||||||
|
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct AccountMetaData {
|
||||||
|
/// An account's public key
|
||||||
|
pub pubkey: Pubkey,
|
||||||
|
/// True if an Instruction requires a Transaction signature matching `pubkey`.
|
||||||
|
pub is_signer: bool,
|
||||||
|
/// True if the `pubkey` can be loaded as a read-write account.
|
||||||
|
pub is_writable: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Instruction> for InstructionData {
|
||||||
|
fn from(instruction: Instruction) -> Self {
|
||||||
|
InstructionData {
|
||||||
|
program_id: instruction.program_id,
|
||||||
|
accounts: instruction
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|a| AccountMetaData {
|
||||||
|
pubkey: a.pubkey,
|
||||||
|
is_signer: a.is_signer,
|
||||||
|
is_writable: a.is_writable,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
data: instruction.data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&InstructionData> for Instruction {
|
||||||
|
fn from(instruction: &InstructionData) -> Self {
|
||||||
|
Instruction {
|
||||||
|
program_id: instruction.program_id,
|
||||||
|
accounts: instruction
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|a| AccountMeta {
|
||||||
|
pubkey: a.pubkey,
|
||||||
|
is_signer: a.is_signer,
|
||||||
|
is_writable: a.is_writable,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
data: instruction.data.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Account for an instruction to be executed for Proposal
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
|
||||||
|
pub struct ProposalInstruction {
|
||||||
|
/// Governance Account type
|
||||||
|
pub account_type: GovernanceAccountType,
|
||||||
|
|
||||||
|
/// The Proposal the instruction belongs to
|
||||||
|
pub proposal: Pubkey,
|
||||||
|
|
||||||
|
/// 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 at flag
|
||||||
|
pub executed_at: Option<Slot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountMaxSize for ProposalInstruction {
|
||||||
|
fn get_max_size(&self) -> Option<usize> {
|
||||||
|
Some(self.instruction.accounts.len() * 34 + self.instruction.data.len() + 90)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsInitialized for ProposalInstruction {
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.account_type == GovernanceAccountType::ProposalInstruction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ProposalInstruction PDA seeds
|
||||||
|
pub fn get_proposal_instruction_address_seeds<'a>(
|
||||||
|
proposal: &'a Pubkey,
|
||||||
|
instruction_index_le_bytes: &'a [u8],
|
||||||
|
) -> [&'a [u8]; 3] {
|
||||||
|
[
|
||||||
|
PROGRAM_AUTHORITY_SEED,
|
||||||
|
proposal.as_ref(),
|
||||||
|
&instruction_index_le_bytes,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns ProposalInstruction PDA address
|
||||||
|
pub fn get_proposal_instruction_address<'a>(
|
||||||
|
proposal: &'a Pubkey,
|
||||||
|
instruction_index_le_bytes: &'a [u8],
|
||||||
|
) -> Pubkey {
|
||||||
|
Pubkey::find_program_address(
|
||||||
|
&get_proposal_instruction_address_seeds(proposal, &instruction_index_le_bytes),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes ProposalInstruction account and checks owner program
|
||||||
|
pub fn get_proposal_instruction_data(
|
||||||
|
proposal_instruction_info: &AccountInfo,
|
||||||
|
) -> Result<ProposalInstruction, ProgramError> {
|
||||||
|
get_account_data::<ProposalInstruction>(proposal_instruction_info, &id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes and returns ProposalInstruction account and checks it belongs to the given Proposal
|
||||||
|
pub fn get_proposal_instruction_data_for_proposal(
|
||||||
|
proposal_instruction_info: &AccountInfo,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
) -> Result<ProposalInstruction, ProgramError> {
|
||||||
|
let proposal_instruction_data = get_proposal_instruction_data(proposal_instruction_info)?;
|
||||||
|
|
||||||
|
if proposal_instruction_data.proposal != *proposal {
|
||||||
|
return Err(GovernanceError::InvalidProposalForProposalInstruction.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(proposal_instruction_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes ProposalInstruction account and checks it belongs to the given Proposal
|
||||||
|
pub fn assert_proposal_instruction_for_proposal(
|
||||||
|
proposal_instruction_info: &AccountInfo,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
get_proposal_instruction_data_for_proposal(proposal_instruction_info, proposal).map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn create_test_account_meta_data() -> AccountMetaData {
|
||||||
|
AccountMetaData {
|
||||||
|
pubkey: Pubkey::new_unique(),
|
||||||
|
is_signer: true,
|
||||||
|
is_writable: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_instruction_data() -> InstructionData {
|
||||||
|
InstructionData {
|
||||||
|
program_id: Pubkey::new_unique(),
|
||||||
|
accounts: vec![
|
||||||
|
create_test_account_meta_data(),
|
||||||
|
create_test_account_meta_data(),
|
||||||
|
],
|
||||||
|
data: vec![1, 2, 3],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_test_proposal_instruction() -> ProposalInstruction {
|
||||||
|
ProposalInstruction {
|
||||||
|
account_type: GovernanceAccountType::ProposalInstruction,
|
||||||
|
proposal: Pubkey::new_unique(),
|
||||||
|
hold_up_time: 10,
|
||||||
|
instruction: create_test_instruction_data(),
|
||||||
|
executed_at: Some(100),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_account_meta_data_size() {
|
||||||
|
let account_meta_data = create_test_account_meta_data();
|
||||||
|
let size = account_meta_data.try_to_vec().unwrap().len();
|
||||||
|
|
||||||
|
assert_eq!(34, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proposal_instruction_max_size() {
|
||||||
|
// Arrange
|
||||||
|
let proposal_instruction = create_test_proposal_instruction();
|
||||||
|
let size = proposal_instruction.try_to_vec().unwrap().len();
|
||||||
|
|
||||||
|
// Act, Assert
|
||||||
|
assert_eq!(proposal_instruction.get_max_size(), Some(size));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_empty_proposal_instruction_max_size() {
|
||||||
|
// Arrange
|
||||||
|
let mut proposal_instruction = create_test_proposal_instruction();
|
||||||
|
proposal_instruction.instruction.data = vec![];
|
||||||
|
proposal_instruction.instruction.accounts = vec![];
|
||||||
|
|
||||||
|
let size = proposal_instruction.try_to_vec().unwrap().len();
|
||||||
|
|
||||||
|
// Act, Assert
|
||||||
|
assert_eq!(proposal_instruction.get_max_size(), Some(size));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,104 @@
|
||||||
|
//! 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, get_account_data, 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 get_realm_data(realm_info: &AccountInfo) -> Result<Realm, ProgramError> {
|
||||||
|
get_account_data::<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
|
||||||
|
}
|
|
@ -0,0 +1,108 @@
|
||||||
|
//! 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::{get_account_data, 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 get_signatory_record_data(
|
||||||
|
signatory_record_info: &AccountInfo,
|
||||||
|
) -> Result<SignatoryRecord, ProgramError> {
|
||||||
|
get_account_data::<SignatoryRecord>(signatory_record_info, &id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes SignatoryRecord and validates its PDA
|
||||||
|
pub fn get_signatory_record_data_for_seeds(
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
get_signatory_record_data(signatory_record_info)
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
//! Token Owner Record Account
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
error::GovernanceError,
|
||||||
|
id,
|
||||||
|
tools::account::{get_account_data, 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 of the tokens
|
||||||
|
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 can be delegated to by the governing_token_owner or current governance_delegate
|
||||||
|
pub governance_delegate: Option<Pubkey>,
|
||||||
|
|
||||||
|
/// The number of votes cast by TokenOwner but not relinquished yet
|
||||||
|
/// Every time a vote is cast this number is increased and it's always decreased when relinquishing a vote regardless of the vote state
|
||||||
|
pub unrelinquished_votes_count: u32,
|
||||||
|
|
||||||
|
/// The total number of votes cast by the TokenOwner
|
||||||
|
/// If TokenOwner withdraws vote while voting is still in progress total_votes_count is decreased and the vote doesn't count towards the total
|
||||||
|
pub total_votes_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AccountMaxSize for TokenOwnerRecord {
|
||||||
|
fn get_max_size(&self) -> Option<usize> {
|
||||||
|
Some(146)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IsInitialized for TokenOwnerRecord {
|
||||||
|
fn is_initialized(&self) -> bool {
|
||||||
|
self.account_type == GovernanceAccountType::TokenOwnerRecord
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TokenOwnerRecord {
|
||||||
|
/// Checks whether the provided Governance Authority signed transaction
|
||||||
|
pub fn assert_token_owner_or_delegate_is_signer(
|
||||||
|
&self,
|
||||||
|
governance_authority_info: &AccountInfo,
|
||||||
|
) -> Result<(), ProgramError> {
|
||||||
|
if governance_authority_info.is_signer {
|
||||||
|
if &self.governing_token_owner == governance_authority_info.key {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(governance_delegate) = self.governance_delegate {
|
||||||
|
if &governance_delegate == governance_authority_info.key {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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 get_token_owner_record_data(
|
||||||
|
token_owner_record_info: &AccountInfo,
|
||||||
|
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||||
|
get_account_data::<TokenOwnerRecord>(token_owner_record_info, &id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes TokenOwnerRecord account and checks its PDA against the provided seeds
|
||||||
|
pub fn get_token_owner_record_data_for_seeds(
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
|
||||||
|
get_token_owner_record_data(token_owner_record_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes TokenOwnerRecord account and checks that its PDA matches the given realm and governing mint
|
||||||
|
pub fn get_token_owner_record_data_for_realm_and_governing_mint(
|
||||||
|
token_owner_record_info: &AccountInfo,
|
||||||
|
realm: &Pubkey,
|
||||||
|
governing_token_mint: &Pubkey,
|
||||||
|
) -> Result<TokenOwnerRecord, ProgramError> {
|
||||||
|
let token_owner_record_data = get_token_owner_record_data(token_owner_record_info)?;
|
||||||
|
|
||||||
|
if token_owner_record_data.governing_token_mint != *governing_token_mint {
|
||||||
|
return Err(GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if token_owner_record_data.realm != *realm {
|
||||||
|
return Err(GovernanceError::InvalidRealmForTokenOwnerRecord.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(token_owner_record_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes TokenOwnerRecord account and checks its address is the give proposal_owner
|
||||||
|
pub fn get_token_owner_record_data_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());
|
||||||
|
}
|
||||||
|
|
||||||
|
get_token_owner_record_data(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()),
|
||||||
|
unrelinquished_votes_count: 1,
|
||||||
|
total_votes_count: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
let size = get_packed_len::<TokenOwnerRecord>();
|
||||||
|
|
||||||
|
assert_eq!(token_owner_record.get_max_size(), Some(size));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
//! Proposal Vote Record Account
|
||||||
|
|
||||||
|
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||||
|
use solana_program::account_info::AccountInfo;
|
||||||
|
use solana_program::program_error::ProgramError;
|
||||||
|
use solana_program::{program_pack::IsInitialized, pubkey::Pubkey};
|
||||||
|
|
||||||
|
use crate::error::GovernanceError;
|
||||||
|
use crate::tools::account::get_account_data;
|
||||||
|
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,
|
||||||
|
|
||||||
|
/// Indicates whether the vote was relinquished by voter
|
||||||
|
pub is_relinquished: bool,
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl VoteRecord {
|
||||||
|
/// Checks the vote can be relinquished
|
||||||
|
pub fn assert_can_relinquish_vote(&self) -> Result<(), ProgramError> {
|
||||||
|
if self.is_relinquished {
|
||||||
|
return Err(GovernanceError::VoteAlreadyRelinquished.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes VoteRecord account and checks owner program
|
||||||
|
pub fn get_vote_record_data(vote_record_info: &AccountInfo) -> Result<VoteRecord, ProgramError> {
|
||||||
|
get_account_data::<VoteRecord>(vote_record_info, &id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deserializes VoteRecord and checks it belongs to the provided Proposal and Governing Token Owner
|
||||||
|
pub fn get_vote_record_data_for_proposal_and_token_owner(
|
||||||
|
vote_record_info: &AccountInfo,
|
||||||
|
proposal: &Pubkey,
|
||||||
|
governing_token_owner: &Pubkey,
|
||||||
|
) -> Result<VoteRecord, ProgramError> {
|
||||||
|
let vote_record_data = get_vote_record_data(vote_record_info)?;
|
||||||
|
|
||||||
|
if vote_record_data.proposal != *proposal {
|
||||||
|
return Err(GovernanceError::InvalidProposalForVoterRecord.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if vote_record_data.governing_token_owner != *governing_token_owner {
|
||||||
|
return Err(GovernanceError::InvalidGoverningTokenOwnerForVoteRecord.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(vote_record_data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
//! 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 get_account_data<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_info: &AccountInfo) {
|
||||||
|
let account_lamports = account_info.lamports();
|
||||||
|
**account_info.lamports.borrow_mut() = 0;
|
||||||
|
|
||||||
|
**beneficiary_info.lamports.borrow_mut() = beneficiary_info
|
||||||
|
.lamports()
|
||||||
|
.checked_add(account_lamports)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut account_data = account_info.data.borrow_mut();
|
||||||
|
|
||||||
|
account_data.fill(0);
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
//! 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(())
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
//! Utility functions
|
||||||
|
|
||||||
|
pub mod account;
|
||||||
|
|
||||||
|
pub mod spl_token;
|
||||||
|
|
||||||
|
pub mod bpf_loader_upgradeable;
|
|
@ -0,0 +1,245 @@
|
||||||
|
//! 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 spl_token::state::{Account, Mint};
|
||||||
|
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts the given account_info represents a valid SPL Token account which is initialized and belongs to spl_token program
|
||||||
|
pub fn assert_is_valid_spl_token_account(account_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||||
|
if account_info.data_is_empty() {
|
||||||
|
return Err(GovernanceError::SplTokenAccountNotInitialized.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if account_info.owner != &spl_token::id() {
|
||||||
|
return Err(GovernanceError::SplTokenAccountWithInvalidOwner.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if account_info.data_len() != Account::LEN {
|
||||||
|
return Err(GovernanceError::SplTokenInvalidTokenAccountData.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asserts the given mint_info represents a valid SPL Token Mint account which is initialized and belongs to spl_token program
|
||||||
|
pub fn assert_is_valid_spl_token_mint(mint_info: &AccountInfo) -> Result<(), ProgramError> {
|
||||||
|
if mint_info.data_is_empty() {
|
||||||
|
return Err(GovernanceError::SplTokenMintNotInitialized.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if mint_info.owner != &spl_token::id() {
|
||||||
|
return Err(GovernanceError::SplTokenMintWithInvalidOwner.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if mint_info.data_len() != Mint::LEN {
|
||||||
|
return Err(GovernanceError::SplTokenInvalidMintAccountData.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computationally cheap method to get amount from a token account
|
||||||
|
/// It reads amount without deserializing full account data
|
||||||
|
pub fn get_spl_token_amount(token_account_info: &AccountInfo) -> Result<u64, ProgramError> {
|
||||||
|
assert_is_valid_spl_token_account(token_account_info)?;
|
||||||
|
|
||||||
|
// 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_spl_token_mint(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
|
||||||
|
assert_is_valid_spl_token_account(token_account_info)?;
|
||||||
|
|
||||||
|
// 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_spl_token_owner(token_account_info: &AccountInfo) -> Result<Pubkey, ProgramError> {
|
||||||
|
assert_is_valid_spl_token_account(token_account_info)?;
|
||||||
|
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computationally cheap method to just get supply from a mint without unpacking the whole object
|
||||||
|
pub fn get_spl_token_mint_supply(mint_info: &AccountInfo) -> Result<u64, ProgramError> {
|
||||||
|
assert_is_valid_spl_token_mint(mint_info)?;
|
||||||
|
// In token program, 36, 8, 1, 1 is the layout, where the first 8 is supply u64.
|
||||||
|
// so we start at 36.
|
||||||
|
let data = mint_info.try_borrow_data().unwrap();
|
||||||
|
let bytes = array_ref![data, 36, 8];
|
||||||
|
|
||||||
|
Ok(u64::from_le_bytes(*bytes))
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
!*.so
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,132 @@
|
||||||
|
#![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_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_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_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_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_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());
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use solana_program_test::tokio;
|
||||||
|
|
||||||
|
use program_test::*;
|
||||||
|
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cancel_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Cancelled, proposal_account.state);
|
||||||
|
assert_eq!(Some(1), proposal_account.closed_at);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cancel_proposal_with_already_completed_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotCancelProposal.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cancel_proposal_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_council_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,567 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use solana_program_test::tokio;
|
||||||
|
|
||||||
|
use program_test::*;
|
||||||
|
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let vote_record_cookie = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let vote_record_account = governance_test
|
||||||
|
.get_vote_record_account(&vote_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(vote_record_cookie.account, vote_record_account);
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
token_owner_record_cookie
|
||||||
|
.account
|
||||||
|
.governing_token_deposit_amount,
|
||||||
|
proposal_account.yes_votes_count
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.state, ProposalState::Succeeded);
|
||||||
|
assert_eq!(proposal_account.voting_completed_at, Some(1));
|
||||||
|
|
||||||
|
let token_owner_record = governance_test
|
||||||
|
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(1, token_owner_record.unrelinquished_votes_count);
|
||||||
|
assert_eq!(1, token_owner_record.total_votes_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_invalid_governance_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Setup Governance for a different account
|
||||||
|
let governed_account_cookie2 = governance_test.with_governed_account().await;
|
||||||
|
|
||||||
|
let account_governance_cookie2 = governance_test
|
||||||
|
.with_account_governance(&realm_cookie, &governed_account_cookie2)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
proposal_cookie.account.governance = account_governance_cookie2.address;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGovernanceForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_invalid_mint_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to use Council Mint with Community Proposal
|
||||||
|
proposal_cookie.account.governing_token_mint = realm_cookie.account.council_mint.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_invalid_token_owner_record_mint_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to use token_owner_record for Council Mint with Community Proposal
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_council_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.address = token_owner_record_cookie2.address;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidGoverningMintForTokenOwnerRecord.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_invalid_token_owner_record_from_different_realm_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to use token_owner_record from another Realm for the same mint
|
||||||
|
let realm_cookie2 = governance_test.with_realm_using_mints(&realm_cookie).await;
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie2)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.address = token_owner_record_cookie2.address;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidRealmForTokenOwnerRecord.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_governance_authority_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to use a different owner to sign
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_vote_tipped_to_succeeded() {
|
||||||
|
// 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_cookie1 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let token_owner_record_cookie3 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 20)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie1, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Succeeded, proposal_account.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_vote_tipped_to_defeated() {
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 100 votes
|
||||||
|
let token_owner_record_cookie1 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 100 votes
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 100 votes
|
||||||
|
let token_owner_record_cookie3 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 320 votes
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 20)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie1, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie1, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie3, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Defeated, proposal_account.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_threshold_below_50_and_vote_not_tipped() {
|
||||||
|
// 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 governance_config =
|
||||||
|
governance_test.get_default_governance_config(&realm_cookie, &governed_account_cookie);
|
||||||
|
|
||||||
|
governance_config.yes_vote_threshold_percentage = 40;
|
||||||
|
|
||||||
|
let mut account_governance_cookie = governance_test
|
||||||
|
.with_account_governance_using_config(
|
||||||
|
&realm_cookie,
|
||||||
|
&governed_account_cookie,
|
||||||
|
&governance_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 210 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 110)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_voting_time_expired_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
|
||||||
|
+ proposal_account.voting_at.unwrap()
|
||||||
|
+ 1;
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(vote_expired_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::ProposalVotingTimeExpired.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_cast_vote_with_cast_twice_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test.context.warp_to_slot(5).unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::VoteAlreadyExists.into());
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
#![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 1% threshold
|
||||||
|
let config = GovernanceConfig {
|
||||||
|
realm: realm_cookie.address,
|
||||||
|
governed_account: governed_account_cookie.address,
|
||||||
|
yes_vote_threshold_percentage: 0, // below 1% 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_using_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,
|
||||||
|
yes_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_using_config(&realm_cookie, &governed_account_cookie, &config)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGovernanceConfig.into());
|
||||||
|
}
|
|
@ -0,0 +1,180 @@
|
||||||
|
#![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_using_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());
|
||||||
|
}
|
|
@ -0,0 +1,256 @@
|
||||||
|
#![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_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);
|
||||||
|
assert_eq!(proposal_account.draft_at, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let council_token_owner_record_cookie = governance_test
|
||||||
|
.with_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_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_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_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let council_token_owner_record_cookie = governance_test
|
||||||
|
.with_council_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_proposal_using_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::InvalidGoverningMintForTokenOwnerRecord.into()
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
#![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);
|
||||||
|
}
|
|
@ -0,0 +1,270 @@
|
||||||
|
#![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_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_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_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;
|
||||||
|
|
||||||
|
governance_test.context.warp_to_slot(5).unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_subsequent_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_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;
|
||||||
|
|
||||||
|
governance_test.context.warp_to_slot(5).unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_subsequent_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.context.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.context.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());
|
||||||
|
}
|
|
@ -0,0 +1,519 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use solana_program::{
|
||||||
|
instruction::{AccountMeta, Instruction},
|
||||||
|
program_error::ProgramError,
|
||||||
|
sysvar::{clock, fees},
|
||||||
|
};
|
||||||
|
use solana_program_test::tokio;
|
||||||
|
|
||||||
|
use program_test::*;
|
||||||
|
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_mint_instruction() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut 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 proposal_instruction_cookie = governance_test
|
||||||
|
.with_mint_tokens_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Advance slot past hold_up_time
|
||||||
|
let execute_at_slot = 1 + proposal_instruction_cookie.account.hold_up_time + 1;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(1, proposal_account.instructions_executed_count);
|
||||||
|
assert_eq!(ProposalState::Completed, proposal_account.state);
|
||||||
|
assert_eq!(Some(execute_at_slot), proposal_account.closed_at);
|
||||||
|
assert_eq!(Some(execute_at_slot), proposal_account.executing_at);
|
||||||
|
|
||||||
|
let proposal_instruction_account = governance_test
|
||||||
|
.get_proposal_instruction_account(&proposal_instruction_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(execute_at_slot),
|
||||||
|
proposal_instruction_account.executed_at
|
||||||
|
);
|
||||||
|
|
||||||
|
let instruction_token_account = governance_test
|
||||||
|
.get_token_account(&proposal_instruction_cookie.account.instruction.accounts[1].pubkey)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(10, instruction_token_account.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_upgrade_program_instruction() {
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
let mut program_governance_cookie = governance_test
|
||||||
|
.with_program_governance(&realm_cookie, &governed_program_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut program_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let signatory_record_cookie = governance_test
|
||||||
|
.with_signatory(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_upgrade_program_instruction(
|
||||||
|
&program_governance_cookie,
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Advance slot past hold_up_time
|
||||||
|
let execute_at_slot = 1 + proposal_instruction_cookie.account.hold_up_time + 1;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure we can invoke the governed program before upgrade
|
||||||
|
let governed_program_instruction = Instruction::new_with_bytes(
|
||||||
|
governed_program_cookie.address,
|
||||||
|
&[0],
|
||||||
|
vec![
|
||||||
|
AccountMeta::new(governed_program_cookie.address, false),
|
||||||
|
AccountMeta::new(clock::id(), false),
|
||||||
|
AccountMeta::new(fees::id(), false),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.process_transaction(&[governed_program_instruction.clone()], None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// solana_bpf_rust_upgradable returns CustomError == 42
|
||||||
|
assert_eq!(ProgramError::Custom(42), err);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(1, proposal_account.instructions_executed_count);
|
||||||
|
assert_eq!(ProposalState::Completed, proposal_account.state);
|
||||||
|
assert_eq!(Some(execute_at_slot), proposal_account.closed_at);
|
||||||
|
assert_eq!(Some(execute_at_slot), proposal_account.executing_at);
|
||||||
|
|
||||||
|
let proposal_instruction_account = governance_test
|
||||||
|
.get_proposal_instruction_account(&proposal_instruction_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
Some(execute_at_slot),
|
||||||
|
proposal_instruction_account.executed_at
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assert we can invoke the governed program after upgrade
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot + 10)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.process_transaction(&[governed_program_instruction.clone()], None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// solana_bpf_rust_upgraded returns CustomError == 43
|
||||||
|
assert_eq!(ProgramError::Custom(43), err);
|
||||||
|
|
||||||
|
// --------------------------- !!! Voila !!! -----------------------------
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_instruction_with_invalid_state_errors() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut 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();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_mint_tokens_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotExecuteInstruction.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie1)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotExecuteInstruction.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie2)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotExecuteInstruction.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test.context.warp_to_slot(5).unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::CannotExecuteInstructionWithinHoldUpTime.into()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
// Advance slot past hold_up_time
|
||||||
|
let execute_at_slot = 1 + proposal_instruction_cookie.account.hold_up_time + 1;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Completed, proposal_account.state);
|
||||||
|
|
||||||
|
// Arrange
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot + 10)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotExecuteInstruction.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_instruction_for_other_proposal_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut 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 proposal_instruction_cookie = governance_test
|
||||||
|
.with_mint_tokens_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Advance slot past hold_up_time
|
||||||
|
let execute_at_slot = 1 + proposal_instruction_cookie.account.hold_up_time + 1;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_cookie2 = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie2, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidProposalForProposalInstruction.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_execute_mint_instruction_twice_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut 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 proposal_instruction_cookie = governance_test
|
||||||
|
.with_mint_tokens_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.sign_off_proposal(&proposal_cookie, &signatory_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Advance slot past hold_up_time
|
||||||
|
let execute_at_slot = 1 + proposal_instruction_cookie.account.hold_up_time + 1;
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(execute_at_slot + 10)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.execute_instruction(&proposal_cookie, &proposal_instruction_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::InstructionAlreadyExecuted.into());
|
||||||
|
}
|
|
@ -0,0 +1,266 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use solana_program::pubkey::Pubkey;
|
||||||
|
use solana_program_test::tokio;
|
||||||
|
|
||||||
|
use program_test::*;
|
||||||
|
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_finalize_vote_to_succeeded() {
|
||||||
|
// 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 governance_config =
|
||||||
|
governance_test.get_default_governance_config(&realm_cookie, &governed_account_cookie);
|
||||||
|
|
||||||
|
governance_config.yes_vote_threshold_percentage = 40;
|
||||||
|
|
||||||
|
let mut account_governance_cookie = governance_test
|
||||||
|
.with_account_governance_using_config(
|
||||||
|
&realm_cookie,
|
||||||
|
&governed_account_cookie,
|
||||||
|
&governance_config,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 210 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 110)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure not tipped
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Advance slot past max_voting_time
|
||||||
|
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
|
||||||
|
+ proposal_account.voting_at.unwrap()
|
||||||
|
+ 1;
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(vote_expired_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.finalize_vote(&proposal_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.state, ProposalState::Succeeded);
|
||||||
|
assert_eq!(
|
||||||
|
Some(vote_expired_at_slot),
|
||||||
|
proposal_account.voting_completed_at
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_finalize_vote_to_defeated() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure not tipped
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Advance slot past max_voting_time
|
||||||
|
let vote_expired_at_slot = account_governance_cookie.account.config.max_voting_time
|
||||||
|
+ proposal_account.voting_at.unwrap()
|
||||||
|
+ 1;
|
||||||
|
governance_test
|
||||||
|
.context
|
||||||
|
.warp_to_slot(vote_expired_at_slot)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.finalize_vote(&proposal_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Defeated, proposal_account.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_finalize_vote_with_invalid_mint_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure not tipped
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
proposal_cookie.account.governing_token_mint = Pubkey::new_unique();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.finalize_vote(&proposal_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_finalize_vote_with_invalid_governance_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure not tipped
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
// Setup Governance for a different account
|
||||||
|
let governed_account_cookie2 = governance_test.with_governed_account().await;
|
||||||
|
|
||||||
|
let account_governance_cookie2 = governance_test
|
||||||
|
.with_account_governance(&realm_cookie, &governed_account_cookie2)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
proposal_cookie.account.governance = account_governance_cookie2.address;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.finalize_vote(&proposal_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGovernanceForProposal.into());
|
||||||
|
}
|
|
@ -0,0 +1,289 @@
|
||||||
|
#![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_insert_instruction() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_instruction_account = governance_test
|
||||||
|
.get_proposal_instruction_account(&proposal_instruction_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
proposal_instruction_cookie.account,
|
||||||
|
proposal_instruction_account
|
||||||
|
);
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.instructions_count, 1);
|
||||||
|
assert_eq!(proposal_account.instructions_next_index, 1);
|
||||||
|
assert_eq!(proposal_account.instructions_executed_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_multiple_instructions() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.instructions_count, 2);
|
||||||
|
assert_eq!(proposal_account.instructions_next_index, 2);
|
||||||
|
assert_eq!(proposal_account.instructions_executed_count, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_instruction_with_invalid_index_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, Some(1))
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::InvalidInstructionIndex.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_instruction_with_instruction_already_exists_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, Some(0))
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(err, GovernanceError::InstructionAlreadyExists.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_instruction_with_invalid_hold_up_time_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 config =
|
||||||
|
governance_test.get_default_governance_config(&realm_cookie, &governed_account_cookie);
|
||||||
|
|
||||||
|
config.min_instruction_hold_up_time = 100;
|
||||||
|
|
||||||
|
let mut account_governance_cookie = governance_test
|
||||||
|
.with_account_governance_using_config(&realm_cookie, &governed_account_cookie, &config)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InstructionHoldUpTimeBelowRequiredMin.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_instruction_with_not_editable_proposal_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotEditInstructions.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_insert_instruction_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_council_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,402 @@
|
||||||
|
#![cfg(feature = "test-bpf")]
|
||||||
|
|
||||||
|
mod program_test;
|
||||||
|
|
||||||
|
use solana_program::{instruction::AccountMeta, pubkey::Pubkey};
|
||||||
|
use solana_program_test::tokio;
|
||||||
|
|
||||||
|
use program_test::*;
|
||||||
|
use spl_governance::{error::GovernanceError, instruction::Vote, state::enums::ProposalState};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_voted_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut vote_record_cookie = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(100, proposal_account.yes_votes_count);
|
||||||
|
assert_eq!(ProposalState::Succeeded, proposal_account.state);
|
||||||
|
|
||||||
|
let token_owner_record = governance_test
|
||||||
|
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
|
||||||
|
assert_eq!(1, token_owner_record.total_votes_count);
|
||||||
|
|
||||||
|
let vote_record_account = governance_test
|
||||||
|
.get_vote_record_account(&vote_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
vote_record_cookie.account.is_relinquished = true;
|
||||||
|
assert_eq!(vote_record_cookie.account, vote_record_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_active_yes_vote() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vote_record_cookie = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(0, proposal_account.yes_votes_count);
|
||||||
|
assert_eq!(0, proposal_account.no_votes_count);
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
let token_owner_record = governance_test
|
||||||
|
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
|
||||||
|
assert_eq!(0, token_owner_record.total_votes_count);
|
||||||
|
|
||||||
|
let vote_record_account = governance_test
|
||||||
|
.get_account(&vote_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(None, vote_record_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_active_no_vote() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vote_record_cookie = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(0, proposal_account.yes_votes_count);
|
||||||
|
assert_eq!(0, proposal_account.no_votes_count);
|
||||||
|
assert_eq!(ProposalState::Voting, proposal_account.state);
|
||||||
|
|
||||||
|
let token_owner_record = governance_test
|
||||||
|
.get_token_owner_record_account(&token_owner_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(0, token_owner_record.unrelinquished_votes_count);
|
||||||
|
assert_eq!(0, token_owner_record.total_votes_count);
|
||||||
|
|
||||||
|
let vote_record_account = governance_test
|
||||||
|
.get_account(&vote_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(None, vote_record_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_vote_with_invalid_mint_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
proposal_cookie.account.governing_token_mint = Pubkey::new_unique();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::InvalidGoverningMintForProposal.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_vote_with_governance_authority_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 300 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Try to use a different owner to sign
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_vote_with_invalid_vote_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Total 400 tokens
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 200)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vote_record_cookie2 = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie2, Vote::Yes)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// // Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.relinquish_vote_using_instruction(&proposal_cookie, &token_owner_record_cookie, |i| {
|
||||||
|
i.accounts[3] = AccountMeta::new(vote_record_cookie2.address, false)
|
||||||
|
// Try to use a vote_record for other token owner
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// // Assert
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidGoverningTokenOwnerForVoteRecord.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_relinquish_vote_with_already_relinquished_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let proposal_cookie = governance_test
|
||||||
|
.with_signed_off_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let vote_record_cookie = governance_test
|
||||||
|
.with_cast_vote(&proposal_cookie, &token_owner_record_cookie, Vote::No)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Ensure vote is relinquished
|
||||||
|
let vote_record_account = governance_test
|
||||||
|
.get_vote_record_account(&vote_record_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(true, vote_record_account.is_relinquished);
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.mint_community_tokens(&realm_cookie, 10)
|
||||||
|
.await;
|
||||||
|
// Act
|
||||||
|
|
||||||
|
let err = governance_test
|
||||||
|
.relinquish_vote(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
assert_eq!(err, GovernanceError::VoteAlreadyRelinquished.into());
|
||||||
|
}
|
|
@ -0,0 +1,343 @@
|
||||||
|
#![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_remove_instruction() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.instructions_count, 0);
|
||||||
|
assert_eq!(proposal_account.instructions_next_index, 1);
|
||||||
|
assert_eq!(proposal_account.instructions_executed_count, 0);
|
||||||
|
|
||||||
|
let proposal_instruction_account = governance_test
|
||||||
|
.get_account(&proposal_instruction_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(None, proposal_instruction_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_replace_instruction() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie2 = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, Some(0))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.instructions_count, 2);
|
||||||
|
assert_eq!(proposal_account.instructions_next_index, 2);
|
||||||
|
|
||||||
|
let proposal_instruction_account2 = governance_test
|
||||||
|
.get_proposal_instruction_account(&proposal_instruction_cookie2.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
proposal_instruction_cookie2.account,
|
||||||
|
proposal_instruction_account2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_remove_front_instruction() {
|
||||||
|
// 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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
let proposal_account = governance_test
|
||||||
|
.get_proposal_account(&proposal_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(proposal_account.instructions_count, 1);
|
||||||
|
assert_eq!(proposal_account.instructions_next_index, 2);
|
||||||
|
|
||||||
|
let proposal_instruction_account = governance_test
|
||||||
|
.get_account(&proposal_instruction_cookie.address)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(None, proposal_instruction_account);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_remove_instruction_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let token_owner_record_cookie2 = governance_test
|
||||||
|
.with_council_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
token_owner_record_cookie.token_owner = token_owner_record_cookie2.token_owner;
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::GoverningTokenOwnerOrDelegateMustSign.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_remove_instruction_with_proposal_not_editable_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.cancel_proposal(&proposal_cookie, &token_owner_record_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidStateCannotEditInstructions.into()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_remove_instruction_with_instruction_from_other_proposal_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_community_token_deposit(&realm_cookie)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let mut proposal_cookie = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut proposal_cookie2 = governance_test
|
||||||
|
.with_proposal(&token_owner_record_cookie, &mut account_governance_cookie)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let proposal_instruction_cookie2 = governance_test
|
||||||
|
.with_instruction(&mut proposal_cookie2, &token_owner_record_cookie, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
let err = governance_test
|
||||||
|
.remove_instruction(
|
||||||
|
&mut proposal_cookie,
|
||||||
|
&token_owner_record_cookie,
|
||||||
|
&proposal_instruction_cookie2,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.err()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
GovernanceError::InvalidProposalForProposalInstruction.into()
|
||||||
|
);
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue